chore: Revert "fix: persist first referrer across app boundaries (#42768)" (#43129)

This reverts commit
https://github.com/supabase/supabase/commit/04e63dfb2e00c43f8c57e538b8056aa647f4e5b4
since it was causing the Studio app to rerender the full page on every
link navigation.
This commit is contained in:
Ivan Vasilov authored and GitHub committed 2026-02-24 11:56:44 +00:00
1 parent 14d36e9480
commit 85e6b1143f
7 files changed
+58 -709

No files matched your search

+11 -33
View File
@@ -1,23 +1,17 @@
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'
import { clientSdkIds } from '~/content/navigation.references'
import { BASE_PATH } from '~/lib/constants'
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
return NextResponse.next()
}
// 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('/')
@@ -30,9 +24,7 @@ export function middleware(request: NextRequest) {
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
return NextResponse.rewrite(rewriteUrl)
}
}
}
@@ -41,42 +33,28 @@ export function middleware(request: NextRequest) {
if (lib === 'cli') {
const rewritePath = [REFERENCE_PATH, 'cli'].join('/')
const response = NextResponse.rewrite(new URL(rewritePath, request.url))
stampFirstReferrerCookie(request, response)
return response
return NextResponse.rewrite(new URL(rewritePath, request.url))
}
if (lib === 'api') {
const rewritePath = [REFERENCE_PATH, 'api'].join('/')
const response = NextResponse.rewrite(new URL(rewritePath, request.url))
stampFirstReferrerCookie(request, response)
return response
return NextResponse.rewrite(new URL(rewritePath, request.url))
}
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
return NextResponse.rewrite(new URL(rewritePath, request.url))
}
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
return NextResponse.rewrite(new URL(rewritePath, request.url))
}
const response = NextResponse.next()
stampFirstReferrerCookie(request, response)
return response
return NextResponse.next()
}
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).*)',
],
matcher: '/reference/:path*',
}
+12 -21
View File
@@ -1,8 +1,10 @@
import { stampFirstReferrerCookie } from 'common/first-referrer-cookie'
import { IS_PLATFORM } from 'lib/constants'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export const config = {
matcher: '/api/:function*',
}
// [Joshen] Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted:
const HOSTED_SUPPORTED_API_URLS = [
'/ai/sql/generate-v4',
@@ -27,24 +29,13 @@ const HOSTED_SUPPORTED_API_URLS = [
]
export function proxy(request: NextRequest) {
// API route filtering for hosted platform
if (request.nextUrl.pathname.startsWith('/api/')) {
if (
IS_PLATFORM &&
!HOSTED_SUPPORTED_API_URLS.some((url) => request.nextUrl.pathname.endsWith(url))
) {
return Response.json(
{ success: false, message: 'Endpoint not supported on hosted' },
{ status: 404 }
)
}
if (
IS_PLATFORM &&
!HOSTED_SUPPORTED_API_URLS.some((url) => request.nextUrl.pathname.endsWith(url))
) {
return Response.json(
{ success: false, message: 'Endpoint not supported on hosted' },
{ status: 404 }
)
}
const response = NextResponse.next()
stampFirstReferrerCookie(request, response)
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|__nextjs).*)'],
}
-15
View File
@@ -1,15 +0,0 @@
import { stampFirstReferrerCookie } from 'common/first-referrer-cookie'
import { NextResponse, type NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
stampFirstReferrerCookie(request, response)
return response
}
export const config = {
matcher: [
// Match all paths except Next.js internals and static files
'/((?!api|_next/static|_next/image|favicon.ico|__nextjs).*)',
],
}
@@ -1,229 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
buildFirstReferrerData,
FIRST_REFERRER_COOKIE_NAME,
hasPaidSignals,
isExternalReferrer,
parseFirstReferrerCookie,
serializeFirstReferrerCookie,
shouldRefreshCookie,
} from './first-referrer-cookie'
describe('first-referrer-cookie', () => {
describe('isExternalReferrer', () => {
it('returns false for supabase domains', () => {
expect(isExternalReferrer('https://supabase.com')).toBe(false)
expect(isExternalReferrer('https://www.supabase.com')).toBe(false)
expect(isExternalReferrer('https://docs.supabase.com')).toBe(false)
})
it('returns true for external domains', () => {
expect(isExternalReferrer('https://google.com')).toBe(true)
expect(isExternalReferrer('https://chatgpt.com')).toBe(true)
})
it('returns true for http:// referrers', () => {
expect(isExternalReferrer('http://google.com')).toBe(true)
expect(isExternalReferrer('http://example.org/page')).toBe(true)
})
it('returns false for invalid values', () => {
expect(isExternalReferrer('')).toBe(false)
expect(isExternalReferrer('not-a-url')).toBe(false)
})
})
describe('buildFirstReferrerData', () => {
it('handles malformed landing URL gracefully', () => {
const data = buildFirstReferrerData({
referrer: 'https://google.com',
landingUrl: 'not-a-valid-url',
})
expect(data.referrer).toBe('https://google.com')
expect(data.landing_url).toBe('not-a-valid-url')
expect(data.utms).toEqual({})
expect(data.click_ids).toEqual({})
})
it('extracts utm and click-id params from landing url', () => {
const data = buildFirstReferrerData({
referrer: 'https://www.google.com/',
landingUrl:
'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456',
})
expect(data.referrer).toBe('https://www.google.com/')
expect(data.landing_url).toBe(
'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456'
)
expect(data.utms).toEqual({
utm_source: 'google',
utm_medium: 'cpc',
utm_campaign: 'test',
})
expect(data.click_ids).toEqual({
gclid: 'abc123',
msclkid: 'xyz456',
})
})
})
describe('serialize / parse', () => {
it('round-trips valid cookie payloads', () => {
const input = buildFirstReferrerData({
referrer: 'https://www.google.com/',
landingUrl: 'https://supabase.com/pricing?utm_source=google',
})
const encoded = serializeFirstReferrerCookie(input)
const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)
expect(parsed).toEqual(input)
})
it('returns null for empty string', () => {
expect(parseFirstReferrerCookie('')).toBeNull()
})
it('parses cookie from header with multiple cookies', () => {
const input = buildFirstReferrerData({
referrer: 'https://google.com/',
landingUrl: 'https://supabase.com/',
})
const encoded = serializeFirstReferrerCookie(input)
const header = `session=abc123; ${FIRST_REFERRER_COOKIE_NAME}=${encoded}; theme=dark`
expect(parseFirstReferrerCookie(header)).toEqual(input)
})
it('returns null for malformed json', () => {
expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=%7Bnot-json`)).toBeNull()
})
it('returns null for invalid payload shape', () => {
const encoded = encodeURIComponent(JSON.stringify({ foo: 'bar' }))
expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)).toBeNull()
})
it('drops non-string values in utms/click_ids', () => {
const encoded = encodeURIComponent(
JSON.stringify({
referrer: 'https://www.google.com/',
landing_url: 'https://supabase.com/pricing',
utms: { utm_source: 'google', utm_medium: 123 },
click_ids: { gclid: 'abc', msclkid: null },
ts: 123,
})
)
const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)
expect(parsed).toEqual({
referrer: 'https://www.google.com/',
landing_url: 'https://supabase.com/pricing',
utms: { utm_source: 'google' },
click_ids: { gclid: 'abc' },
ts: 123,
})
})
})
describe('hasPaidSignals', () => {
it('detects click IDs', () => {
expect(hasPaidSignals(new URL('https://supabase.com/?gclid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?fbclid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?msclkid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?gbraid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?wbraid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?rdt_cid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?ttclid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?twclid=abc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?li_fat_id=abc'))).toBe(true)
})
it('detects paid utm_medium values', () => {
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=cpc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=ppc'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_search'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paidsocial'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_social'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=display'))).toBe(true)
})
it('is case-insensitive for utm_medium', () => {
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=CPC'))).toBe(true)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=Paid_Search'))).toBe(true)
})
it('returns false for organic traffic', () => {
expect(hasPaidSignals(new URL('https://supabase.com/'))).toBe(false)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_source=google'))).toBe(false)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=email'))).toBe(false)
expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=organic'))).toBe(false)
})
})
describe('shouldRefreshCookie', () => {
it('stamps when no cookie and external referrer', () => {
expect(
shouldRefreshCookie(false, {
referrer: 'https://google.com',
url: 'https://supabase.com/',
})
).toEqual({ stamp: true })
})
it('skips when no cookie and internal referrer', () => {
expect(
shouldRefreshCookie(false, {
referrer: 'https://supabase.com/docs',
url: 'https://supabase.com/dashboard',
})
).toEqual({ stamp: false })
})
it('skips when cookie exists and no paid signals', () => {
expect(
shouldRefreshCookie(true, {
referrer: 'https://google.com',
url: 'https://supabase.com/',
})
).toEqual({ stamp: false })
})
it('refreshes when cookie exists but URL has paid signals', () => {
expect(
shouldRefreshCookie(true, {
referrer: 'https://google.com',
url: 'https://supabase.com/?gclid=abc123',
})
).toEqual({ stamp: true })
expect(
shouldRefreshCookie(true, {
referrer: 'https://google.com',
url: 'https://supabase.com/?utm_medium=cpc&utm_source=google',
})
).toEqual({ stamp: true })
})
it('skips when no cookie and no referrer (direct navigation)', () => {
expect(shouldRefreshCookie(false, { referrer: '', url: 'https://supabase.com/' })).toEqual({
stamp: false,
})
})
it('handles malformed URL gracefully', () => {
expect(
shouldRefreshCookie(true, {
referrer: 'https://google.com',
url: 'not-a-valid-url',
})
).toEqual({ stamp: false })
})
})
})
-307
View File
@@ -1,307 +0,0 @@
/**
* Shared utilities for the cross-app first-referrer handoff cookie.
*
* The `_sb_first_referrer` cookie is written by edge middleware on `apps/www`,
* `apps/docs`, and `apps/studio` when a user arrives from an
* external source. Studio reads it on the first telemetry pageview to recover
* external attribution context that would otherwise be lost at the app boundary.
*
* The cookie is normally write-once (365-day TTL, domain=supabase.com), but is
* refreshed when a returning visitor arrives with paid traffic signals (click IDs
* or paid UTM medium values) to ensure paid attribution overrides stale organic data.
*/
// ---------------------------------------------------------------------------
// Structural types for Next.js middleware request/response
// ---------------------------------------------------------------------------
// Using structural interfaces instead of importing NextRequest/NextResponse
// avoids version conflicts when different apps pin different Next.js versions
// (e.g. studio on Next 15, docs/www on Next 16).
interface MiddlewareRequest {
headers: { get(name: string): string | null }
cookies: { has(name: string): boolean }
url: string
nextUrl: { hostname: string }
}
interface MiddlewareResponse {
cookies: {
set(
name: string,
value: string,
options?: {
path?: string
sameSite?: 'lax' | 'strict' | 'none'
secure?: boolean
domain?: string
maxAge?: number
}
): void
}
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
export const FIRST_REFERRER_COOKIE_NAME = '_sb_first_referrer'
/** 365 days in seconds */
export const FIRST_REFERRER_COOKIE_MAX_AGE = 365 * 24 * 60 * 60
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface FirstReferrerData {
/** The external referrer URL (e.g. https://www.google.com/) */
referrer: string
/** The landing URL on our site when the external referrer was captured */
landing_url: string
/** UTM params parsed from the landing URL (e.g. utm_source, utm_medium) */
utms: Record<string, string>
/** Ad-network click IDs parsed from the landing URL */
click_ids: Record<string, string>
/** Unix timestamp (ms) when the cookie was written */
ts: number
}
// ---------------------------------------------------------------------------
// Referrer classification
// ---------------------------------------------------------------------------
/**
* Returns true if the referrer URL points to an external (non-Supabase) domain.
* Handles malformed URLs gracefully by returning false.
*/
export function isExternalReferrer(referrer: string): boolean {
if (!referrer) return false
try {
const hostname = new URL(referrer).hostname
return hostname !== 'supabase.com' && !hostname.endsWith('.supabase.com')
} catch {
return false
}
}
// ---------------------------------------------------------------------------
// UTM + click-ID extraction
// ---------------------------------------------------------------------------
const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const
const CLICK_ID_KEYS = [
'gclid', // Google Ads
'gbraid', // Google Ads (iOS)
'wbraid', // Google Ads (iOS)
'msclkid', // Microsoft Ads (Bing)
'fbclid', // Meta (Facebook/Instagram)
'rdt_cid', // Reddit Ads
'ttclid', // TikTok Ads
'twclid', // X Ads (Twitter)
'li_fat_id', // LinkedIn Ads
] as const
function pickParams(
searchParams: URLSearchParams,
keys: readonly string[]
): Record<string, string> {
const result: Record<string, string> = {}
for (const key of keys) {
const value = searchParams.get(key)
if (value) {
result[key] = value
}
}
return result
}
function toStringRecord(value: unknown): Record<string, string> {
if (!value || typeof value !== 'object') return {}
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).filter(
([key, v]) => typeof key === 'string' && typeof v === 'string'
)
) as Record<string, string>
}
// ---------------------------------------------------------------------------
// Build cookie payload from a request (edge-compatible)
// ---------------------------------------------------------------------------
/**
* Build a `FirstReferrerData` payload from raw request values.
* Intended for use in Next.js middleware where `document` is not available.
*/
export function buildFirstReferrerData({
referrer,
landingUrl,
}: {
referrer: string
landingUrl: string
}): FirstReferrerData {
let utms: Record<string, string> = {}
let click_ids: Record<string, string> = {}
try {
const url = new URL(landingUrl)
utms = pickParams(url.searchParams, UTM_KEYS)
click_ids = pickParams(url.searchParams, CLICK_ID_KEYS)
} catch {
// If landing URL is malformed, just skip param extraction
}
return {
referrer,
landing_url: landingUrl,
utms,
click_ids,
ts: Date.now(),
}
}
// ---------------------------------------------------------------------------
// Serialize / parse
// ---------------------------------------------------------------------------
export function serializeFirstReferrerCookie(data: FirstReferrerData): string {
return encodeURIComponent(JSON.stringify(data))
}
// ---------------------------------------------------------------------------
// Paid-signal detection
// ---------------------------------------------------------------------------
const PAID_UTM_MEDIUMS = new Set([
'cpc',
'ppc',
'paid_search',
'paidsocial',
'paid_social',
'display',
])
/**
* Returns true if the URL contains ad-network click IDs or paid UTM medium values.
* These indicate the user arrived via a paid campaign, which should override
* stale organic attribution.
*/
export function hasPaidSignals(url: URL): boolean {
for (const key of CLICK_ID_KEYS) {
if (url.searchParams.has(key)) return true
}
const medium = url.searchParams.get('utm_medium')?.toLowerCase()
return medium !== undefined && PAID_UTM_MEDIUMS.has(medium)
}
/**
* Decides whether the first-referrer cookie should be (re-)stamped.
*
* - No cookie + external referrer → stamp (first visit attribution)
* - Cookie exists + paid signals in URL → stamp (paid traffic refresh)
* - Otherwise → skip
*/
export function shouldRefreshCookie(
existingCookie: boolean,
request: { referrer: string; url: string }
): { stamp: boolean } {
if (!existingCookie) {
return { stamp: isExternalReferrer(request.referrer) }
}
try {
const url = new URL(request.url)
return { stamp: hasPaidSignals(url) }
} catch {
return { stamp: false }
}
}
// ---------------------------------------------------------------------------
// Middleware helper — shared across apps/www, apps/docs, and apps/studio
// ---------------------------------------------------------------------------
/**
* Stamp the first-referrer cookie on a Next.js middleware response if the
* request warrants it. This is the single entry point for all app middleware
* files — call it with the incoming request and outgoing response.
*
* On *.supabase.com the cookie is set with `domain=supabase.com` so it's
* readable across all subdomains (www, docs, studio). On other hosts
* (localhost, preview deploys) the domain is left unset so the browser
* stores a host-only cookie instead of rejecting an invalid domain.
*/
export function stampFirstReferrerCookie(request: MiddlewareRequest, response: MiddlewareResponse): void {
const referrer = request.headers.get('referer') ?? ''
const { stamp } = shouldRefreshCookie(request.cookies.has(FIRST_REFERRER_COOKIE_NAME), {
referrer,
url: request.url,
})
if (!stamp) return
const data = buildFirstReferrerData({
referrer,
landingUrl: request.url,
})
response.cookies.set(FIRST_REFERRER_COOKIE_NAME, serializeFirstReferrerCookie(data), {
path: '/',
sameSite: 'lax',
...(request.nextUrl.hostname === 'supabase.com' ||
request.nextUrl.hostname.endsWith('.supabase.com')
? { domain: 'supabase.com', secure: true }
: {}),
maxAge: FIRST_REFERRER_COOKIE_MAX_AGE,
})
}
// ---------------------------------------------------------------------------
// Parse cookie from document.cookie header (client-side)
// ---------------------------------------------------------------------------
export function parseFirstReferrerCookie(cookieHeader: string): FirstReferrerData | null {
try {
const cookies = cookieHeader.split(';')
const match = cookies
.map((c) => c.trim())
.find((c) => c.startsWith(`${FIRST_REFERRER_COOKIE_NAME}=`))
if (!match) return null
const value = match.slice(`${FIRST_REFERRER_COOKIE_NAME}=`.length)
const parsed = JSON.parse(decodeURIComponent(value)) as unknown
if (!parsed || typeof parsed !== 'object') return null
const parsedRecord = parsed as Record<string, unknown>
const referrer = parsedRecord.referrer
const landingUrl = parsedRecord.landing_url
if (typeof referrer !== 'string' || typeof landingUrl !== 'string') {
return null
}
const utmsRaw = parsedRecord.utms
const clickIdsRaw = parsedRecord.click_ids
const tsRaw = parsedRecord.ts
const utms = toStringRecord(utmsRaw)
const click_ids = toStringRecord(clickIdsRaw)
const ts = typeof tsRaw === 'number' && Number.isFinite(tsRaw) ? tsRaw : Date.now()
return {
referrer,
landing_url: landingUrl,
utms,
click_ids,
ts,
}
} catch {
return null
}
}
-1
View File
@@ -10,6 +10,5 @@ export * from './helpers'
export * from './hooks'
export * from './MetaFavicons/pages-router'
export * from './Providers'
export * from './first-referrer-cookie'
export * from './telemetry'
export * from './telemetry-utils'
+35 -103
View File
@@ -12,8 +12,6 @@ import { hasConsented } from './consent-state'
import { IS_PLATFORM, IS_PROD, LOCAL_STORAGE_KEYS } from './constants'
import { useFeatureFlags } from './feature-flags'
import { post } from './fetchWrappers'
import type { FirstReferrerData } from './first-referrer-cookie'
import { isExternalReferrer, parseFirstReferrerCookie } from './first-referrer-cookie'
import { ensurePlatformSuffix, isBrowser } from './helpers'
import { useParams, useTelemetryCookie } from './hooks'
import { posthogClient, type ClientTelemetryEvent } from './posthog-client'
@@ -98,25 +96,25 @@ function getFirstTouchAttributionProps(telemetryData: SharedTelemetryData) {
}
}
interface HandlePageTelemetryOptions {
apiUrl: string
pathname?: string
featureFlags?: Record<string, unknown>
slug?: string
ref?: string
telemetryDataOverride?: SharedTelemetryData
firstReferrerData?: FirstReferrerData | null
function isExternalReferrer(referrer: string) {
try {
const hostname = new URL(referrer).hostname
return hostname !== 'supabase.com' && !hostname.endsWith('.supabase.com')
} catch {
return false
}
}
function handlePageTelemetry({
apiUrl: API_URL,
pathname,
featureFlags,
slug,
ref,
telemetryDataOverride,
firstReferrerData,
}: HandlePageTelemetryOptions) {
function handlePageTelemetry(
API_URL: string,
pathname?: string,
featureFlags?: {
[key: string]: unknown
},
slug?: string,
ref?: string,
telemetryDataOverride?: SharedTelemetryData
) {
// Send to PostHog client-side (only in browser)
if (typeof window !== 'undefined') {
const livePageData = getSharedTelemetryData(pathname)
@@ -135,51 +133,10 @@ function handlePageTelemetry({
referrer: shouldUseCookieReferrer ? cookieReferrer! : liveReferrer,
},
}
: { ...livePageData, ph: { ...livePageData.ph } }
const firstTouchAttributionProps: Record<string, string> = {
...(telemetryDataOverride ? getFirstTouchAttributionProps(telemetryDataOverride) : {}),
}
// --- First-referrer edge cookie handoff ---
// If the edge cookie has external context and the current referrer is internal,
// override the referrer so PostHog gets the real acquisition source.
const firstReferrerCookiePresent = Boolean(firstReferrerData)
let firstReferrerCookieConsumed = false
if (
firstReferrerData &&
isExternalReferrer(firstReferrerData.referrer) &&
!isExternalReferrer(pageData.ph.referrer)
) {
pageData.ph.referrer = firstReferrerData.referrer
firstReferrerCookieConsumed = true
// Prefer attribution context captured at the external entry point.
const { utms, click_ids, landing_url } = firstReferrerData
Object.entries(utms).forEach(([key, value]) => {
const phKey = key.startsWith('utm_') ? `$${key}` : key
firstTouchAttributionProps[phKey] = value
})
Object.entries(click_ids).forEach(([key, value]) => {
firstTouchAttributionProps[key] = value
})
try {
const url = new URL(landing_url)
firstTouchAttributionProps.first_touch_url = url.href
firstTouchAttributionProps.first_touch_pathname = url.pathname
if (url.search) {
firstTouchAttributionProps.first_touch_search = url.search
} else {
delete firstTouchAttributionProps.first_touch_search
}
} catch {
// Skip if landing URL is malformed
}
}
: livePageData
const firstTouchAttributionProps = telemetryDataOverride
? getFirstTouchAttributionProps(telemetryDataOverride)
: {}
const $referrer = pageData.ph.referrer
const $referring_domain = (() => {
@@ -213,13 +170,6 @@ function handlePageTelemetry({
...Object.fromEntries(
Object.entries(featureFlags || {}).map(([k, v]) => [`$feature/${k}`, v])
),
// Measurement properties for handoff observability
// Only included on the initial pageview (when firstReferrerData is explicitly
// passed as null or a value — subsequent pageviews leave it as undefined)
...(firstReferrerData !== undefined && {
first_referrer_cookie_present: firstReferrerCookiePresent,
first_referrer_cookie_consumed: firstReferrerCookieConsumed,
}),
})
}
@@ -284,13 +234,13 @@ export const PageTelemetry = ({
const sendPageTelemetry = useCallback(() => {
if (!(enabled && hasAcceptedConsent)) return Promise.resolve()
return handlePageTelemetry({
apiUrl: API_URL,
pathname: pathnameRef.current,
featureFlags: featureFlagsRef.current,
return handlePageTelemetry(
API_URL,
pathnameRef.current,
featureFlagsRef.current,
slug,
ref,
}).catch((e) => {
ref
).catch((e) => {
console.error('Problem sending telemetry page:', e)
})
}, [API_URL, enabled, hasAcceptedConsent, slug, ref])
@@ -333,9 +283,6 @@ export const PageTelemetry = ({
hasAcceptedConsent &&
!hasSentInitialPageTelemetryRef.current
) {
// Read the edge-set first-referrer cookie (cross-app handoff)
const firstReferrerData = parseFirstReferrerCookie(document.cookie)
const cookies = document.cookie.split(';')
const telemetryCookieValue = cookies
.map((cookie) => cookie.trim())
@@ -347,39 +294,24 @@ export const PageTelemetry = ({
const telemetryData = JSON.parse(
decodeURIComponent(telemetryCookieValue)
) as SharedTelemetryData
handlePageTelemetry({
apiUrl: API_URL,
pathname: pathnameRef.current,
featureFlags: featureFlagsRef.current,
handlePageTelemetry(
API_URL,
pathnameRef.current,
featureFlagsRef.current,
slug,
ref,
telemetryDataOverride: telemetryData,
firstReferrerData,
})
telemetryData
)
} catch (error) {
if (!IS_PROD) {
console.warn('Invalid telemetry cookie data:', error)
}
handlePageTelemetry({
apiUrl: API_URL,
pathname: pathnameRef.current,
featureFlags: featureFlagsRef.current,
slug,
ref,
firstReferrerData,
})
handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, slug, ref)
} finally {
clearTelemetryDataCookie()
}
} else {
handlePageTelemetry({
apiUrl: API_URL,
pathname: pathnameRef.current,
featureFlags: featureFlagsRef.current,
slug,
ref,
firstReferrerData,
})
handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, slug, ref)
}
hasSentInitialPageTelemetryRef.current = true