lilith-platform.live/codebase/@features/user-data/shared/src/utm-extractor.ts

162 lines
4.5 KiB
TypeScript

/**
* 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<StoredAttribution> {
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();
}