mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## Problem The `_sb_first_referrer` cookie isn't working. The www middleware matcher explicitly excludes `/dashboard` and `/docs`, so the cookie never gets stamped for Studio or Docs traffic. PostHog confirmed: only 1 event with `first_referrer_cookie_present=true` out of ~46.5M Studio pageviews in the last 7 days. ## Background: what the matcher does In Next.js, the `matcher` config controls which incoming requests the middleware function even runs on. If a path doesn't match, the middleware is skipped entirely — the request passes through untouched. If it matches, the middleware runs and can mutate the response (set cookies, headers, etc.). This matters because Studio's SPA navigation works via silent `/_next/data/` JSON fetches. If middleware runs on those requests and returns `NextResponse.next()` with any mutations, it breaks those fetches and causes full page reloads instead of client-side transitions. ## What we tried before | PR | www runs on `/dashboard`? | Studio `proxy.ts` runs on all routes? | Result | |---|---|---|---| | **#42768** (Attempt 1) | ✅ Yes — and also intercepts `_next/data` | ✅ Yes — `matcher` config removed, stamps cookie everywhere | Full page reloads in Studio | | **#43129** (Full revert) | ❌ No — www middleware deleted entirely | ❌ No — restored to `matcher: '/api/*'` only | Back to baseline, no cookie stamping anywhere | | **#43153** (Attempt 2) | ❌ No — `/dashboard` explicitly excluded | ✅ Yes — `matcher` config removed again, stamps cookie everywhere | Full page reloads in Studio again | | **#43189** (Attempt 3) | ❌ No — same as #43153 | ✅ Yes — `matcher` config still removed, cookie stamping made conditional | Still broken | | **#43190** (Ivan's fix) | ❌ No — `/dashboard` still excluded | ❌ No — restored to `matcher: '/api/*'` only | Works — but cookie never stamps for `/dashboard` traffic | | **#43413** (this PR) | ✅ Yes — sets diagnostic cookie only, no attribution stamping yet | ❌ No — unchanged, still `matcher: '/api/*'` only | ❓ Untested in prod | The common factor in every failure: Studio's `proxy.ts` ran on all routes (including `_next/data` requests), which broke SPA navigation. This PR is the first one that runs www middleware on `/dashboard` while Studio's `proxy.ts` stays in its original narrow `/api/*` scope. ## What changed This is Phase 1 of a two-phase rollout. We remove `dashboard|docs` from the matcher's negative lookahead so www middleware runs on those paths — but instead of stamping cookies, we set a short-lived (60s) diagnostic cookie `_sb_mw_diag` on `/dashboard` and `/docs` requests. The diagnostic cookie encodes `hit=1&would_stamp={0|1}&has_cookie={0|1}`, which Studio telemetry reads on the initial pageview and reports to PostHog as `mw_diag_hit`, `mw_diag_would_stamp`, and `mw_diag_has_existing_cookie` properties. A cookie rather than a header because response headers aren't readable by JS. It also tests the actual Set-Cookie mutation path that Phase 2 will use (which is what Next.js issue #41885 is specifically about). Phase 1 answers two key questions before we commit to Phase 2: 1. Does expanding the matcher break Studio SPA navigation? 2. What % of /dashboard arrivals would get a first-referrer cookie stamped in Phase 2? ## Phase 2 readiness criteria **Important caveat**: `mw_diag_*` data reflects consented users only and may under-represent first-visit anonymous traffic. The Phase 2 decision should account for this — the actual middleware execution rate is likely higher than what PostHog reports. ### PostHog query spec **Middleware execution rate**: Of all Studio initial pageviews on `/dashboard` or `/docs` paths, what percentage have `mw_diag_hit = true`? Expected: >= 90%. Below 70% warrants investigation (could indicate edge caching bypassing middleware, or a matcher configuration issue). ``` Filter: event = "$pageview" AND (current_url contains "/dashboard" OR current_url contains "/docs") Breakdown: mw_diag_hit (true vs null/missing) Metric: count(mw_diag_hit = true) / count(all) * 100 ``` **Would-stamp rate**: Of events with `mw_diag_hit = true`, what percentage have `mw_diag_would_stamp = true`? This tells us what percentage of Phase 2 traffic would actually get a cookie stamped. No hard threshold — unexpected values (< 5% or > 95%) suggest a logic bug worth investigating before Phase 2. ``` Filter: event = "$pageview" AND mw_diag_hit = true Breakdown: mw_diag_would_stamp (true vs false) Metric: count(mw_diag_would_stamp = true) / count(all) * 100 ``` **Existing cookie rate**: Of events with `mw_diag_hit = true`, what percentage have `mw_diag_has_existing_cookie = true`? This tells us how many users already have the cookie from a prior www visit. ``` Filter: event = "$pageview" AND mw_diag_hit = true Breakdown: mw_diag_has_existing_cookie (true vs false) Metric: count(mw_diag_has_existing_cookie = true) / count(all) * 100 ``` ### Go / no-go threshold table | Signal | Go | Investigate | No-Go | |---|---|---|---| | `mw_diag_hit` rate (% of /dashboard+/docs pageviews) | >= 90% | 70-90% | < 70% | | SPA navigation errors (Sentry / Vercel logs) | No increase | < 0.1% increase | > 0.5% increase | | Middleware p99 latency (Vercel function logs) | < 50ms added | 50-100ms | > 100ms | | Sample volume in first 24h | > 1,000 events | 100-1,000 (extend window) | < 100 (insufficient data) | ## Changes - `apps/www/middleware.ts`: Removed `dashboard|docs` from matcher; added `isDashboardOrDocs` guard that sets `_sb_mw_diag` diagnostic cookie instead of stamping attribution - `apps/www/middleware.test.ts`: 12 tests covering cookie stamping on www paths, diagnostic cookie encoding for all scenarios (external referrer, direct nav, internal referrer, existing cookie) - `packages/common/first-referrer-cookie.ts`: Exported `MW_DIAG_COOKIE_NAME`, `MwDiagData` type, and `parseMwDiagCookie()` helper - `packages/common/telemetry.tsx`: Reads `_sb_mw_diag` on initial Studio pageview; reports `mw_diag_*` properties to PostHog ## Testing Unit tests pass (13/13 www, 31/31 first-referrer-cookie). Production validation needed: - [ ] Studio SPA navigation works (tab changes, SQL editor, no full page reloads) - [ ] PostHog shows `mw_diag_hit = true` on Studio initial pageviews - [ ] `mw_diag_would_stamp` distribution looks reasonable before enabling Phase 2 - [ ] Monitor 24h before Phase 2 Ref: GROWTH-625 / GROWTH-668
136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
import { NextRequest } from 'next/server'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { FIRST_REFERRER_COOKIE_NAME, MW_DIAG_COOKIE_NAME } from 'common/first-referrer-cookie'
|
|
|
|
import { middleware } from './middleware'
|
|
|
|
function makeRequest(
|
|
url: string,
|
|
{ referer, hasCookie }: { referer?: string; hasCookie?: boolean } = {}
|
|
): NextRequest {
|
|
const req = new NextRequest(new URL(url, 'https://supabase.com'), {
|
|
headers: referer ? { referer } : {},
|
|
})
|
|
if (hasCookie) {
|
|
req.cookies.set(FIRST_REFERRER_COOKIE_NAME, 'existing')
|
|
}
|
|
return req
|
|
}
|
|
|
|
function parseDiagCookie(value: string) {
|
|
const params = new URLSearchParams(value)
|
|
return {
|
|
hit: params.get('hit') === '1',
|
|
would_stamp: params.get('would_stamp') === '1',
|
|
has_cookie: params.get('has_cookie') === '1',
|
|
}
|
|
}
|
|
|
|
describe('www middleware', () => {
|
|
describe('cookie stamping on normal paths', () => {
|
|
it('stamps cookie for external referrer on www path', () => {
|
|
const req = makeRequest('/pricing', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeDefined()
|
|
})
|
|
|
|
it('does not stamp cookie for internal referrer', () => {
|
|
const req = makeRequest('/pricing', { referer: 'https://supabase.com/docs' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('dashboard/docs diagnostic guard', () => {
|
|
it('sets diagnostic cookie on /dashboard paths', () => {
|
|
const req = makeRequest('/dashboard/project/123', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(MW_DIAG_COOKIE_NAME)).toBeDefined()
|
|
})
|
|
|
|
it('sets diagnostic cookie on /docs paths', () => {
|
|
const req = makeRequest('/docs/guides/auth', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(MW_DIAG_COOKIE_NAME)).toBeDefined()
|
|
})
|
|
|
|
it('encodes hit=1 in diagnostic cookie', () => {
|
|
const req = makeRequest('/dashboard/project/123', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
const raw = res.cookies.get(MW_DIAG_COOKIE_NAME)?.value ?? ''
|
|
const diag = parseDiagCookie(raw)
|
|
|
|
expect(diag.hit).toBe(true)
|
|
})
|
|
|
|
it('encodes would_stamp=1 for external referrer with no existing cookie', () => {
|
|
const req = makeRequest('/dashboard/project/123', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
const raw = res.cookies.get(MW_DIAG_COOKIE_NAME)?.value ?? ''
|
|
const diag = parseDiagCookie(raw)
|
|
|
|
expect(diag.would_stamp).toBe(true)
|
|
expect(diag.has_cookie).toBe(false)
|
|
})
|
|
|
|
it('encodes would_stamp=0 for direct navigation (no referrer)', () => {
|
|
const req = makeRequest('/dashboard/project/123')
|
|
const res = middleware(req)
|
|
const raw = res.cookies.get(MW_DIAG_COOKIE_NAME)?.value ?? ''
|
|
const diag = parseDiagCookie(raw)
|
|
|
|
expect(diag.would_stamp).toBe(false)
|
|
expect(diag.has_cookie).toBe(false)
|
|
})
|
|
|
|
it('encodes would_stamp=0 for internal referrer', () => {
|
|
const req = makeRequest('/dashboard/project/123', {
|
|
referer: 'https://supabase.com/pricing',
|
|
})
|
|
const res = middleware(req)
|
|
const raw = res.cookies.get(MW_DIAG_COOKIE_NAME)?.value ?? ''
|
|
const diag = parseDiagCookie(raw)
|
|
|
|
expect(diag.would_stamp).toBe(false)
|
|
})
|
|
|
|
it('encodes has_cookie=1 when first-referrer cookie is already present', () => {
|
|
const req = makeRequest('/dashboard/project/123', {
|
|
referer: 'https://google.com',
|
|
hasCookie: true,
|
|
})
|
|
const res = middleware(req)
|
|
const raw = res.cookies.get(MW_DIAG_COOKIE_NAME)?.value ?? ''
|
|
const diag = parseDiagCookie(raw)
|
|
|
|
expect(diag.has_cookie).toBe(true)
|
|
})
|
|
|
|
it('does NOT stamp first-referrer cookie on /dashboard paths', () => {
|
|
const req = makeRequest('/dashboard/project/123', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
|
|
it('does NOT stamp first-referrer cookie on /docs paths', () => {
|
|
const req = makeRequest('/docs/guides/auth', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
|
|
it('does NOT set diagnostic cookie on normal www paths', () => {
|
|
const req = makeRequest('/pricing', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(MW_DIAG_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
})
|
|
})
|