First-party, consent-based funnel tracking: a cookie notice asks once, and only visitors who allow it get an anonymous visit id. Event page views and registration starts are tracked from the browser; submit and paid come from the registration. Adds the Registration funnel report (conversion per stage, daily trend, by source/device/ticket option), a conversion tile on the supervisor/admin dashboards, short-link source tagging, and a daily rollup of raw rows older than 90 days. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CP62L9JxHomA36aYP1ekCe
69 lines
2.3 KiB
TypeScript
69 lines
2.3 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 {
|
|
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 */ }
|
|
}
|