Repository navigation
feat(analytics): send a hashed user id to GA and server-render the GTM snippet - #934
Conversation
Bundle sizeShared by all pages: 624.6 kB (-0.0 kB) ⚪ No route changed by more than 1 kB. ✅ First load = polyfills + shared |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #934 +/- ##
========================================
+ Coverage 67.9% 68.0% +0.2%
========================================
Files 379 381 +2
Lines 44188 44185 -3
Branches 2380 2399 +19
========================================
+ Hits 29974 30039 +65
+ Misses 14165 14098 -67
+ Partials 49 48 -1
🚀 New features to boost your workflow:
|
d9af15d to
7252639
Compare
7252639 to
ec12fc0
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Web Crypto test setup, expiry handling and coverage, and hash-failure state clearing remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds SHA-256 hashed user-ID tracking for authenticated GA sessions across login, hydration, refresh, and logout.
Changes:
- Adds and mounts
useTrackUserId. - Emits deduplicated GTM
user_updateevents. - Adds tracking tests for authentication and logout behavior.
File summaries
| File | Summary |
|---|---|
src/providers.tsx |
Mounts user-ID telemetry in Telemetry. |
src/lib/useTrackUserId.ts |
Implements hashed user-ID tracking and session gating. |
src/lib/useTrackUserId.test.ts |
Tests tracking, deduplication, and logout behavior. |
Review details
Suppressed comments (5)
src/lib/useTrackUserId.test.ts:47
- [medium] The new token-expiry gate is not covered: the tests exercise anonymous and valid-token users, but never assert that a non-anonymous user with an expired
expires_atproduces no hashed event. Since avoiding tracking a rehydrated dead session is a stated behavior of this hook, add an expired-session case so a regression is caught. Confidence: high.
test('sends nothing for an anonymous session', async () => {
user = { ...authed('anonymous@ads'), anonymous: true };
renderHook(() => useTrackUserId());
await waitFor(() => expect(sendGTMEvent).not.toHaveBeenCalled());
});
src/lib/useTrackUserId.ts:45
- [medium] Impact: if hashing fails after a user ID was already sent (for example during a user switch or a transient Web Crypto failure),
lastSentRefremains the old hash and GA continues attributing subsequent events to the wrong account. Clear the tracked ID and emit the null update when the active hash cannot be produced, then allow a later refresh to retry. Confidence: high.
.catch((err: unknown) => {
logger.error({ err }, 'useTrackUserId: hash error');
});
src/lib/useTrackUserId.ts:23
- This validity check only runs when the
userobject changes, so it does not react whenexpires_atpasses. If the periodic/api/userrefresh returns an invalid session,UserSyncleaves the previous valid store user in place (src/pages/_app.tsx:187-192), and the previously sent hash remains active; subsequent anonymous traffic can therefore stay attributed to an expired session. Clear the tracking state when the session expires or arrange for invalid refresh results to clear the store.
const username = isValidToken(user) && !user.anonymous ? user.username : null;
src/lib/useTrackUserId.ts:23
- Impact: an invalid persisted session can still emit a user ID.
isValidTokenultimately comparescurrentTime >= parseInt(expires_at, 10), so a non-numeric but non-emptyexpires_atbecomesNaNand is treated as not expired; this line will then hash/send the username even though the expiry is unusable. Please make the shared expiry validation require a finite numeric timestamp (or add an equivalent guard before tracking). Confidence: high.
const username = isValidToken(user) && !user.anonymous ? user.username : null;
src/lib/useTrackUserId.ts:23
- The expiry check is the key protection for rehydrated sessions, but the added tests cover only valid, anonymous, token-refresh, and undefined users. Add an expired-token case that asserts no
user_updateis emitted (and covers the transition from a previously tracked user), so a regression in this new privacy-sensitive gate is caught. Confidence: high.
const username = isValidToken(user) && !user.anonymous ? user.username : null;
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ec12fc0 to
f65c21e
Compare
GoogleTagManager rendered inside <Layout> via next/script at the default afterInteractive strategy, which only runs client-side in the pages router, so GTM flagged the tag as incorrectly placed on / and /abs/*. Inlines Google's snippet as the first child of <Head> in _document, gated on NEXT_PUBLIC_GTM_ID.
Bumblebee tags the GA session with a hashed user id after login; Nectar sent nothing. useTrackUserId sends a user_update GTM event with the SHA-256 hash of the username on login, clears it on logout, and dedupes against the 5-minute token refresh. The hash matches Bumblebee's, so one account maps to one GA User-ID across both frontends.
The Google tag fires from <head> before React mounts, so the client hook's push always lost the race and page_view carried no User-ID. getGtmUserId hashes the username server-side, from the session data injectSessionGSSP already puts in __NEXT_DATA__, and getGtmSnippet seeds it ahead of gtm.start.
Copilot review flagged that isValidToken parses expires_at with parseInt and compares with >=, so a malformed expiry compares false against NaN and reads as "not expired" — defeating the gate that stops a rehydrated dead session emitting an id. - isTrackableSession replaces the inline checks in both the hook and the server seed; standalone rather than built on isValidToken, which also closes a circular import through api.ts - Resets the remembered hash when hashing fails, so GA stops attributing hits to whoever was last hashed - Covers expired, unparseable and empty expires_at, which had no tests
4b859f0 to
4525282
Compare
GTM flagged the container tag as incorrectly placed because it rendered client-side only, and Nectar sent no user id at all, so logged-in traffic couldn't be segmented the way Bumblebee segments it.
Known limitation: username is the user's editable email, so an email change mints a new GA User-ID, same as Bumblebee today. Companion PR adsabs/bumblebee#2381 clears the id on logout.