feat: track sign_up events via GA4 Measurement Protocol#506
Conversation
Fires a server-side GA4 sign_up event from the Ory OAuth callback when a new account is bootstrapped, stitched to the web session via the _ga cookies so the event attributes to its traffic source. Falls back to a random client_id when the cookie is absent so counts stay intact.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85429c24fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
LGTM — straightforward, well-tested analytics addition with no security-sensitive logic changes.
What was reviewed: the new GA4 Measurement Protocol client (cookie parsing, fallback client_id, fire-and-forget send via after()), the signup-tracking wrapper, and its wiring into the Ory callback route. Checked that tracking only fires after a successful bootstrap (not before, not on bootstrap failure) and that a GA4 send failure can't block the auth redirect. Two candidate issues (stale-read sign_up misfire, missing Content-Type header) were investigated and refuted by the bug hunting system.
Extended reasoning...
Overview
Adds a server-side GA4 Measurement Protocol client (measurement-protocol.server.ts) and a thin wrapper (signup-tracking.ts) that fires a sign_up event from the Ory OAuth callback route when a new dashboard user is bootstrapped. Also adds a readKratosAuthMethod helper to session.ts, two optional env vars (GA4_MEASUREMENT_ID/GA4_API_SECRET), and unit/integration tests.
Security risks
No new attack surface: the GA4 API secret and measurement ID are read from server-only env vars and only used to build an outbound URL to Google's collect endpoint — never exposed to the client or logged. Client/session IDs are parsed from the _ga/_ga_<id> cookies with defensive regexes that reject malformed input (return null rather than throwing or passing through unsanitized data). The auth flow itself (token exchange, bootstrap, session sealing) is untouched; the only functional addition to the callback route is a fire-and-forget analytics call gated behind a successful bootstrap, dispatched via after() so it cannot block or fail the redirect.
Level of scrutiny
Moderate. This touches files in the auth callback path, which normally warrants a close look, but the diff to that path is minimal and additive (one new import, one new call inside the existing bootstrap branch) — the pre-existing auth/session logic is unchanged. The bulk of the new code is a self-contained, side-effect-only analytics client with clear unit test coverage of its parsing helpers and an integration test verifying the tracking call fires/doesn't fire in the right branches.
Other factors
Both candidate issues raised by finder agents (a possible sign_up misfire on a transient external_id read, and a missing Content-Type header on the GA4 request) were examined by verifiers and refuted. Test coverage is solid for the new surface (cookie parsing edge cases, fallback client ID shape, callback branch behavior). No CODEOWNER-restricted paths appear to be involved, and there's no outstanding reviewer feedback in the timeline to address.
The collect URL carries api_secret as a query param, which FetchInstrumentation would otherwise export on trace spans.
A missing _ga cookie means the user blocked analytics; emitting with a synthetic client_id would bypass that choice.
Fires a server-side GA4
sign_upevent from the Ory OAuth callback when a new account is bootstrapped, stitched to the web session via the_gacookies so the event attributes to its traffic source. Falls back to a random client_id when the cookie is absent so counts stay intact.