Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/analytics/ga4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down
7 changes: 5 additions & 2 deletions src/analytics/ga4Model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion src/ledger/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
10 changes: 8 additions & 2 deletions test/ga4-analytics.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading