Development #24
@@ -41,6 +41,13 @@ const writeEvents = async (
|
||||
events: BeaconEvent[],
|
||||
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");
|
||||
await pbAdmin.authPromise;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Logger } from "@/lib/logger";
|
||||
import { isExcludedPhone, isExcludedPlayerId } from "./exclusions.server";
|
||||
|
||||
const logger = new Logger("Telemetry");
|
||||
|
||||
@@ -12,6 +13,8 @@ interface AuthEventInput {
|
||||
export const recordAuthEvent = (name: string, input: AuthEventInput) => {
|
||||
void (async () => {
|
||||
try {
|
||||
if (isExcludedPhone(input.phone)) return;
|
||||
|
||||
const { pbAdmin } = await import("@/lib/pocketbase/client");
|
||||
await pbAdmin.authPromise;
|
||||
|
||||
@@ -35,6 +38,8 @@ export const recordAuthEvent = (name: string, input: AuthEventInput) => {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (await isExcludedPlayerId(playerId)) return;
|
||||
|
||||
await pbAdmin.createActivity({
|
||||
name,
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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 { setRequestActor } from "@/lib/telemetry/request-context.server";
|
||||
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");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
setRequest("http://localhost:3000/_serverFn/doThing");
|
||||
setRequestActor(h.request as unknown as Request, { playerId: "p9" });
|
||||
|
||||
@@ -13,14 +13,17 @@ import type { ActivityInput } from "@/lib/pocketbase/services/activities";
|
||||
const logger = new Logger("Activities");
|
||||
|
||||
export const recordActivity = (activity: ActivityInput) => {
|
||||
import("@/lib/pocketbase/client")
|
||||
.then(async ({ pbAdmin }) => {
|
||||
void (async () => {
|
||||
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.createActivity(activity);
|
||||
})
|
||||
.catch((activityError) => {
|
||||
} catch (activityError) {
|
||||
logger.error("Failed to record activity", activityError);
|
||||
});
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const auditErrorMessage = (
|
||||
|
||||
Reference in New Issue
Block a user