diff --git a/packages/common/first-referrer-cookie.test.ts b/packages/common/first-referrer-cookie.test.ts index 3b83b6ca4df..f909691a460 100644 --- a/packages/common/first-referrer-cookie.test.ts +++ b/packages/common/first-referrer-cookie.test.ts @@ -3,9 +3,10 @@ import { describe, expect, it } from 'vitest' import { buildFirstReferrerData, FIRST_REFERRER_COOKIE_NAME, - MW_DIAG_COOKIE_NAME, hasPaidSignals, isExternalReferrer, + isOAuthRedirectReferrer, + MW_DIAG_COOKIE_NAME, parseFirstReferrerCookie, parseMwDiagCookie, serializeFirstReferrerCookie, @@ -36,6 +37,75 @@ describe('first-referrer-cookie', () => { }) }) + describe('isOAuthRedirectReferrer', () => { + // Google SSO — block entire domain + it('returns true for accounts.google.com (bare)', () => { + expect(isOAuthRedirectReferrer('https://accounts.google.com/')).toBe(true) + }) + + it('returns true for accounts.google.com with path', () => { + expect( + isOAuthRedirectReferrer('https://accounts.google.com/o/oauth2/auth?client_id=abc') + ).toBe(true) + }) + + // GitHub OAuth — block bare domain only + it('returns true for bare github.com/', () => { + expect(isOAuthRedirectReferrer('https://github.com/')).toBe(true) + }) + + it('returns true for bare github.com (no trailing slash)', () => { + expect(isOAuthRedirectReferrer('https://github.com')).toBe(true) + }) + + // GitHub genuine referrals — preserve these + it('returns false for github.com with repo path', () => { + expect(isOAuthRedirectReferrer('https://github.com/supabase/supabase')).toBe(false) + }) + + it('returns false for github.com with README path', () => { + expect( + isOAuthRedirectReferrer('https://github.com/supabase/supabase?tab=readme-ov-file') + ).toBe(false) + }) + + it('returns false for github.com with discussion path', () => { + expect(isOAuthRedirectReferrer('https://github.com/orgs/supabase/discussions/42949')).toBe( + false + ) + }) + + it('returns false for github.com with blob path', () => { + expect( + isOAuthRedirectReferrer('https://github.com/supabase/supabase/blob/master/README.md') + ).toBe(false) + }) + + // GitHub OAuth explicit path (rare, but should still be caught) + it('returns true for github.com/login/oauth/authorize', () => { + expect( + isOAuthRedirectReferrer('https://github.com/login/oauth/authorize?client_id=abc') + ).toBe(true) + }) + + // Non-OAuth domains — should not match + it('returns false for google.com (search)', () => { + expect(isOAuthRedirectReferrer('https://www.google.com/')).toBe(false) + }) + + it('returns false for claude.ai', () => { + expect(isOAuthRedirectReferrer('https://claude.ai/')).toBe(false) + }) + + it('returns false for empty string', () => { + expect(isOAuthRedirectReferrer('')).toBe(false) + }) + + it('returns false for malformed URL', () => { + expect(isOAuthRedirectReferrer('not-a-url')).toBe(false) + }) + }) + describe('buildFirstReferrerData', () => { it('handles malformed landing URL gracefully', () => { const data = buildFirstReferrerData({ @@ -333,5 +403,37 @@ describe('first-referrer-cookie', () => { }) ).toEqual({ stamp: false }) }) + + it('does not stamp for GitHub OAuth redirect (bare domain)', () => { + const result = shouldRefreshCookie(false, { + referrer: 'https://github.com/', + url: 'https://supabase.com/dashboard', + }) + expect(result.stamp).toBe(false) + }) + + it('does not stamp for Google SSO redirect', () => { + const result = shouldRefreshCookie(false, { + referrer: 'https://accounts.google.com/', + url: 'https://supabase.com/dashboard', + }) + expect(result.stamp).toBe(false) + }) + + it('still stamps for genuine GitHub referral with path', () => { + const result = shouldRefreshCookie(false, { + referrer: 'https://github.com/supabase/supabase?tab=readme-ov-file', + url: 'https://supabase.com/', + }) + expect(result.stamp).toBe(true) + }) + + it('still re-stamps existing cookie for paid signals regardless of OAuth referrer', () => { + const result = shouldRefreshCookie(true, { + referrer: 'https://github.com/', + url: 'https://supabase.com/pricing?gclid=abc123', + }) + expect(result.stamp).toBe(true) + }) }) }) diff --git a/packages/common/first-referrer-cookie.ts b/packages/common/first-referrer-cookie.ts index 5617d764e5f..ac2672100e9 100644 --- a/packages/common/first-referrer-cookie.ts +++ b/packages/common/first-referrer-cookie.ts @@ -92,6 +92,43 @@ export function isExternalReferrer(referrer: string): boolean { } } +/** + * Returns true if the referrer URL is an OAuth/SSO redirect that should NOT + * be treated as a genuine traffic source. + * + * A referrer should reflect how someone discovered Supabase, not how they + * authenticated. This function identifies auth provider redirects: + * - accounts.google.com — blocked entirely (dedicated SSO subdomain) + * - github.com with no path (bare domain) — blocked (OAuth strips the path + * via origin-when-cross-origin Referrer-Policy) + * - github.com/login/oauth/* — blocked (explicit OAuth path, rare) + * - github.com with a specific path — allowed (genuine repo/README referrals) + */ +export function isOAuthRedirectReferrer(referrer: string): boolean { + if (!referrer) return false + try { + const url = new URL(referrer) + const hostname = url.hostname + + // Google SSO — entire subdomain is auth traffic + if (hostname === 'accounts.google.com') return true + + // GitHub — bare domain (no meaningful path) is OAuth redirect noise + if (hostname === 'github.com') { + const path = url.pathname + if (path === '/') return true + // Explicit OAuth path (rare — GitHub usually strips this) + if (path.startsWith('/login/oauth')) return true + // Any other path = genuine referral (README, repo, discussion, etc.) + return false + } + + return false + } catch { + return false + } +} + // --------------------------------------------------------------------------- // UTM + click-ID extraction // --------------------------------------------------------------------------- @@ -207,7 +244,10 @@ export function hasPaidSignals(url: URL): boolean { * Decides whether the first-referrer cookie should be (re-)stamped. * * - No cookie + external referrer → stamp (first visit attribution) + * - No cookie + OAuth/SSO redirect referrer → skip (auth ≠ discovery) * - Cookie exists + paid signals in URL → stamp (paid traffic refresh) + * Note: OAuth check intentionally skipped for paid refresh — the paid + * signal comes from the URL (gclid, utm_medium=cpc), not the referrer. * - Otherwise → skip */ export function shouldRefreshCookie( @@ -215,6 +255,7 @@ export function shouldRefreshCookie( request: { referrer: string; url: string } ): { stamp: boolean } { if (!existingCookie) { + if (isOAuthRedirectReferrer(request.referrer)) return { stamp: false } return { stamp: isExternalReferrer(request.referrer) } } diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index d9b3efaf688..21001f778b7 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -15,6 +15,7 @@ import { post } from './fetchWrappers' import type { FirstReferrerData, MwDiagData } from './first-referrer-cookie' import { isExternalReferrer, + isOAuthRedirectReferrer, parseFirstReferrerCookie, parseMwDiagCookie, } from './first-referrer-cookie' @@ -123,7 +124,10 @@ function handlePageTelemetry({ const storedReferrer = telemetryDataOverride?.ph?.referrer const shouldUseStoredReferrer = Boolean( - storedReferrer && isExternalReferrer(storedReferrer) && !isExternalReferrer(liveReferrer) + storedReferrer && + isExternalReferrer(storedReferrer) && + !isOAuthRedirectReferrer(storedReferrer) && + (!isExternalReferrer(liveReferrer) || isOAuthRedirectReferrer(liveReferrer)) ) const pageData = telemetryDataOverride @@ -145,7 +149,8 @@ function handlePageTelemetry({ if ( firstReferrerData && isExternalReferrer(firstReferrerData.referrer) && - !isExternalReferrer(pageData.ph.referrer) + !isOAuthRedirectReferrer(firstReferrerData.referrer) && + (!isExternalReferrer(pageData.ph.referrer) || isOAuthRedirectReferrer(pageData.ph.referrer)) ) { pageData.ph.referrer = firstReferrerData.referrer firstReferrerCookieConsumed = true