Development #24
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 { 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" });
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
Reference in New Issue
Block a user