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:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user