feat(telemetry): exclude configured phones from usage telemetry

- TELEMETRY_EXCLUDE_PHONES resolves to player ids at runtime, cached 10m
- excluded players write no activities, client events, or vitals
- their client errors are still captured
This commit is contained in:
2026-08-25 23:04:14 -07:00
parent c05bdec252
commit 9f5fac2753
6 changed files with 125 additions and 5 deletions
+7
View File
@@ -41,6 +41,13 @@ const writeEvents = async (
events: BeaconEvent[], events: BeaconEvent[],
base: { sessionId: string; playerId?: string; userAgent?: string } base: { sessionId: string; playerId?: string; userAgent?: string }
) => { ) => {
const { isExcludedPlayerId } = await import("@/lib/telemetry/exclusions.server");
if (base.playerId && (await isExcludedPlayerId(base.playerId))) {
// Excluded players still report errors; their usage stays out.
events = events.filter((event) => event.kind === "error");
if (events.length === 0) return;
}
const { pbAdmin } = await import("@/lib/pocketbase/client"); const { pbAdmin } = await import("@/lib/pocketbase/client");
await pbAdmin.authPromise; await pbAdmin.authPromise;
+5
View File
@@ -1,4 +1,5 @@
import { Logger } from "@/lib/logger"; import { Logger } from "@/lib/logger";
import { isExcludedPhone, isExcludedPlayerId } from "./exclusions.server";
const logger = new Logger("Telemetry"); const logger = new Logger("Telemetry");
@@ -12,6 +13,8 @@ interface AuthEventInput {
export const recordAuthEvent = (name: string, input: AuthEventInput) => { export const recordAuthEvent = (name: string, input: AuthEventInput) => {
void (async () => { void (async () => {
try { try {
if (isExcludedPhone(input.phone)) return;
const { pbAdmin } = await import("@/lib/pocketbase/client"); const { pbAdmin } = await import("@/lib/pocketbase/client");
await pbAdmin.authPromise; await pbAdmin.authPromise;
@@ -35,6 +38,8 @@ export const recordAuthEvent = (name: string, input: AuthEventInput) => {
} catch {} } catch {}
} }
if (await isExcludedPlayerId(playerId)) return;
await pbAdmin.createActivity({ await pbAdmin.createActivity({
name, name,
player: playerId, player: playerId,
@@ -0,0 +1,37 @@
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/logger", () => ({
Logger: class {
error() {}
info() {}
},
}));
import { isExcludedPhone } from "./exclusions.server";
const ENV_KEY = "TELEMETRY_EXCLUDE_PHONES";
afterEach(() => {
delete process.env[ENV_KEY];
});
describe("isExcludedPhone", () => {
it("matches exact and formatted variants of a configured phone", () => {
process.env[ENV_KEY] = "+17135550142";
expect(isExcludedPhone("+17135550142")).toBe(true);
expect(isExcludedPhone("+1 (713) 555-0142")).toBe(true);
expect(isExcludedPhone("+17135550143")).toBe(false);
});
it("supports a comma-separated list with whitespace", () => {
process.env[ENV_KEY] = "+17135550142, +14155550100";
expect(isExcludedPhone("+14155550100")).toBe(true);
});
it("excludes nothing when unset or empty", () => {
expect(isExcludedPhone("+17135550142")).toBe(false);
process.env[ENV_KEY] = "";
expect(isExcludedPhone("+17135550142")).toBe(false);
expect(isExcludedPhone(undefined)).toBe(false);
});
});
+49
View File
@@ -0,0 +1,49 @@
import { Logger } from "@/lib/logger";
const logger = new Logger("Telemetry");
const CACHE_TTL_MS = 10 * 60 * 1000;
const normalizePhone = (phone: string): string => phone.replace(/[^\d+]/g, "");
const excludedPhones = (): string[] =>
(process.env.TELEMETRY_EXCLUDE_PHONES ?? "")
.split(",")
.map((phone) => normalizePhone(phone.trim()))
.filter(Boolean);
export const isExcludedPhone = (phone?: string): boolean =>
!!phone && excludedPhones().includes(normalizePhone(phone));
let cache: { playerIds: Set<string>; expiresAt: number } | null = null;
export const getExcludedPlayerIds = async (): Promise<Set<string>> => {
const now = Date.now();
if (cache && cache.expiresAt > now) return cache.playerIds;
const playerIds = new Set<string>();
const phones = excludedPhones();
if (phones.length > 0) {
try {
const SuperTokens = (await import("supertokens-node")).default;
const { pbAdmin } = await import("@/lib/pocketbase/client");
for (const phoneNumber of phones) {
const users = await SuperTokens.listUsersByAccountInfo("public", { phoneNumber });
const authId = users[0]?.id;
if (!authId) continue;
const player = await pbAdmin.getPlayerByAuthId(authId);
if (player) playerIds.add(player.id);
}
} catch (error) {
logger.error("Failed to resolve telemetry exclusions", error);
return playerIds;
}
}
cache = { playerIds, expiresAt: now + CACHE_TTL_MS };
return playerIds;
};
export const isExcludedPlayerId = async (playerId?: string): Promise<boolean> =>
!!playerId && (await getExcludedPlayerIds()).has(playerId);
+19
View File
@@ -20,6 +20,11 @@ vi.mock("@/lib/logger", () => ({
}, },
})); }));
vi.mock("@/lib/telemetry/exclusions.server", () => ({
isExcludedPlayerId: async (playerId?: string) => playerId === "excluded-player",
isExcludedPhone: () => false,
}));
import { recordDeniedServerFn, serverFnLoggingMiddleware } from "./activities"; import { recordDeniedServerFn, serverFnLoggingMiddleware } from "./activities";
import { setRequestActor } from "@/lib/telemetry/request-context.server"; import { setRequestActor } from "@/lib/telemetry/request-context.server";
import { redirect } from "@tanstack/react-router"; import { redirect } from "@tanstack/react-router";
@@ -303,6 +308,20 @@ 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 activity for excluded players", async () => {
setRequest("http://localhost:3000/_serverFn/doThing");
setRequestActor(h.request as unknown as Request, { playerId: "excluded-player" });
await runMiddleware({
next: async () => successEnvelope,
data: undefined,
context: {},
});
await flushWrites();
expect(h.createActivity).not.toHaveBeenCalled();
});
it("prefers the stashed request actor over middleware context", async () => { it("prefers the stashed request actor over middleware context", async () => {
setRequest("http://localhost:3000/_serverFn/doThing"); setRequest("http://localhost:3000/_serverFn/doThing");
setRequestActor(h.request as unknown as Request, { playerId: "p9" }); setRequestActor(h.request as unknown as Request, { playerId: "p9" });
+8 -5
View File
@@ -13,14 +13,17 @@ import type { ActivityInput } from "@/lib/pocketbase/services/activities";
const logger = new Logger("Activities"); const logger = new Logger("Activities");
export const recordActivity = (activity: ActivityInput) => { export const recordActivity = (activity: ActivityInput) => {
import("@/lib/pocketbase/client") void (async () => {
.then(async ({ pbAdmin }) => { try {
const { isExcludedPlayerId } = await import("@/lib/telemetry/exclusions.server");
if (await isExcludedPlayerId(activity.player)) return;
const { pbAdmin } = await import("@/lib/pocketbase/client");
await pbAdmin.authPromise; await pbAdmin.authPromise;
await pbAdmin.createActivity(activity); await pbAdmin.createActivity(activity);
}) } catch (activityError) {
.catch((activityError) => {
logger.error("Failed to record activity", activityError); logger.error("Failed to record activity", activityError);
}); }
})();
}; };
const auditErrorMessage = ( const auditErrorMessage = (