Files
supabase/apps/www/middleware.test.ts
T
Sean Oliver 8ebbad3a5b feat(growth): expand www middleware to /dashboard and /docs (Phase 1 - instrumentation only) (#43413)
## 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
2026-03-09 11:47:20 -07:00

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