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:
@@ -79,6 +79,8 @@ export interface Config {
|
||||
'availability-overrides': AvailabilityOverride;
|
||||
'booking-requests': BookingRequest;
|
||||
redirects: Redirect;
|
||||
'analytics-events': AnalyticsEvent;
|
||||
'analytics-daily-stats': AnalyticsDailyStat;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
@@ -97,6 +99,8 @@ export interface Config {
|
||||
'availability-overrides': AvailabilityOverridesSelect<false> | AvailabilityOverridesSelect<true>;
|
||||
'booking-requests': BookingRequestsSelect<false> | BookingRequestsSelect<true>;
|
||||
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
|
||||
'analytics-events': AnalyticsEventsSelect<false> | AnalyticsEventsSelect<true>;
|
||||
'analytics-daily-stats': AnalyticsDailyStatsSelect<false> | AnalyticsDailyStatsSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
@@ -116,6 +120,8 @@ export interface Config {
|
||||
'meeting-settings': MeetingSetting;
|
||||
'booking-settings': BookingSetting;
|
||||
'seo-settings': SeoSetting;
|
||||
'cookie-settings': CookieSetting;
|
||||
'analytics-settings': AnalyticsSetting;
|
||||
};
|
||||
globalsSelect: {
|
||||
home: HomeSelect<false> | HomeSelect<true>;
|
||||
@@ -127,6 +133,8 @@ export interface Config {
|
||||
'meeting-settings': MeetingSettingsSelect<false> | MeetingSettingsSelect<true>;
|
||||
'booking-settings': BookingSettingsSelect<false> | BookingSettingsSelect<true>;
|
||||
'seo-settings': SeoSettingsSelect<false> | SeoSettingsSelect<true>;
|
||||
'cookie-settings': CookieSettingsSelect<false> | CookieSettingsSelect<true>;
|
||||
'analytics-settings': AnalyticsSettingsSelect<false> | AnalyticsSettingsSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
widgets: {
|
||||
@@ -184,6 +192,10 @@ export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
role: 'admin';
|
||||
/**
|
||||
* Steuert, ob dieser Zugang das Analytics-Dashboard im Admin-Bereich sehen kann.
|
||||
*/
|
||||
canViewAnalytics?: boolean | null;
|
||||
calendarFeedTokenHash?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -611,6 +623,89 @@ export interface Redirect {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "analytics-events".
|
||||
*/
|
||||
export interface AnalyticsEvent {
|
||||
id: number;
|
||||
sessionId: string;
|
||||
visitorHash: string;
|
||||
path: string;
|
||||
eventName: string;
|
||||
referrerHost?: string | null;
|
||||
device?: ('mobile' | 'desktop' | 'tablet') | null;
|
||||
browser?: ('chrome' | 'firefox' | 'safari' | 'edge' | 'other') | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "analytics-daily-stats".
|
||||
*/
|
||||
export interface AnalyticsDailyStat {
|
||||
id: number;
|
||||
date: string;
|
||||
sessions: number;
|
||||
pageViews: number;
|
||||
uniqueVisitors: number;
|
||||
eventCounts?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
deviceCounts?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
browserCounts?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
topPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
entryPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
exitPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
@@ -678,6 +773,14 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'redirects';
|
||||
value: number | Redirect;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'analytics-events';
|
||||
value: number | AnalyticsEvent;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'analytics-daily-stats';
|
||||
value: number | AnalyticsDailyStat;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user:
|
||||
@@ -738,6 +841,7 @@ export interface PayloadMigration {
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
role?: T;
|
||||
canViewAnalytics?: T;
|
||||
calendarFeedTokenHash?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
@@ -1004,6 +1108,39 @@ export interface RedirectsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "analytics-events_select".
|
||||
*/
|
||||
export interface AnalyticsEventsSelect<T extends boolean = true> {
|
||||
sessionId?: T;
|
||||
visitorHash?: T;
|
||||
path?: T;
|
||||
eventName?: T;
|
||||
referrerHost?: T;
|
||||
device?: T;
|
||||
browser?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "analytics-daily-stats_select".
|
||||
*/
|
||||
export interface AnalyticsDailyStatsSelect<T extends boolean = true> {
|
||||
date?: T;
|
||||
sessions?: T;
|
||||
pageViews?: T;
|
||||
uniqueVisitors?: T;
|
||||
eventCounts?: T;
|
||||
deviceCounts?: T;
|
||||
browserCounts?: T;
|
||||
topPaths?: T;
|
||||
entryPaths?: T;
|
||||
exitPaths?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
@@ -1364,6 +1501,63 @@ export interface SeoSetting {
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* Kategorien und Texte für den Cookie-Consent-Banner der Website.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "cookie-settings".
|
||||
*/
|
||||
export interface CookieSetting {
|
||||
id: number;
|
||||
/**
|
||||
* Bei Änderung müssen alle Besucher:innen erneut zustimmen — bereits gespeicherte Einwilligungen mit einer älteren Version werden ignoriert und der Banner erscheint erneut.
|
||||
*/
|
||||
consentVersion: string;
|
||||
/**
|
||||
* Immer aktiv — technisch erforderlich, kann nicht deaktiviert werden.
|
||||
*/
|
||||
necessary: {
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
preferences: {
|
||||
label: string;
|
||||
description: string;
|
||||
/**
|
||||
* Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten.
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
};
|
||||
statistics: {
|
||||
label: string;
|
||||
description: string;
|
||||
/**
|
||||
* Globaler Schalter für das gesamte Analytics-System. Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten und Analytics-Daten werden serverseitig abgelehnt — auch bei einer noch gespeicherten alten Zustimmung.
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* Aufbewahrungsdauer für Analytics-Rohdaten und aggregierte Tagesstatistiken.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "analytics-settings".
|
||||
*/
|
||||
export interface AnalyticsSetting {
|
||||
id: number;
|
||||
/**
|
||||
* Wie lange einzelne Analytics-Events gespeichert bleiben. Begrenzt auch den wählbaren Zeitraum im Analytics-Dashboard.
|
||||
*/
|
||||
rawRetentionDays: number;
|
||||
/**
|
||||
* Wie lange die täglichen Aggregat-Statistiken (ohne Einzel-Events) gespeichert bleiben.
|
||||
*/
|
||||
aggregateRetentionMonths: number;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "home_select".
|
||||
@@ -1575,6 +1769,47 @@ export interface SeoSettingsSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "cookie-settings_select".
|
||||
*/
|
||||
export interface CookieSettingsSelect<T extends boolean = true> {
|
||||
consentVersion?: T;
|
||||
necessary?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
description?: T;
|
||||
};
|
||||
preferences?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
description?: T;
|
||||
enabled?: T;
|
||||
};
|
||||
statistics?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
description?: T;
|
||||
enabled?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "analytics-settings_select".
|
||||
*/
|
||||
export interface AnalyticsSettingsSelect<T extends boolean = true> {
|
||||
rawRetentionDays?: T;
|
||||
aggregateRetentionMonths?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "collections_widget".
|
||||
|
||||
Reference in New Issue
Block a user