feat(growth): filter OAuth/SSO redirect referrers from attribution (#44405)

## 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/<specific-path>` 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
This commit is contained in:
Sean Oliver
2026-04-01 11:25:39 -07:00
committed by GitHub
parent 91a8d59e43
commit 273102323d
3 changed files with 151 additions and 3 deletions

View File

@@ -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)
})
})
})

View File

@@ -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) }
}

View File

@@ -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