import type { AnalyticsEvent } from "@/payload-types"; export type AggregatedStats = { sessions: number; pageViews: number; uniqueVisitors: number; avgPagesPerSession: number; eventCounts: Record; deviceCounts: Record; browserCounts: Record; topPaths: Record; entryPaths: Record; exitPaths: Record; }; function bump(map: Record, key: string | null | undefined) { if (!key) return; map[key] = (map[key] ?? 0) + 1; } /** Sorted (descending) [key, count] pairs, capped at `limit` — for dashboard tables. */ export function topEntries(map: Record, limit = 10): [string, number][] { return Object.entries(map) .sort(([, a], [, b]) => b - a) .slice(0, limit); } /** * Pure aggregation over a set of raw AnalyticsEvents rows (any date range — * one day for the cleanup rollup, or a whole dashboard-selected range for * live queries). Kept side-effect free and DB-agnostic so both callers * (lib/analytics/cleanup.ts and lib/analytics/dashboardQueries.ts) share * one implementation. */ export function aggregateEvents(events: AnalyticsEvent[]): AggregatedStats { const eventCounts: Record = {}; const deviceCounts: Record = {}; const browserCounts: Record = {}; const topPaths: Record = {}; const entryPaths: Record = {}; const exitPaths: Record = {}; const visitorHashes = new Set(); const sessionIds = new Set(); let pageViews = 0; // Per-session first/last page_view, in chronological order, for entry/exit pages. const sessionPageViews = new Map(); const sorted = [...events].sort((a, b) => a.createdAt.localeCompare(b.createdAt)); for (const ev of sorted) { visitorHashes.add(ev.visitorHash); sessionIds.add(ev.sessionId); bump(eventCounts, ev.eventName); bump(deviceCounts, ev.device); bump(browserCounts, ev.browser); if (ev.eventName === "page_view") { pageViews += 1; bump(topPaths, ev.path); const existing = sessionPageViews.get(ev.sessionId); if (!existing) { sessionPageViews.set(ev.sessionId, { first: { at: ev.createdAt, path: ev.path }, last: { at: ev.createdAt, path: ev.path }, }); } else { existing.last = { at: ev.createdAt, path: ev.path }; } } } for (const { first, last } of sessionPageViews.values()) { bump(entryPaths, first.path); bump(exitPaths, last.path); } const sessions = sessionIds.size; return { sessions, pageViews, uniqueVisitors: visitorHashes.size, avgPagesPerSession: sessions > 0 ? Math.round((pageViews / sessions) * 10) / 10 : 0, eventCounts, deviceCounts, browserCounts, topPaths, entryPaths, exitPaths, }; }