diff --git a/packages/common/hooks/useFirstTouchStore.tsx b/packages/common/hooks/useFirstTouchStore.tsx new file mode 100644 index 00000000000..4127c846347 --- /dev/null +++ b/packages/common/hooks/useFirstTouchStore.tsx @@ -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 diff --git a/packages/common/telemetry-first-touch-store.ts b/packages/common/telemetry-first-touch-store.ts new file mode 100644 index 00000000000..ef6a1ffc4ad --- /dev/null +++ b/packages/common/telemetry-first-touch-store.ts @@ -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 + +// --------------------------------------------------------------------------- +// 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 +}