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,52 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
import { canViewAnalytics } from "@/access";
|
||||
|
||||
/**
|
||||
* One row per calendar day, upserted by the cleanup cron
|
||||
* (lib/analytics/cleanup.ts) from raw analytics-events BEFORE those raw rows
|
||||
* age out. This is what lets aggregated history survive well beyond the raw
|
||||
* retention window (default 90 days raw vs. 12 months aggregated — see
|
||||
* globals/AnalyticsSettings.ts) without keeping individual events around.
|
||||
*
|
||||
* Not currently read by the main Analytics dashboard filters (those all
|
||||
* stay within raw retention by design — see the implementation plan); this
|
||||
* exists so long-term aggregated history isn't silently lost once raw
|
||||
* events are purged, and is available for a future longer-range view.
|
||||
*/
|
||||
export const AnalyticsDailyStats: CollectionConfig = {
|
||||
slug: "analytics-daily-stats",
|
||||
labels: {
|
||||
singular: "Analytics-Tagesstatistik",
|
||||
plural: "Analytics-Tagesstatistiken",
|
||||
},
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
access: {
|
||||
create: () => false,
|
||||
read: canViewAnalytics,
|
||||
update: () => false,
|
||||
delete: () => false,
|
||||
},
|
||||
timestamps: true,
|
||||
fields: [
|
||||
{
|
||||
name: "date",
|
||||
type: "date",
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: { date: { pickerAppearance: "dayOnly" } },
|
||||
},
|
||||
{ name: "sessions", type: "number", required: true, defaultValue: 0 },
|
||||
{ name: "pageViews", type: "number", required: true, defaultValue: 0 },
|
||||
{ name: "uniqueVisitors", type: "number", required: true, defaultValue: 0 },
|
||||
// Each a { [key: string]: number } map, e.g. eventCounts.booking_started.
|
||||
{ name: "eventCounts", type: "json" },
|
||||
{ name: "deviceCounts", type: "json" },
|
||||
{ name: "browserCounts", type: "json" },
|
||||
{ name: "topPaths", type: "json" },
|
||||
{ name: "entryPaths", type: "json" },
|
||||
{ name: "exitPaths", type: "json" },
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user