From 273102323d2959bf5e24678a3388409e91e2ccb4 Mon Sep 17 00:00:00 2001 From: Sean Oliver <882952+seanoliver@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:25:39 -0700 Subject: [PATCH] feat(growth): filter OAuth/SSO redirect referrers from attribution (#44405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem GitHub OAuth redirects and Google SSO set the browser's Referer header to their domain when redirecting back to supabase.com. Our attribution pipeline treats these as genuine referral traffic, inflating the `github` channel by ~20K orgs/week. The internal referrer fix (GROWTH-647) surfaced this by reducing `unknown-internal` — it didn't cause the issue, it revealed OAuth noise that was previously hidden. ## What happened When users sign in with GitHub, the browser sends `Referer: https://github.com/`. GitHub's login pages use `origin-when-cross-origin` Referrer-Policy, which strips the path. So OAuth redirects arrive as bare `github.com/` — indistinguishable from a direct visit to github.com. Meanwhile, genuine GitHub referrals from repos/READMEs always include the full path because those pages use `no-referrer-when-downgrade`. We validated against `mart_marketing_organization_attribution`: 98.5% of GitHub-attributed orgs have bare `github.com/` as the referrer. Only ~250/week have specific paths (genuine referrals). ## Changes - Added `isOAuthRedirectReferrer()` to `first-referrer-cookie.ts` — identifies auth provider redirects: - `accounts.google.com` blocked entirely (dedicated SSO subdomain) - Bare `github.com/` blocked (OAuth redirect signature) - `github.com/` preserved (genuine repo/README referrals) - Wired into `shouldRefreshCookie()` so OAuth referrers never get stamped into cookies - Wired into `handlePageTelemetry()` referrer overrides as defense-in-depth - 17 new tests covering all OAuth patterns and edge cases ## Testing All 52 tests pass. New tests cover Google SSO (bare + with path), GitHub bare domain (with/without trailing slash), genuine GitHub referrals (repo, README, discussion, blob), explicit OAuth path, non-OAuth domains, empty/malformed URLs. Verified TDD — tests failed red before implementation, green after. Companion dbt PR in data-engineering handles historical data. GROWTH-732 --- packages/common/first-referrer-cookie.test.ts | 104 +++++++++++++++++- packages/common/first-referrer-cookie.ts | 41 +++++++ packages/common/telemetry.tsx | 9 +- 3 files changed, 151 insertions(+), 3 deletions(-) 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