import { fleetSettings } from '../settings.js'; import { db } from '../db/index.js'; import { type TelemetryRequestLogEntry } from '../../telemetry-diagnostics.js'; const MAX_PAYLOAD_LENGTH = 16 * 1024; // Bound storage even when requests claim many different, unregistered names. export function telemetryRetention() { const settings = fleetSettings(); return { maxAgeDays: settings.telemetryKeepDays, maxPerServer: settings.telemetryMaxPerServer, maxTotal: settings.telemetryMaxTotal }; } export const telemetryCutoff = () => telemetryRetention().maxAgeDays ? new Date(Date.now() - telemetryRetention().maxAgeDays * 86_400_000).toISOString() : ''; const cutoff = telemetryCutoff; const textField = (value: unknown) => (typeof value === 'string' ? value.slice(0, 255) : null); export function recordTelemetryRequest(input: { value: unknown; receivedAt: string; status: number; error: string | null; contentType: string | null; }): void { // Use only the top-level JSON field, never a player's name or a substring. // Malformed JSON cannot be reliably attributed to an individual server. if (!input.value || typeof input.value !== 'object' || Array.isArray(input.value)) return; const body = input.value as Record; if (typeof body.server !== 'string' || !body.server.trim() || body.server.length > 255) return; const payload = JSON.stringify(body); const handle = db(); handle.transaction(() => { handle .prepare( `INSERT INTO telemetry_requests (server, received_at, status, error, event, map, round, player_count, content_type, payload, payload_truncated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .run( body.server, input.receivedAt, input.status, input.error?.slice(0, 2000) ?? null, textField(body.event), textField(body.map), Number.isSafeInteger(body.round) ? (body.round as number) : null, Array.isArray(body.players) ? body.players.length : null, textField(input.contentType), payload.slice(0, MAX_PAYLOAD_LENGTH), Number(payload.length > MAX_PAYLOAD_LENGTH) ); if (telemetryRetention().maxAgeDays) handle.prepare('DELETE FROM telemetry_requests WHERE received_at < ?').run(cutoff()); if (telemetryRetention().maxPerServer) handle .prepare( `DELETE FROM telemetry_requests WHERE server = ? AND id <= ( SELECT id FROM telemetry_requests WHERE server = ? ORDER BY id DESC LIMIT 1 OFFSET ? )` ) .run(body.server, body.server, telemetryRetention().maxPerServer); if (telemetryRetention().maxTotal) handle .prepare( `DELETE FROM telemetry_requests WHERE id <= ( SELECT id FROM telemetry_requests ORDER BY id DESC LIMIT 1 OFFSET ? )` ) .run(telemetryRetention().maxTotal); })(); } export function listTelemetryRequests(server: string): TelemetryRequestLogEntry[] { const rows = db() .prepare( `SELECT id, server, received_at AS receivedAt, status, error, event, map, round, player_count AS playerCount, content_type AS contentType, payload, payload_truncated AS payloadTruncated FROM telemetry_requests WHERE server = ? AND received_at >= ? ORDER BY id DESC LIMIT ?` ) .all(server, cutoff(), telemetryRetention().maxPerServer || 10000) as Array< Omit & { payloadTruncated: number } >; return rows.map((row) => ({ ...row, payloadTruncated: !!row.payloadTruncated })); }