Files
hope-events/frontend/src/lib/analytics.ts
T
joshuaandClaude Opus 5.5 2b453b00e6 Add read-only admin impersonation
Admins (with 2FA) can view the site as a non-admin user for up to 30
minutes, with a required reason and a "Viewing as" banner. It is
strictly read-only: a guard mounted before every route refuses any
write made with an impersonation token, protect/optionalAuth check the
session is live, and GET handlers skip the price-refresh writes. Start,
end, reason and every page/API call (including blocked attempts) are
audited; optional email to the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP62L9JxHomA36aYP1ekCe
2026-10-19 18:25:02 +02:00

70 lines
2.4 KiB
TypeScript

"use client";
/**
* First-party registration-funnel tracking. Nothing is tracked until the visitor accepts
* cookies (CookieConsent); then a random session id is kept in localStorage and the
* event page ("view") and registration page ("start") report to our own API. No
* third-party analytics, and no personal data — the server records only the stage, the
* traffic source and a device type.
*/
import { API_BASE } from "@/lib/api";
const CONSENT_KEY = "hope_events_cookie_consent";
const SESSION_KEY = "hope_events_analytics_sid";
export const CONSENT_EVENT = "hope-events-consent";
export type Consent = "accepted" | "declined" | null;
export function getConsent(): Consent {
try {
const v = localStorage.getItem(CONSENT_KEY);
return v === "accepted" || v === "declined" ? v : null;
} catch {
return null;
}
}
export function setConsent(v: "accepted" | "declined") {
try {
localStorage.setItem(CONSENT_KEY, v);
if (v === "declined") localStorage.removeItem(SESSION_KEY);
} catch { /* storage unavailable */ }
try { window.dispatchEvent(new Event(CONSENT_EVENT)); } catch { /* no window */ }
}
/** The anonymous session id — only when cookies were accepted. */
export function analyticsSessionId(): string | undefined {
if (getConsent() !== "accepted") return undefined;
try {
if (localStorage.getItem("hope_events_impersonation")) return undefined;
let id = localStorage.getItem(SESSION_KEY);
if (!id) {
id = crypto.randomUUID();
localStorage.setItem(SESSION_KEY, id);
}
return id;
} catch {
return undefined;
}
}
/** Where the visitor came from: ?utm_source=… or ?src=<short-link alias>. */
function sourceFromUrl(): string | undefined {
try {
const p = new URLSearchParams(window.location.search);
return p.get("utm_source") || p.get("src") || undefined;
} catch {
return undefined;
}
}
/** Reports a funnel step. Silently does nothing without consent or on any error. */
export function trackFunnel(eventId: string, stage: "view" | "start") {
const sessionId = analyticsSessionId();
if (!sessionId || !eventId) return;
const body = JSON.stringify({ sessionId, eventId, stage, source: sourceFromUrl() });
try {
fetch(`${API_BASE}/api/analytics/track`, { method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true }).catch(() => {});
} catch { /* ignore */ }
}