89beb5f160
installTracking() beacons one view per page load and one click per control that carries a data-key, so every existing button is already counted. The new STATS pane reads it back: a 7/30/90-day range, the three totals, an SVG timeline and eight proportional bar lists (pages, clicked features, country, region, city, browser, system, device).
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
// 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,
|
|
);
|
|
}
|