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,29 @@
|
||||
import type { Payload } from "payload";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const SESSION_IDLE_WINDOW_MS = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Groups events into sessions without any client-side identifier: looks up
|
||||
* the most recent event from the same `visitorHash` within the last 30
|
||||
* minutes and reuses its sessionId, or mints a new one. One extra indexed
|
||||
* read per tracked request — acceptable at this site's traffic volume, and
|
||||
* simpler/safer than trying to hold a lock across an unauthenticated,
|
||||
* high-frequency public endpoint (unlike lib/booking/lock.ts's advisory
|
||||
* lock, an occasional extra spurious session here has no correctness
|
||||
* consequences worth the added complexity).
|
||||
*/
|
||||
export async function resolveSessionId(payload: Payload, visitorHash: string): Promise<string> {
|
||||
const since = new Date(Date.now() - SESSION_IDLE_WINDOW_MS).toISOString();
|
||||
const { docs } = await payload.find({
|
||||
collection: "analytics-events",
|
||||
where: {
|
||||
and: [{ visitorHash: { equals: visitorHash } }, { createdAt: { greater_than_equal: since } }],
|
||||
},
|
||||
sort: "-createdAt",
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
return docs[0]?.sessionId ?? randomUUID();
|
||||
}
|
||||
Reference in New Issue
Block a user