Files

95 lines
2.9 KiB
TypeScript

import type { AnalyticsEvent } from "@/payload-types";
export type AggregatedStats = {
sessions: number;
pageViews: number;
uniqueVisitors: number;
avgPagesPerSession: number;
eventCounts: Record<string, number>;
deviceCounts: Record<string, number>;
browserCounts: Record<string, number>;
topPaths: Record<string, number>;
entryPaths: Record<string, number>;
exitPaths: Record<string, number>;
};
function bump(map: Record<string, number>, 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<string, number>, 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<string, number> = {};
const deviceCounts: Record<string, number> = {};
const browserCounts: Record<string, number> = {};
const topPaths: Record<string, number> = {};
const entryPaths: Record<string, number> = {};
const exitPaths: Record<string, number> = {};
const visitorHashes = new Set<string>();
const sessionIds = new Set<string>();
let pageViews = 0;
// Per-session first/last page_view, in chronological order, for entry/exit pages.
const sessionPageViews = new Map<string, { first: { at: string; path: string }; last: { at: string; path: string } }>();
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,
};
}