feat(telemetry): rollups, alerts, scheduler, health checks, admin server fns

- pure rollup accumulators with tests; daily upserts keyed date/metric/dim
- alert checks with db-backed cooldown and push to admin players
- in-process scheduler started from health probes
- health endpoint checks pb + supertokens, stale-while-revalidate, always 200
- sse connection counter moved to emitter
- telemetry feature server fns: events/errors/groups/rollups/alerts/status
This commit is contained in:
2026-08-25 21:38:08 -07:00
parent e2863c1f6c
commit 06fe1f4ed7
12 changed files with 1009 additions and 11 deletions
+103
View File
@@ -0,0 +1,103 @@
import { pbAdmin } from "@/lib/pocketbase/client";
import { Logger } from "@/lib/logger";
import {
accumulateActivities,
accumulateClientErrors,
accumulateClientEvents,
buildDayRollups,
newActivityAccumulator,
newClientErrorAccumulator,
newClientEventAccumulator,
type ActivityRow,
type ClientErrorRow,
type ClientEventRow,
} from "./rollup-core";
const logger = new Logger("Telemetry > Rollup");
export const utcDay = (date: Date): string => date.toISOString().slice(0, 10);
const addDays = (dateStr: string, days: number): string => {
const d = new Date(`${dateStr}T00:00:00.000Z`);
d.setUTCDate(d.getUTCDate() + days);
return utcDay(d);
};
// PB stores autodates as "YYYY-MM-DD HH:MM:SS.sssZ" UTC; boundaries in the
// same shape compare correctly in filters.
const bound = (dateStr: string): string => `${dateStr} 00:00:00.000Z`;
const dayFilter = (dateStr: string): string =>
`created >= '${bound(dateStr)}' && created < '${bound(addDays(dateStr, 1))}'`;
const distinctPlayersInWindow = async (fromDate: string, toDateExclusive: string): Promise<number> => {
const players = new Set<string>();
const filter = `created >= '${bound(fromDate)}' && created < '${bound(toDateExclusive)}' && player != ''`;
await pbAdmin.pageCollection<{ player?: string }>("activities", { filter, fields: "player,created" }, (rows) => {
for (const row of rows) if (row.player) players.add(row.player);
});
await pbAdmin.pageCollection<{ player?: string }>("client_events", { filter, fields: "player,created" }, (rows) => {
for (const row of rows) if (row.player) players.add(row.player);
});
return players.size;
};
export const runRollups = async (dateStr: string): Promise<number> => {
await pbAdmin.authPromise;
const activities = newActivityAccumulator();
const clientEvents = newClientEventAccumulator();
const clientErrors = newClientErrorAccumulator();
await pbAdmin.pageCollection<ActivityRow>(
"activities",
{ filter: dayFilter(dateStr), fields: "name,player,duration,success,error,created" },
(rows) => accumulateActivities(activities, rows)
);
await pbAdmin.pageCollection<ClientEventRow>(
"client_events",
{ filter: dayFilter(dateStr), fields: "name,player,session_id,route_id,value,created" },
(rows) => accumulateClientEvents(clientEvents, rows)
);
await pbAdmin.pageCollection<ClientErrorRow>(
"client_errors",
{ filter: dayFilter(dateStr), fields: "group_hash,created" },
(rows) => accumulateClientErrors(clientErrors, rows)
);
const nextDay = addDays(dateStr, 1);
const [wau, mau] = await Promise.all([
distinctPlayersInWindow(addDays(dateStr, -6), nextDay),
distinctPlayersInWindow(addDays(dateStr, -29), nextDay),
]);
const partial = dateStr >= utcDay(new Date());
const rollups = buildDayRollups(dateStr, {
activities,
clientEvents,
clientErrors,
wau,
mau,
partial,
});
for (const rollup of rollups) {
await pbAdmin.upsertRollup(rollup);
}
logger.info(`Rolled up ${dateStr}: ${rollups.length} metrics${partial ? " (partial)" : ""}`);
return rollups.length;
};
// The first run after UTC midnight recomputes yesterday once more so its
// partial rows become final. Idempotent via the (date, metric, dim) key.
export const runScheduledRollups = async (lastRunDay?: string): Promise<string> => {
const today = utcDay(new Date());
if (lastRunDay && lastRunDay !== today) {
await runRollups(lastRunDay);
}
await runRollups(today);
return today;
};