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
29 lines
1.2 KiB
TypeScript
29 lines
1.2 KiB
TypeScript
/**
|
|
* Coarse device/browser classification from the User-Agent header. No
|
|
* library, no raw UA string ever stored — just one bucket per request,
|
|
* computed on the fly and discarded after classification. Not meant to be
|
|
* precise (e.g. WebView vs. real browser); good enough for the "device
|
|
* type" / "browser" breakdown in the Analytics dashboard.
|
|
*/
|
|
|
|
export type DeviceType = "mobile" | "desktop" | "tablet";
|
|
export type BrowserFamily = "chrome" | "firefox" | "safari" | "edge" | "other";
|
|
|
|
export function classifyDevice(userAgent: string): DeviceType {
|
|
const ua = userAgent.toLowerCase();
|
|
if (/ipad|tablet(?!.*mobile)|(android(?!.*mobile))/.test(ua)) return "tablet";
|
|
if (/mobi|iphone|ipod|android/.test(ua)) return "mobile";
|
|
return "desktop";
|
|
}
|
|
|
|
export function classifyBrowser(userAgent: string): BrowserFamily {
|
|
const ua = userAgent.toLowerCase();
|
|
// Order matters: Edge/Chrome UAs also contain "safari"; Chrome UAs also
|
|
// contain "chrome" but so do Edge/Opera — check the most specific tokens first.
|
|
if (/edg\//.test(ua)) return "edge";
|
|
if (/chrome\/|crios\//.test(ua)) return "chrome";
|
|
if (/firefox\/|fxios\//.test(ua)) return "firefox";
|
|
if (/safari\//.test(ua)) return "safari";
|
|
return "other";
|
|
}
|