Development #24
@@ -9,8 +9,7 @@ export const Route = createFileRoute("/api/health")({
|
|||||||
ensureTelemetryScheduler();
|
ensureTelemetryScheduler();
|
||||||
const snapshot = await getHealthSnapshot();
|
const snapshot = await getHealthSnapshot();
|
||||||
|
|
||||||
// Always 200: a PB/SuperTokens outage must not fail the k8s probes;
|
// Always 200: a dependency outage must not fail the k8s probes.
|
||||||
// restarting the app pod cannot fix a dependency.
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
status: snapshot.status,
|
status: snapshot.status,
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ const writeEvents = async (
|
|||||||
const { pbAdmin } = await import("@/lib/pocketbase/client");
|
const { pbAdmin } = await import("@/lib/pocketbase/client");
|
||||||
await pbAdmin.authPromise;
|
await pbAdmin.authPromise;
|
||||||
|
|
||||||
// Sequential on purpose: SQLite is single-writer; a burst of parallel
|
|
||||||
// creates just queues behind the lock.
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
try {
|
try {
|
||||||
if (event.kind === "error") {
|
if (event.kind === "error") {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ const csrfMiddleware = createCsrfMiddleware({
|
|||||||
filter: (ctx) => ctx.handlerType === "serverFn",
|
filter: (ctx) => ctx.handlerType === "serverFn",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global function middleware: every server fn (current and future) is audited
|
|
||||||
// without opting in at the definition site.
|
|
||||||
export const startInstance = createStart(() => ({
|
export const startInstance = createStart(() => ({
|
||||||
requestMiddleware: [csrfMiddleware],
|
requestMiddleware: [csrfMiddleware],
|
||||||
functionMiddleware: [serverFnLoggingMiddleware],
|
functionMiddleware: [serverFnLoggingMiddleware],
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ interface QuietBoundaryState {
|
|||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One failing telemetry card must not blank the whole dashboard.
|
|
||||||
export class QuietBoundary extends Component<QuietBoundaryProps, QuietBoundaryState> {
|
export class QuietBoundary extends Component<QuietBoundaryProps, QuietBoundaryState> {
|
||||||
state: QuietBoundaryState = { hasError: false };
|
state: QuietBoundaryState = { hasError: false };
|
||||||
|
|
||||||
|
|||||||
@@ -99,8 +99,6 @@ export const getClientErrorGroups = createServerFn()
|
|||||||
const sample = latest.items[0];
|
const sample = latest.items[0];
|
||||||
if (!sample) continue;
|
if (!sample) continue;
|
||||||
|
|
||||||
// A group counts as resolved when its most recent occurrence is
|
|
||||||
// resolved; a recurrence arrives resolved=false and surfaces again.
|
|
||||||
const resolved = !!sample.resolved;
|
const resolved = !!sample.resolved;
|
||||||
if (resolved && !includeResolved) continue;
|
if (resolved && !includeResolved) continue;
|
||||||
|
|
||||||
@@ -161,8 +159,6 @@ export const getTelemetryRuntimeStatus = createServerFn()
|
|||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async () =>
|
.handler(async () =>
|
||||||
toServerResult<TelemetryRuntimeStatus>(async () => {
|
toServerResult<TelemetryRuntimeStatus>(async () => {
|
||||||
// Dev has no k8s probes hitting /api/health, so opening the dashboard
|
|
||||||
// is the scheduler's fallback starter.
|
|
||||||
ensureTelemetryScheduler();
|
ensureTelemetryScheduler();
|
||||||
const health = await getHealthSnapshot();
|
const health = await getHealthSnapshot();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -199,8 +199,7 @@ export function createTelemetryService(pb: PocketBase) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Idempotent by the (date, metric, dim) unique index; dim must be "" (never
|
// dim must be "" (never null/undefined) or the unique index stops deduplicating.
|
||||||
// null/undefined) or the index stops deduplicating.
|
|
||||||
async upsertRollup(input: RollupInput): Promise<RollupRecord> {
|
async upsertRollup(input: RollupInput): Promise<RollupRecord> {
|
||||||
const data = { ...input, dim: input.dim ?? "" };
|
const data = { ...input, dim: input.dim ?? "" };
|
||||||
const filter = pbFilter(pb, "date = {:date} && metric = {:metric} && dim = {:dim}", {
|
const filter = pbFilter(pb, "date = {:date} && metric = {:metric} && dim = {:dim}", {
|
||||||
@@ -277,8 +276,6 @@ export function createTelemetryService(pb: PocketBase) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Bounded flip of a group's rows; new occurrences arrive resolved=false by
|
|
||||||
// design, so a partial flip on a huge group is acceptable.
|
|
||||||
async resolveErrorsByGroup(groupHash: string, resolved = true): Promise<number> {
|
async resolveErrorsByGroup(groupHash: string, resolved = true): Promise<number> {
|
||||||
const MAX_PAGES = 25;
|
const MAX_PAGES = 25;
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
@@ -300,8 +297,6 @@ export function createTelemetryService(pb: PocketBase) {
|
|||||||
return updated;
|
return updated;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Paged scan over a base collection (never SQL views). Stable created sort;
|
|
||||||
// tight `fields` keeps payloads small for rollup passes.
|
|
||||||
async pageCollection<T>(
|
async pageCollection<T>(
|
||||||
collection: string,
|
collection: string,
|
||||||
opts: { filter?: string; fields?: string },
|
opts: { filter?: string; fields?: string },
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { createMiddleware } from "@tanstack/react-start";
|
|||||||
import { recordActivity } from "@/utils/activities";
|
import { recordActivity } from "@/utils/activities";
|
||||||
import { getRequestActor } from "./request-context.server";
|
import { getRequestActor } from "./request-context.server";
|
||||||
|
|
||||||
// Request-type counterpart of serverFnLoggingMiddleware for createFileRoute
|
|
||||||
// API handlers, which the global function middleware never sees. Not for the
|
|
||||||
// SSE route (a long-lived stream is not a request-shaped unit) or /api/health.
|
|
||||||
export const apiLoggingMiddleware = createMiddleware({
|
export const apiLoggingMiddleware = createMiddleware({
|
||||||
type: "request",
|
type: "request",
|
||||||
}).server(async ({ next, request }) => {
|
}).server(async ({ next, request }) => {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ interface AuthEventInput {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire-and-forget: auth flows must never fail or slow down because telemetry
|
|
||||||
// did. The phone number is used only to resolve the player, never stored.
|
|
||||||
export const recordAuthEvent = (name: string, input: AuthEventInput) => {
|
export const recordAuthEvent = (name: string, input: AuthEventInput) => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
@@ -52,8 +50,6 @@ export const recordAuthEvent = (name: string, input: AuthEventInput) => {
|
|||||||
|
|
||||||
type PasswordlessOverride = (originalImplementation: any) => any;
|
type PasswordlessOverride = (originalImplementation: any) => any;
|
||||||
|
|
||||||
// Composes with a recipe's existing override.functions so telemetry wraps the
|
|
||||||
// final implementation (Twilio verify included).
|
|
||||||
export const withPasswordlessTelemetry =
|
export const withPasswordlessTelemetry =
|
||||||
(existing?: PasswordlessOverride): PasswordlessOverride =>
|
(existing?: PasswordlessOverride): PasswordlessOverride =>
|
||||||
(originalImplementation: any) => {
|
(originalImplementation: any) => {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import type { AnyRouter } from "@tanstack/react-router";
|
import type { AnyRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
// Provider-free by design: renders nothing and mounts no React context, so it
|
|
||||||
// cannot disturb the LinguiProvider -> AuthProvider order. Browser-only; every
|
|
||||||
// entry point is a no-op during SSR. Runs in dev too - events go to the same
|
|
||||||
// PB the app is pointed at, which in dev is the local instance.
|
|
||||||
|
|
||||||
type QueuedEvent = Record<string, unknown> & { kind: "event" | "error" | "vital" };
|
type QueuedEvent = Record<string, unknown> & { kind: "event" | "error" | "vital" };
|
||||||
|
|
||||||
const FLUSH_INTERVAL_MS = 10_000;
|
const FLUSH_INTERVAL_MS = 10_000;
|
||||||
@@ -148,8 +143,7 @@ export const initTelemetry = (router: AnyRouter) => {
|
|||||||
captureError("unhandledrejection", event.reason);
|
captureError("unhandledrejection", event.reason);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vitals mostly report at hidden-visibility; their callbacks are registered
|
// Vitals report at hidden-visibility; register before the flush listeners below.
|
||||||
// before the flush listeners so those reports make the final beacon.
|
|
||||||
void import("web-vitals")
|
void import("web-vitals")
|
||||||
.then(({ onLCP, onCLS, onINP, onFCP, onTTFB }) => {
|
.then(({ onLCP, onCLS, onINP, onFCP, onTTFB }) => {
|
||||||
const report = (metric: { name: string; value: number; rating: string }) => {
|
const report = (metric: { name: string; value: number; rating: string }) => {
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Deterministic client-error grouping across deploys: volatile fragments
|
|
||||||
// (ids, URLs, asset content hashes) are normalized away before hashing.
|
|
||||||
|
|
||||||
const normalizeMessage = (message: string): string =>
|
const normalizeMessage = (message: string): string =>
|
||||||
message
|
message
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -13,8 +10,6 @@ const normalizeMessage = (message: string): string =>
|
|||||||
|
|
||||||
const FRAME_NOISE = /^(at\s+)?(async\s+)?(module code|<anonymous>|eval)/i;
|
const FRAME_NOISE = /^(at\s+)?(async\s+)?(module code|<anonymous>|eval)/i;
|
||||||
|
|
||||||
// Vite emits content-hashed asset names (index-Bq2z1a3f.js); strip the hash so
|
|
||||||
// the same frame groups across deploys.
|
|
||||||
const normalizeFrame = (frame: string): string =>
|
const normalizeFrame = (frame: string): string =>
|
||||||
frame
|
frame
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ const runChecks = async (): Promise<HealthSnapshot> => {
|
|||||||
return cached;
|
return cached;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stale-while-revalidate: k8s probes get the cached snapshot instantly while a
|
|
||||||
// refresh runs in the background; only the very first request awaits a check.
|
|
||||||
export const getHealthSnapshot = async (): Promise<HealthSnapshot> => {
|
export const getHealthSnapshot = async (): Promise<HealthSnapshot> => {
|
||||||
if (cached && Date.now() - Date.parse(cached.checkedAt) < CHECK_TTL_MS) {
|
if (cached && Date.now() - Date.parse(cached.checkedAt) < CHECK_TTL_MS) {
|
||||||
return cached;
|
return cached;
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Per-request side-channel between the session layer and the global logging
|
|
||||||
// middleware, which runs outside session middleware and cannot see its context.
|
|
||||||
|
|
||||||
const actorByRequest = new WeakMap<Request, { playerId?: string }>();
|
const actorByRequest = new WeakMap<Request, { playerId?: string }>();
|
||||||
const deniedRecorded = new WeakSet<Request>();
|
const deniedRecorded = new WeakSet<Request>();
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export interface ClientErrorRow {
|
|||||||
group_hash: string;
|
group_hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nearest-rank percentile; values need not be pre-sorted.
|
|
||||||
export const percentile = (values: number[], p: number): number => {
|
export const percentile = (values: number[], p: number): number => {
|
||||||
if (values.length === 0) return 0;
|
if (values.length === 0) return 0;
|
||||||
const sorted = [...values].sort((a, b) => a - b);
|
const sorted = [...values].sort((a, b) => a - b);
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ const addDays = (dateStr: string, days: number): string => {
|
|||||||
return utcDay(d);
|
return utcDay(d);
|
||||||
};
|
};
|
||||||
|
|
||||||
// PB stores autodates as "YYYY-MM-DD HH:MM:SS.sssZ" UTC; boundaries in the
|
|
||||||
// same shape compare correctly in filters.
|
|
||||||
const bound = (dateStr: string): string => `${dateStr} 00:00:00.000Z`;
|
const bound = (dateStr: string): string => `${dateStr} 00:00:00.000Z`;
|
||||||
|
|
||||||
const dayFilter = (dateStr: string): string =>
|
const dayFilter = (dateStr: string): string =>
|
||||||
@@ -91,8 +89,6 @@ export const runRollups = async (dateStr: string): Promise<number> => {
|
|||||||
return rollups.length;
|
return rollups.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The first run after UTC midnight recomputes yesterday once more so its
|
|
||||||
// partial rows become final. Idempotent via the (date, metric, dim) key.
|
|
||||||
export const runScheduledRollups = async (lastRunDay?: string): Promise<string> => {
|
export const runScheduledRollups = async (lastRunDay?: string): Promise<string> => {
|
||||||
const today = utcDay(new Date());
|
const today = utcDay(new Date());
|
||||||
if (lastRunDay && lastRunDay !== today) {
|
if (lastRunDay && lastRunDay !== today) {
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ const tickAlerts = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Single-replica deployment makes in-process intervals a valid scheduler
|
// In-process intervals are valid only while the deployment runs a single replica.
|
||||||
// (same precedent as the badge migration job). Started lazily from request
|
|
||||||
// handlers (health probes guarantee a call within seconds of boot).
|
|
||||||
export const ensureTelemetryScheduler = () => {
|
export const ensureTelemetryScheduler = () => {
|
||||||
if (initialized || typeof window !== "undefined") return;
|
if (initialized || typeof window !== "undefined") return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ const serverFnName = (request: Request, meta?: ServerFnMeta): string => {
|
|||||||
return pathParts[pathParts.length - 1] || "unknown";
|
return pathParts[pathParts.length - 1] || "unknown";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Successful reads are skipped (client page views cover usage); read failures
|
|
||||||
// are always recorded. Naming is uniform across every feature server.ts.
|
|
||||||
export const READ_FN_NAME = /^(get|list|search|fetch|preview|is[A-Z])/;
|
export const READ_FN_NAME = /^(get|list|search|fetch|preview|is[A-Z])/;
|
||||||
|
|
||||||
const readSampleRate = (): number => {
|
const readSampleRate = (): number => {
|
||||||
@@ -56,8 +54,6 @@ const shouldRecordSuccess = (name: string): boolean => {
|
|||||||
return sample > 0 && Math.random() < sample;
|
return sample > 0 && Math.random() < sample;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Thrown Responses/redirects are control flow (session refresh 401s, route
|
|
||||||
// redirects), not failures.
|
|
||||||
const isControlFlowError = (error: unknown): boolean =>
|
const isControlFlowError = (error: unknown): boolean =>
|
||||||
error instanceof Response || isRedirect(error);
|
error instanceof Response || isRedirect(error);
|
||||||
|
|
||||||
@@ -86,8 +82,7 @@ export const serverFnLoggingMiddleware = createMiddleware({
|
|||||||
const userAgent = request.headers.get("user-agent") || undefined;
|
const userAgent = request.headers.get("user-agent") || undefined;
|
||||||
const safeArgs = redactArguments(data);
|
const safeArgs = redactArguments(data);
|
||||||
|
|
||||||
// Registered globally, so session middleware runs inside this one; the actor
|
// Runs outside session middleware; the actor is stashed on the Request and read after next().
|
||||||
// is stashed on the Request by getSessionContextCore and read after next().
|
|
||||||
const resolveActor = (): string | undefined =>
|
const resolveActor = (): string | undefined =>
|
||||||
getRequestActor(request)?.playerId ??
|
getRequestActor(request)?.playerId ??
|
||||||
((context as any)?.metadata?.player_id as string | undefined);
|
((context as any)?.metadata?.player_id as string | undefined);
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ export const superTokensRequestMiddleware = createMiddleware({
|
|||||||
return next({ context });
|
return next({ context });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Session context when available, anonymous otherwise — never throws or
|
|
||||||
// redirects. For endpoints that must accept logged-out traffic (telemetry
|
|
||||||
// beacon: login-page errors are exactly the signal we want).
|
|
||||||
export const superTokensOptionalRequestMiddleware = createMiddleware({
|
export const superTokensOptionalRequestMiddleware = createMiddleware({
|
||||||
type: "request",
|
type: "request",
|
||||||
}).server(async ({ next, request }) => {
|
}).server(async ({ next, request }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user