From 2a2224b26760d4b4a92d2bad14ad66cd0f6d909b Mon Sep 17 00:00:00 2001 From: aliferous3 Date: Wed, 30 Sep 2026 02:42:41 +0400 Subject: [PATCH] fix: beacon-transport login_start so the event survives OAuth navigation The GitHub login button immediately navigates to /api/auth/login; a batched gtag send races the unload and can drop login_start. transport_type:'beacon' is a gtag control field (consumed by the tag, never sent as an event param) that flushes via sendBeacon at emit time. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/analytics/ga4.ts | 2 +- src/analytics/ga4Model.mjs | 7 +++++-- src/ledger/shared.tsx | 2 +- test/ga4-analytics.test.mjs | 10 ++++++++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/analytics/ga4.ts b/src/analytics/ga4.ts index b65aa88..9b1b0e7 100644 --- a/src/analytics/ga4.ts +++ b/src/analytics/ga4.ts @@ -41,7 +41,7 @@ export { export type GA4EventName = 'login_start' | 'login' | 'select_content'; export interface GA4EventProps { - login_start: { method: 'github' }; + login_start: { method: 'github'; transport_type: 'beacon' }; login: { method: 'github' }; select_content: { content_type: 'product_cta'; item_id: string }; } diff --git a/src/analytics/ga4Model.mjs b/src/analytics/ga4Model.mjs index 62492c5..93f4923 100644 --- a/src/analytics/ga4Model.mjs +++ b/src/analytics/ga4Model.mjs @@ -29,8 +29,11 @@ export const GA4_EVENT_PROPERTIES = { // Google recommended `login` event — method only, never any identity. login: { method: ['github'] }, - // Funnel step: the GitHub OAuth button was activated. - login_start: { method: ['github'] }, + // Funnel step: the GitHub OAuth button was activated. transport_type is + // a gtag control field (consumed by the tag, never sent as an ep param) + // — beacon sends immediately instead of batching, so the event survives + // the OAuth navigation that follows the click. + login_start: { method: ['github'], transport_type: ['beacon'] }, // Google recommended `select_content` — used for the blog/product CTA. // item_id carries a slug-like identifier only (validated below). select_content: { content_type: ['product_cta'], item_id: null }, diff --git a/src/ledger/shared.tsx b/src/ledger/shared.tsx index c0d1c64..98df053 100644 --- a/src/ledger/shared.tsx +++ b/src/ledger/shared.tsx @@ -43,7 +43,7 @@ export function GithubButton({ large = false, onLogin, remember = false }: { lar // pending flag lets the post-callback boot emit login_succeeded once. markLoginPending(); captureEvent('github_login_started', {}); - captureGoogleEvent('login_start', { method: 'github' }); + captureGoogleEvent('login_start', { method: 'github', transport_type: 'beacon' }); window.location.assign(remember ? '/api/auth/login?remember=1' : '/api/auth/login'); }; return ( diff --git a/test/ga4-analytics.test.mjs b/test/ga4-analytics.test.mjs index 6966ea8..f359be8 100644 --- a/test/ga4-analytics.test.mjs +++ b/test/ga4-analytics.test.mjs @@ -177,7 +177,13 @@ test('GA4 event set is exactly the acquisition funnel', () => { test('login events carry method:github only — never identity', () => { assert.deepEqual(validateGA4Event('login', { method: 'github' }), { method: 'github' }); - assert.deepEqual(validateGA4Event('login_start', { method: 'github' }), { method: 'github' }); + assert.deepEqual( + validateGA4Event('login_start', { method: 'github', transport_type: 'beacon' }), + { method: 'github', transport_type: 'beacon' }, + ); + // beacon is required — a missing it falls back to batched sends that lose + // the event to the OAuth navigation + assert.equal(validateGA4Event('login_start', { method: 'github' }), null); assert.equal(validateGA4Event('login', { method: 'github', user_id: 'x' }), null); assert.equal(validateGA4Event('login', { method: 'github', email: 'a@b.c' }), null); assert.equal(validateGA4Event('login', { method: 'google' }), null); @@ -266,7 +272,7 @@ test('GA4 wiring: boot init + pageview, popstate, navigate, login events', () => assert.ok((pages.match(/captureGooglePageview\(\)/g) || []).length >= 2, 'navigate + popstate must each emit a GA4 pageview'); const shared = src('src/ledger/shared.tsx'); - assert.match(shared, /captureGoogleEvent\('login_start', \{ method: 'github' \}\)/); + assert.match(shared, /captureGoogleEvent\('login_start', \{ method: 'github', transport_type: 'beacon' \}\)/); }); test('GA4 and PostHog are separate modules — no cross-import', () => {