Files
flxn-app/src/utils/activities.ts
T
kyle 1ddcffc511 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
2026-08-25 23:26:34 -07:00

151 lines
4.5 KiB
TypeScript

import { createMiddleware } from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";
import { isRedirect } from "@tanstack/react-router";
import { Logger } from "@/lib/logger";
import { redactArguments } from "@/lib/telemetry/redact-args";
import {
getRequestActor,
markDeniedRecorded,
wasDeniedRecorded,
} from "@/lib/telemetry/request-context.server";
import type { ActivityInput } from "@/lib/pocketbase/services/activities";
const logger = new Logger("Activities");
export const recordActivity = (activity: ActivityInput) => {
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) {
logger.error("Failed to record activity", activityError);
}
})();
};
const auditErrorMessage = (
error?: { code?: string; userMessage?: string }
): string => {
if (!error) return "error";
return [error.code, error.userMessage].filter(Boolean).join(": ") || "error";
};
type Actor = { player_id?: string };
type ServerFnMeta = { name?: string };
// The URL segment is a hash of the fn id; serverFnMeta carries the readable name.
const serverFnName = (request: Request, meta?: ServerFnMeta): string => {
if (meta?.name) return meta.name;
const pathParts = new URL(request.url).pathname.split("/").filter(Boolean);
return pathParts[pathParts.length - 1] || "unknown";
};
export const READ_FN_NAME = /^(get|list|search|fetch|preview|is[A-Z])/;
const readSampleRate = (): number => {
const raw = Number(process.env.TELEMETRY_READ_SAMPLE);
return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 1) : 0;
};
const shouldRecordSuccess = (name: string): boolean => {
if (!READ_FN_NAME.test(name)) return true;
const sample = readSampleRate();
return sample > 0 && Math.random() < sample;
};
const isControlFlowError = (error: unknown): boolean =>
error instanceof Response || isRedirect(error);
export const recordDeniedServerFn = (
request: Request,
actor?: Actor,
meta?: ServerFnMeta
) => {
markDeniedRecorded(request);
recordActivity({
name: serverFnName(request, meta),
player: actor?.player_id,
duration: 0,
success: false,
error: "FORBIDDEN: Access denied",
user_agent: request.headers.get("user-agent") || undefined,
});
};
export const serverFnLoggingMiddleware = createMiddleware({
type: "function",
}).server(async ({ next, data, context, serverFnMeta }) => {
const request = getRequest();
const name = serverFnName(request, serverFnMeta);
const userAgent = request.headers.get("user-agent") || undefined;
const safeArgs = redactArguments(data);
// Runs outside session middleware; the actor is stashed on the Request and read after next().
const resolveActor = (): string | undefined =>
getRequestActor(request)?.playerId ??
((context as any)?.metadata?.player_id as string | undefined);
const startTime = Date.now();
try {
const result = await next();
const duration = Date.now() - startTime;
const envelope = (result as { result?: unknown })?.result;
const failed =
!!envelope &&
typeof envelope === "object" &&
"success" in envelope &&
(envelope as { success: boolean }).success === false;
if (failed || shouldRecordSuccess(name)) {
recordActivity({
name,
player: resolveActor(),
duration,
success: !failed,
error: failed
? auditErrorMessage(
(envelope as { error?: { code?: string; userMessage?: string } })
.error
)
: undefined,
arguments: safeArgs,
user_agent: userAgent,
});
}
return result;
} catch (error) {
if (isControlFlowError(error) || wasDeniedRecorded(request)) {
throw error;
}
const duration = Date.now() - startTime;
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({
name,
player: resolveActor(),
duration,
success: false,
error: errorMessage,
arguments: safeArgs,
user_agent: userAgent,
});
throw error;
}
});