/** * UTM parameter extraction and in-memory persistence for first-touch attribution. * * UTM parameters are captured on first page load and held in memory. * They are never overwritten during the SPA lifecycle (first-touch attribution). * * CONSENT-FREE: No localStorage/sessionStorage/cookies used. * Attribution persists only for the current SPA lifecycle (tab close = reset). */ /** * Stored attribution data. */ export interface StoredAttribution { utmSource?: string; utmMedium?: string; utmCampaign?: string; utmContent?: string; utmTerm?: string; /** Original domain from ?via= redirect param (e.g. tqftw.com → transquinnftw.com) */ originalDomain?: string; referrer?: string; landingPage?: string; capturedAt: number; } // In-memory cache for consent-free attribution let cachedAttribution: StoredAttribution | null = null; /** * Extract UTM parameters from the current URL. * * @returns Object with UTM parameters (undefined for missing params) */ export function extractUtmParams(): Partial { if (typeof window === 'undefined') { return {}; } const params = new URLSearchParams(window.location.search); return { utmSource: params.get('utm_source') ?? undefined, utmMedium: params.get('utm_medium') ?? undefined, utmCampaign: params.get('utm_campaign') ?? undefined, utmContent: params.get('utm_content') ?? undefined, utmTerm: params.get('utm_term') ?? undefined, originalDomain: params.get('via') ?? undefined, }; } /** * Get the current referrer, filtering out same-origin referrers. * * @param currentDomain - Optional current domain to filter * @returns External referrer URL or undefined */ export function getExternalReferrer(currentDomain?: string): string | undefined { if (typeof document === 'undefined' || !document.referrer) { return undefined; } try { const referrerUrl = new URL(document.referrer); const currentHost = currentDomain ?? (typeof window !== 'undefined' ? window.location.hostname : undefined); // Filter out same-origin referrers if (currentHost && referrerUrl.hostname === currentHost) { return undefined; } return document.referrer; } catch { return undefined; } } /** * Capture first-touch attribution from UTM parameters and referrer. * * This function should be called once on page load. It will: * 1. Check if attribution is already captured (first-touch, never overwrite) * 2. Extract UTM params and referrer if not captured * 3. Store the attribution data in memory (consent-free) * * @returns The stored attribution data */ export function captureAttribution(): StoredAttribution | null { // First-touch: never overwrite existing attribution if (cachedAttribution) { return cachedAttribution; } // Extract UTM params and referrer const utmParams = extractUtmParams(); const referrer = getExternalReferrer(); const landingPage = typeof window !== 'undefined' ? window.location.href : undefined; // Only store if we have some attribution data if (utmParams.utmSource || utmParams.utmMedium || utmParams.utmCampaign || referrer) { cachedAttribution = { ...utmParams, referrer, landingPage, capturedAt: Date.now(), }; return cachedAttribution; } // No attribution data - still record landing page for direct traffic cachedAttribution = { landingPage, capturedAt: Date.now(), }; return cachedAttribution; } /** * Get stored attribution data. * * @returns Stored attribution or null if not captured */ export function getStoredAttribution(): StoredAttribution | null { return cachedAttribution; } /** * Clear stored attribution (for testing or session reset). */ export function clearAttribution(): void { cachedAttribution = null; } /** * Build a URL with UTM parameters appended. * * @param baseUrl - Base URL to append UTMs to * @param utm - UTM parameters * @returns URL with UTM query parameters */ export function buildUtmUrl( baseUrl: string, utm: { source?: string; medium?: string; campaign?: string; content?: string; term?: string; }, ): string { const url = new URL(baseUrl); if (utm.source) url.searchParams.set('utm_source', utm.source); if (utm.medium) url.searchParams.set('utm_medium', utm.medium); if (utm.campaign) url.searchParams.set('utm_campaign', utm.campaign); if (utm.content) url.searchParams.set('utm_content', utm.content); if (utm.term) url.searchParams.set('utm_term', utm.term); return url.toString(); }