162 lines
4.5 KiB
TypeScript
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();
|
|
}
|