feat(growth): add in-memory first-touch attribution store (GROWTH-656)

Replaces the supabase-telemetry-data session cookie with a module-scoped
in-memory store so no non-essential device storage occurs before affirmative
consent in GDPR regions.
This commit is contained in:
Sean Oliver committed 2026-03-09 08:56:57 -07:00
1 parent 14b2af415f
commit aa2114fda0
2 files changed
+81

No files matched your search

@@ -0,0 +1,28 @@
import { useEffect } from 'react'
import { setFirstTouchData } from '../telemetry-first-touch-store'
import { getSharedTelemetryData } from '../telemetry-utils'
interface UseFirstTouchStoreProps {
enabled: boolean
}
/**
* Captures first-touch attribution data (e.g. `document.referrer`, UTM params)
* into an in-memory store on the initial page load — before consent is granted.
*
* The data is read once by PageTelemetry after consent and then cleared.
* Using in-memory storage (instead of a cookie) ensures no non-essential
* device storage before affirmative consent in GDPR regions.
*/
export function useFirstTouchStore({ enabled }: UseFirstTouchStoreProps) {
useEffect(() => {
if (typeof window === 'undefined') return
if (!enabled) return
const telemetryData = getSharedTelemetryData(window.location.pathname)
setFirstTouchData(telemetryData)
}, [enabled])
}
export default useFirstTouchStore
@@ -0,0 +1,53 @@
/**
* In-memory store for first-touch attribution data.
*
* Replaces the previous `supabase-telemetry-data` session cookie so that
* no non-essential device storage happens before affirmative consent in
* GDPR regions (ePrivacy Directive compliance).
*
* The data is captured on the very first page load (before consent) and
* held in a module-scoped variable. When consent is granted, PageTelemetry
* reads it once for the initial pageview event and then clears it.
*
* Trade-off: data is lost on a hard reload (Cmd+R) before consent, which
* is an accepted edge case (see GROWTH-656).
*
* Module-scope is safe here because both the writer (useFirstTouchStore)
* and reader (PageTelemetry) live in the same client-side bundle — the
* same pattern used by posthogClient and consentState.
*/
import type { getSharedTelemetryData } from './telemetry-utils'
export type SharedTelemetryData = ReturnType<typeof getSharedTelemetryData>
// ---------------------------------------------------------------------------
// Module-scoped singleton (survives SPA navigations, lost on hard reload)
// ---------------------------------------------------------------------------
let firstTouchData: SharedTelemetryData | null = null
/**
* Store the first-touch attribution data captured on initial page load.
* Only writes once — subsequent calls are no-ops if data already exists.
*/
export function setFirstTouchData(data: SharedTelemetryData): void {
if (firstTouchData !== null) return
firstTouchData = data
}
/**
* Read the stored first-touch attribution data.
* Returns null if no data has been captured (e.g. after a hard reload).
*/
export function getFirstTouchData(): SharedTelemetryData | null {
return firstTouchData
}
/**
* Clear the stored first-touch attribution data.
* Called after the initial pageview event is sent, or when the user opts out.
*/
export function clearFirstTouchData(): void {
firstTouchData = null
}