Add privacy-friendly analytics and cookie consent, integrated into the CMS
Adds a first-party analytics system with no third-party trackers and no persistent per-visitor identifiers: events are grouped into sessions via a daily-rotating pseudonymous hash (IP+UA+host, HMAC-salted, never stored raw). - New "Analytics" section in the Payload admin, gated by a canViewAnalytics permission on Users, following the existing custom-admin-view pattern used by the calendar feature - Sessions, page views, unique visitors, top/entry/exit pages, events, device/browser breakdown, date range filter with period comparison - Raw events and daily aggregate rollups as their own collections, with configurable retention and a cron endpoint that rolls up before purging - Cookie consent banner (necessary/preferences/statistics) with categories and copy editable from a new CMS global; a track() helper that is a complete no-op without statistics consent, so nothing is ever sent before a visitor opts in - track() calls wired into the booking flow, contact form, and navigation
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { createHmac } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Derives a daily-rotating, non-reversible pseudonymous visitor identifier
|
||||
* for session grouping — the same approach used by privacy-friendly
|
||||
* analytics tools (Plausible/Fathom-style): hash(ip + userAgent + host)
|
||||
* salted with a value that changes every calendar day. Raw IP/UA are never
|
||||
* persisted anywhere, only this hash — and since the salt rotates daily,
|
||||
* the same visitor hashes differently tomorrow, so there is no cross-day
|
||||
* linkage and no long-term individual profile.
|
||||
*
|
||||
* ANALYTICS_SALT_SECRET is a dedicated secret (not PAYLOAD_SECRET) — this
|
||||
* codebase keeps one secret per purpose on purpose (see
|
||||
* MEETING_SESSION_SECRET's comment in .env.example).
|
||||
*/
|
||||
function secret(): string {
|
||||
const value = process.env.ANALYTICS_SALT_SECRET;
|
||||
if (!value) throw new Error("ANALYTICS_SALT_SECRET is not set");
|
||||
return value;
|
||||
}
|
||||
|
||||
function dateKey(date: Date): string {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function dailySalt(date: Date): string {
|
||||
return createHmac("sha256", secret()).update(dateKey(date)).digest("hex");
|
||||
}
|
||||
|
||||
export function visitorHash(args: { ip: string; userAgent: string; host: string; date?: Date }): string {
|
||||
const salt = dailySalt(args.date ?? new Date());
|
||||
return createHmac("sha256", salt).update(`${args.ip}|${args.userAgent}|${args.host}`).digest("hex");
|
||||
}
|
||||
Reference in New Issue
Block a user