mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## Summary Fixes GROWTH-625. Preserves first-touch attribution across app boundaries by persisting external referrer context at the edge and consuming it on Studio's initial pageview. When users come from an external source to www/docs and then navigate to Studio, Studio often only sees the internal `supabase.com` hop. This change preserves the original external context so first-touch attribution is retained. ## What changed - **Shared first-referrer cookie utilities** (`packages/common/first-referrer-cookie.ts`): - `isExternalReferrer`, `buildFirstReferrerData`, `serializeFirstReferrerCookie`, `parseFirstReferrerCookie` - `hasPaidSignals` — detects click IDs (gclid, fbclid, etc.) and paid utm_medium values - `shouldRefreshCookie` — centralizes stamp-or-skip decision for all apps - `stampFirstReferrerCookie` — shared middleware helper used by all apps (extracted from duplicated inline logic) - **Edge middleware on all apps** — stamps cookie for external visitors, refreshes on paid signals: - `apps/www/middleware.ts` (simplified to use shared helper) - `apps/docs/middleware.ts` (simplified to use shared helper) - `apps/studio/proxy.ts` (integrated into existing proxy file) - **Docs middleware matcher** — broadened from `/reference/:path*` to all non-static paths so the first-referrer cookie is stamped on all docs pages, not just reference paths - **Telemetry** — Studio consumes cookie on initial pageview (`packages/common/telemetry.tsx`). `handlePageTelemetry` refactored from 7 positional params to an options object for readability. - **Tests** — 22 unit tests covering all utilities and edge cases (including direct-navigation scenario) ## Behavior - Writes `_sb_first_referrer` cookie when: - cookie is not already set and request has an external referrer, OR - cookie exists but incoming URL has paid traffic signals (click IDs or paid utm_medium) - Cookie: 365-day TTL, `domain=supabase.com`, `sameSite=lax`, `secure=true` in production - On first Studio pageview, if current referrer is internal and cookie has external context: - use persisted external referrer - apply persisted UTM/click-id/landing-url attribution props - Measurement properties: `first_referrer_cookie_present`, `first_referrer_cookie_consumed` ## Manual testing 1. Visit `supabase.com/pricing?utm_source=google&utm_medium=cpc` from an external referrer (or use DevTools to set a `Referer` header) 2. Check `_sb_first_referrer` cookie is set in Application > Cookies 3. Navigate to Studio (`supabase.com/dashboard`) 4. In PostHog (or browser network tab), verify the first `$pageview` event has: - `first_referrer_cookie_present: true` - `first_referrer_cookie_consumed: true` - `$utm_source: "google"`, `$utm_medium: "cpc"` - `$referrer` points to the external source, not `supabase.com` 5. Verify subsequent route changes do NOT include `first_referrer_cookie_*` properties ## Review feedback addressed - Added `secure: true` flag on production cookies (Pam's first comment) - Fixed inaccurate JSDoc on `utms` field — keys retain `utm_` prefix (Pam's fourth comment) - Added test coverage for edge cases: malformed URLs, multi-cookie headers, http:// referrers (Pam's sixth comment) - Docs matcher broadening: fast-path exit on cookie-exists check keeps overhead minimal, exclusion list is correct - Extracted shared middleware helper to eliminate duplication across 3 apps - Refactored `handlePageTelemetry` from positional params to options object - Removed redundant null check in `hasPaidSignals` - Added direct-navigation test case - Deleted dead `apps/learn/middleware.ts` - Fixed studio build: integrated cookie stamping into existing `proxy.ts` (Next.js 16 rejects both middleware.ts and proxy.ts) --------- Co-authored-by: pamelachia <26612111+pamelachia@users.noreply.github.com> Co-authored-by: Pamela Chia <pamelachiamayyee@gmail.com>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import { clientSdkIds } from '~/content/navigation.references'
|
|
import { BASE_PATH } from '~/lib/constants'
|
|
import { stampFirstReferrerCookie } from 'common/first-referrer-cookie'
|
|
import { isbot } from 'isbot'
|
|
import { NextResponse, type NextRequest } from 'next/server'
|
|
|
|
const REFERENCE_PATH = `${BASE_PATH ?? ''}/reference`
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const url = new URL(request.url)
|
|
|
|
// Non-reference paths: just handle the first-referrer cookie and pass through
|
|
if (!url.pathname.startsWith(REFERENCE_PATH)) {
|
|
const response = NextResponse.next()
|
|
stampFirstReferrerCookie(request, response)
|
|
return response
|
|
}
|
|
|
|
// Reference paths: existing rewrite logic with cookie stamping on every response
|
|
|
|
if (isbot(request.headers.get('user-agent'))) {
|
|
let [, lib, maybeVersion, ...slug] = url.pathname.replace(REFERENCE_PATH, '').split('/')
|
|
|
|
if (clientSdkIds.includes(lib)) {
|
|
const version = /v\d+/.test(maybeVersion) ? maybeVersion : undefined
|
|
if (!version) {
|
|
slug = [maybeVersion, ...slug]
|
|
}
|
|
|
|
if (slug.length > 0) {
|
|
const rewriteUrl = new URL(url)
|
|
rewriteUrl.pathname = (BASE_PATH ?? '') + '/api/crawlers'
|
|
const response = NextResponse.rewrite(rewriteUrl)
|
|
stampFirstReferrerCookie(request, response)
|
|
return response
|
|
}
|
|
}
|
|
}
|
|
|
|
const [, lib, maybeVersion] = url.pathname.replace(REFERENCE_PATH, '').split('/')
|
|
|
|
if (lib === 'cli') {
|
|
const rewritePath = [REFERENCE_PATH, 'cli'].join('/')
|
|
const response = NextResponse.rewrite(new URL(rewritePath, request.url))
|
|
stampFirstReferrerCookie(request, response)
|
|
return response
|
|
}
|
|
|
|
if (lib === 'api') {
|
|
const rewritePath = [REFERENCE_PATH, 'api'].join('/')
|
|
const response = NextResponse.rewrite(new URL(rewritePath, request.url))
|
|
stampFirstReferrerCookie(request, response)
|
|
return response
|
|
}
|
|
|
|
if (lib?.startsWith('self-hosting-')) {
|
|
const rewritePath = [REFERENCE_PATH, lib].join('/')
|
|
const response = NextResponse.rewrite(new URL(rewritePath, request.url))
|
|
stampFirstReferrerCookie(request, response)
|
|
return response
|
|
}
|
|
|
|
if (clientSdkIds.includes(lib)) {
|
|
const version = /v\d+/.test(maybeVersion) ? maybeVersion : null
|
|
const rewritePath = [REFERENCE_PATH, lib, version].filter(Boolean).join('/')
|
|
const response = NextResponse.rewrite(new URL(rewritePath, request.url))
|
|
stampFirstReferrerCookie(request, response)
|
|
return response
|
|
}
|
|
|
|
const response = NextResponse.next()
|
|
stampFirstReferrerCookie(request, response)
|
|
return response
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
// Broadened from `/reference/:path*` to stamp first-referrer cookies on all
|
|
// docs pages, not just reference paths. Excludes Next.js internals and static files.
|
|
'/((?!api|_next/static|_next/image|favicon.ico|__nextjs).*)',
|
|
],
|
|
}
|