fix(telemetry): review findings

- wau/mau trailing scans run hourly and on day finalization, not every tick
- exclusion lookup failures cached 30s so an st outage cannot amplify
- unverified relation filter dropped; player emptiness checked in js
- anonymous unauthenticated read failures no longer logged
- alerts and exclusions ensure supertokens init on cold pods
This commit is contained in:
2026-08-25 23:26:34 -07:00
parent 31f6237446
commit 1ddcffc511
8 changed files with 93 additions and 15 deletions
+2
View File
@@ -31,6 +31,8 @@ const getAdminPlayerIds = async (): Promise<string[]> => {
return adminPlayersCache.playerIds; return adminPlayersCache.playerIds;
} }
const { ensureSuperTokensBackend } = await import("@/lib/supertokens/server");
ensureSuperTokensBackend();
const UserRoles = (await import("supertokens-node/recipe/userroles")).default; const UserRoles = (await import("supertokens-node/recipe/userroles")).default;
const response = await UserRoles.getUsersThatHaveRole("public", ADMIN_ROLE); const response = await UserRoles.getUsersThatHaveRole("public", ADMIN_ROLE);
const users = response.status === "OK" ? response.users : []; const users = response.status === "OK" ? response.users : [];
+6
View File
@@ -3,6 +3,7 @@ import { Logger } from "@/lib/logger";
const logger = new Logger("Telemetry"); const logger = new Logger("Telemetry");
const CACHE_TTL_MS = 10 * 60 * 1000; const CACHE_TTL_MS = 10 * 60 * 1000;
const FAILURE_TTL_MS = 30 * 1000;
const normalizePhone = (phone: string): string => phone.replace(/[^\d+]/g, ""); const normalizePhone = (phone: string): string => phone.replace(/[^\d+]/g, "");
@@ -26,6 +27,8 @@ export const getExcludedPlayerIds = async (): Promise<Set<string>> => {
if (phones.length > 0) { if (phones.length > 0) {
try { try {
const { ensureSuperTokensBackend } = await import("@/lib/supertokens/server");
ensureSuperTokensBackend();
const SuperTokens = (await import("supertokens-node")).default; const SuperTokens = (await import("supertokens-node")).default;
const { pbAdmin } = await import("@/lib/pocketbase/client"); const { pbAdmin } = await import("@/lib/pocketbase/client");
for (const phoneNumber of phones) { for (const phoneNumber of phones) {
@@ -37,6 +40,9 @@ export const getExcludedPlayerIds = async (): Promise<Set<string>> => {
} }
} catch (error) { } catch (error) {
logger.error("Failed to resolve telemetry exclusions", error); logger.error("Failed to resolve telemetry exclusions", error);
// Short-lived failure cache: an outage must not amplify into a
// per-request SuperTokens lookup storm.
cache = { playerIds, expiresAt: now + FAILURE_TTL_MS };
return playerIds; return playerIds;
} }
} }
+12
View File
@@ -95,6 +95,18 @@ describe("buildDayRollups", () => {
} }
}); });
it("omits wau/mau rows when trailing scans were skipped", () => {
const rollups = buildDayRollups("2026-08-25", {
activities: newActivityAccumulator(),
clientEvents: newClientEventAccumulator(),
clientErrors: newClientErrorAccumulator(),
partial: true,
});
expect(rollupFor(rollups, "wau")).toBeUndefined();
expect(rollupFor(rollups, "mau")).toBeUndefined();
expect(rollupFor(rollups, "dau")?.value).toBe(0);
});
it("marks partial days and is deterministic across re-runs", () => { it("marks partial days and is deterministic across re-runs", () => {
const build = () => const build = () =>
buildDayRollups("2026-08-25", { buildDayRollups("2026-08-25", {
+4 -4
View File
@@ -113,8 +113,8 @@ export const buildDayRollups = (
activities: ActivityAccumulator; activities: ActivityAccumulator;
clientEvents: ClientEventAccumulator; clientEvents: ClientEventAccumulator;
clientErrors: ClientErrorAccumulator; clientErrors: ClientErrorAccumulator;
wau: number; wau?: number;
mau: number; mau?: number;
partial: boolean; partial: boolean;
} }
): RollupInput[] => { ): RollupInput[] => {
@@ -133,8 +133,8 @@ export const buildDayRollups = (
const dayPlayers = new Set([...activities.players, ...clientEvents.players]); const dayPlayers = new Set([...activities.players, ...clientEvents.players]);
push("dau", "", dayPlayers.size); push("dau", "", dayPlayers.size);
push("wau", "", wau); if (wau !== undefined) push("wau", "", wau);
push("mau", "", mau); if (mau !== undefined) push("mau", "", mau);
for (const [name, fn] of activities.perFn) { for (const [name, fn] of activities.perFn) {
push("server_fn.count", name, fn.count); push("server_fn.count", name, fn.count);
+21 -10
View File
@@ -30,7 +30,7 @@ const dayFilter = (dateStr: string): string =>
const distinctPlayersInWindow = async (fromDate: string, toDateExclusive: string): Promise<number> => { const distinctPlayersInWindow = async (fromDate: string, toDateExclusive: string): Promise<number> => {
const players = new Set<string>(); const players = new Set<string>();
const filter = `created >= '${bound(fromDate)}' && created < '${bound(toDateExclusive)}' && player != ''`; const filter = `created >= '${bound(fromDate)}' && created < '${bound(toDateExclusive)}'`;
await pbAdmin.pageCollection<{ player?: string }>("activities", { filter, fields: "player,created" }, (rows) => { await pbAdmin.pageCollection<{ player?: string }>("activities", { filter, fields: "player,created" }, (rows) => {
for (const row of rows) if (row.player) players.add(row.player); for (const row of rows) if (row.player) players.add(row.player);
@@ -42,7 +42,10 @@ const distinctPlayersInWindow = async (fromDate: string, toDateExclusive: string
return players.size; return players.size;
}; };
export const runRollups = async (dateStr: string): Promise<number> => { export const runRollups = async (
dateStr: string,
opts: { trailing?: boolean } = {}
): Promise<number> => {
await pbAdmin.authPromise; await pbAdmin.authPromise;
const activities = newActivityAccumulator(); const activities = newActivityAccumulator();
@@ -65,11 +68,16 @@ export const runRollups = async (dateStr: string): Promise<number> => {
(rows) => accumulateClientErrors(clientErrors, rows) (rows) => accumulateClientErrors(clientErrors, rows)
); );
const nextDay = addDays(dateStr, 1); // Trailing scans re-read the full 7/30-day windows; hourly at most.
const [wau, mau] = await Promise.all([ let wau: number | undefined;
distinctPlayersInWindow(addDays(dateStr, -6), nextDay), let mau: number | undefined;
distinctPlayersInWindow(addDays(dateStr, -29), nextDay), if (opts.trailing !== false) {
]); const nextDay = addDays(dateStr, 1);
[wau, mau] = await Promise.all([
distinctPlayersInWindow(addDays(dateStr, -6), nextDay),
distinctPlayersInWindow(addDays(dateStr, -29), nextDay),
]);
}
const partial = dateStr >= utcDay(new Date()); const partial = dateStr >= utcDay(new Date());
const rollups = buildDayRollups(dateStr, { const rollups = buildDayRollups(dateStr, {
@@ -89,11 +97,14 @@ export const runRollups = async (dateStr: string): Promise<number> => {
return rollups.length; return rollups.length;
}; };
export const runScheduledRollups = async (lastRunDay?: string): Promise<string> => { export const runScheduledRollups = async (
lastRunDay: string | undefined,
opts: { trailing?: boolean } = {}
): Promise<string> => {
const today = utcDay(new Date()); const today = utcDay(new Date());
if (lastRunDay && lastRunDay !== today) { if (lastRunDay && lastRunDay !== today) {
await runRollups(lastRunDay); await runRollups(lastRunDay, { trailing: true });
} }
await runRollups(today); await runRollups(today, opts);
return today; return today;
}; };
+6 -1
View File
@@ -6,6 +6,7 @@ const logger = new Logger("Telemetry");
const ROLLUP_INTERVAL_MS = 15 * 60 * 1000; const ROLLUP_INTERVAL_MS = 15 * 60 * 1000;
const ALERT_INTERVAL_MS = 5 * 60 * 1000; const ALERT_INTERVAL_MS = 5 * 60 * 1000;
const TRAILING_INTERVAL_MS = 60 * 60 * 1000;
interface SchedulerStatus { interface SchedulerStatus {
running: boolean; running: boolean;
@@ -21,12 +22,16 @@ let initialized = false;
let rollupInFlight = false; let rollupInFlight = false;
let alertsInFlight = false; let alertsInFlight = false;
let lastRollupDay: string | undefined; let lastRollupDay: string | undefined;
let lastTrailingAt = 0;
const tickRollups = async () => { const tickRollups = async () => {
if (rollupInFlight) return; if (rollupInFlight) return;
rollupInFlight = true; rollupInFlight = true;
try { try {
lastRollupDay = await runScheduledRollups(lastRollupDay); const now = Date.now();
const trailing = now - lastTrailingAt >= TRAILING_INTERVAL_MS;
lastRollupDay = await runScheduledRollups(lastRollupDay, { trailing });
if (trailing) lastTrailingAt = now;
status.lastRollupAt = new Date().toISOString(); status.lastRollupAt = new Date().toISOString();
status.lastRollupError = undefined; status.lastRollupError = undefined;
} catch (error) { } catch (error) {
+36
View File
@@ -308,6 +308,42 @@ describe("serverFnLoggingMiddleware control flow and dedup", () => {
expect(h.createActivity.mock.calls[0][0].error).toContain("FORBIDDEN"); expect(h.createActivity.mock.calls[0][0].error).toContain("FORBIDDEN");
}); });
it("does not record anonymous Unauthenticated failures", async () => {
setRequest("http://localhost:3000/_serverFn/x");
await expect(
runMiddleware({
next: async () => {
throw new Error("Unauthenticated");
},
data: undefined,
context: {},
serverFnMeta: { id: "x", name: "fetchMe" },
})
).rejects.toThrow("Unauthenticated");
await flushWrites();
expect(h.createActivity).not.toHaveBeenCalled();
});
it("still records Unauthenticated failures for identified actors", async () => {
setRequest("http://localhost:3000/_serverFn/x");
setRequestActor(h.request as unknown as Request, { playerId: "p1" });
await expect(
runMiddleware({
next: async () => {
throw new Error("Unauthenticated");
},
data: undefined,
context: {},
serverFnMeta: { id: "x", name: "fetchMe" },
})
).rejects.toThrow("Unauthenticated");
await vi.waitFor(() => expect(h.createActivity).toHaveBeenCalledTimes(1));
});
it("does not record activity for excluded players", async () => { it("does not record activity for excluded players", async () => {
setRequest("http://localhost:3000/_serverFn/doThing"); setRequest("http://localhost:3000/_serverFn/doThing");
setRequestActor(h.request as unknown as Request, { playerId: "excluded-player" }); setRequestActor(h.request as unknown as Request, { playerId: "excluded-player" });
+6
View File
@@ -129,6 +129,12 @@ export const serverFnLoggingMiddleware = createMiddleware({
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
// Anonymous visitors (crawlers included) hitting authed reads are noise,
// not failures worth a row each.
if (errorMessage === "Unauthenticated" && !resolveActor()) {
throw error;
}
recordActivity({ recordActivity({
name, name,
player: resolveActor(), player: resolveActor(),