Skip to content

Commit 777f25f

Browse files
chanceaclarkclaude
andcommitted
CATALYST-1532: fix(core) - Fix analytics visit count inflation (#2916)
Resolve inflated visitor counts caused by three issues: - Visit cookie used a fixed TTL instead of a sliding window, creating new visits for users browsing longer than 30 minutes. Now refreshes the cookie TTL on every request. - Prefetch and RSC requests triggered new visit and product view events. Now guards against Next-Router-Prefetch and RSC headers so only real navigations fire analytics mutations. - Locale redirects from withIntl bypassed analytics entirely. Reorder middleware so withAnalyticsCookies runs before withIntl, ensuring cookies survive redirect responses. Fixes CATALYST-1532 Co-authored-by: Claude <noreply@anthropic.com>
1 parent dda9016 commit 777f25f

4 files changed

Lines changed: 29 additions & 11 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bigcommerce/catalyst-core": patch
3+
---
4+
5+
Fix analytics visit count inflation by implementing a sliding window for the visit cookie TTL, guarding against prefetch/RSC requests creating spurious visits, and reordering middleware so analytics cookies survive locale redirects.

core/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { withRoutes } from './middlewares/with-routes';
77

88
export const middleware = composeMiddlewares(
99
withAuth,
10-
withIntl,
1110
withAnalyticsCookies,
11+
withIntl,
1212
withChannelId,
1313
withRoutes,
1414
);

core/middlewares/with-analytics-cookies.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,30 @@ import { MiddlewareFactory } from './compose-middlewares';
1212

1313
export const withAnalyticsCookies: MiddlewareFactory = (next) => {
1414
return async (request, event) => {
15-
let visitorId = await getVisitorIdCookie();
16-
let visitId = await getVisitIdCookie();
15+
const existingVisitorId = await getVisitorIdCookie();
16+
const existingVisitId = await getVisitIdCookie();
1717

18-
if (!visitorId || !isUuid(visitorId)) {
19-
visitorId = uuidv4();
20-
}
18+
const isPrefetch = request.headers.get('Next-Router-Prefetch') === '1';
19+
const isRSC = request.headers.get('RSC') === '1';
20+
21+
const visitorId = existingVisitorId && isUuid(existingVisitorId) ? existingVisitorId : uuidv4();
2122

22-
// Update the visitorId cookie every time
2323
await setVisitorIdCookie(visitorId);
2424

25-
if (!visitId || !isUuid(visitId)) {
26-
visitId = uuidv4();
27-
await setVisitIdCookie(visitId);
25+
const hasValidVisit = existingVisitId != null && isUuid(existingVisitId);
2826

27+
if (hasValidVisit) {
28+
// Sliding window: refresh the TTL on every request
29+
await setVisitIdCookie(existingVisitId);
30+
} else if (!isPrefetch && !isRSC) {
31+
// New visit on a real navigation: create cookie and fire event
32+
const visitId = uuidv4();
33+
34+
await setVisitIdCookie(visitId);
2935
event.waitUntil(recordNewVisit(request, visitorId, visitId));
3036
}
37+
// Prefetch/RSC with no valid visit: skip entirely so the
38+
// subsequent real navigation properly detects a new visit.
3139

3240
return next(request, event);
3341
};

core/middlewares/with-routes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,12 @@ export const withRoutes: MiddlewareFactory = () => {
369369
case 'Product': {
370370
url = `/${locale}/product/${node.entityId}`;
371371

372-
event.waitUntil(recordProductVisit(request, node.entityId));
372+
const isPrefetch = request.headers.get('Next-Router-Prefetch') === '1';
373+
const isRSC = request.headers.get('RSC') === '1';
374+
375+
if (!isPrefetch && !isRSC) {
376+
event.waitUntil(recordProductVisit(request, node.entityId));
377+
}
373378

374379
break;
375380
}

0 commit comments

Comments
 (0)