web: put the traffic screen in the admin frame
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).
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user