// The page counter's browser end. One beacon per page view, one per click on an // element that carries a `data-key` — that attribute is already on nearly every // control, so it doubles as the feature's name. Nothing here may break the app: // every path is wrapped, a refused beacon is simply not counted, and a visitor // who asked not to be tracked is left alone. const ENDPOINT = '/api/events'; type Kind = 'view' | 'click'; function send(kind: Kind, path: string, target?: string): void { try { if (navigator.doNotTrack === '1') return; const body = JSON.stringify({ kind, path, target }); const blob = new Blob([body], { type: 'application/json' }); // sendBeacon survives the page unload a click often triggers; the fetch is // the fallback for a browser that has no beacon (or refuses this one). if (navigator.sendBeacon?.(ENDPOINT, blob)) return; void fetch(ENDPOINT, { method: 'POST', body, keepalive: true, headers: { 'content-type': 'application/json' }, }).catch(() => {}); } catch { // Analytics is best-effort — a failure here must never reach the app. } } export function track(kind: Kind, path: string, target?: string): void { send(kind, path, target); } // Call once, before the first render. The view is recorded now; the click is // recorded from one capturing listener on the document, so a control keeps its // own handler and needs no tracking code of its own. export function installTracking(): void { send('view', window.location.pathname); document.addEventListener( 'click', (event) => { const node = event.target; const el = node instanceof Element ? node.closest('[data-key]') : null; const target = el?.getAttribute('data-key'); if (target) send('click', window.location.pathname, target); }, true, ); }