/** * Analytics tracking for Umami. * * Single typed wrapper around `window.umami.track()`. All events flow * through `track(name, data?)` — this gives us: * - Refactor-safe event names (one place to maintain). * - A schema for properties so we don't ship typos that fragment dashboards. * - No-op on the server and never-throws semantics, so analytics outages * can't take the app down. * * Umami is privacy-friendly (no cookies, no IPs, no PII), so it's safe to * include school identifiers and full search query text. */ export type EventName = // Discovery | 'search_submitted' | 'near_me_used' | 'empty_results' // Engagement | 'school_viewed' | 'section_nav_used' | 'chart_metric_changed' | 'metric_compared_in_rankings' | 'external_link_clicked' // Conversion | 'compare_school_added' | 'compare_school_removed' | 'compare_viewed' | 'compare_metric_changed' | 'compare_shared' // Operational | 'api_error' | 'results_load_more'; type Primitive = string | number | boolean; type Payload = Record; /** * Fire an event. No-ops if Umami isn't loaded yet (the script is `defer`) * or if we're rendering server-side. Never throws. */ export function track(name: EventName, data?: Payload): void { if (typeof window === 'undefined') return; const umami = (window as unknown as { umami?: { track?: (n: string, d?: Payload) => void } }).umami; if (!umami?.track) return; try { umami.track(name, data); } catch { // Analytics must never crash the app. } } /** * Categorise where the user navigated from, for funnel attribution * (mostly used on school_viewed). Only checks same-origin referrers. */ export function getNavigationSource(): 'search' | 'rankings' | 'compare' | 'detail' | 'direct' { if (typeof window === 'undefined' || !document.referrer) return 'direct'; try { const ref = new URL(document.referrer); if (ref.origin !== window.location.origin) return 'direct'; const p = ref.pathname; if (p === '/' || p === '') return 'search'; if (p.startsWith('/rankings')) return 'rankings'; if (p.startsWith('/compare')) return 'compare'; if (p.startsWith('/school/')) return 'detail'; return 'direct'; } catch { return 'direct'; } } /** Split mobile vs desktop on a per-event basis. */ export function getViewport(): 'mobile' | 'desktop' { if (typeof window === 'undefined') return 'desktop'; return window.matchMedia('(max-width: 640px)').matches ? 'mobile' : 'desktop'; }