import { EventStore } from "./event-store.js"; import { SqliteProjector } from "./sqlite-projector.js"; import { event, type EventIdentity, type UltraEventType } from "./events.js"; import type { BaseEvent, PrivacyProfile, UltraConfig } from "../types.js"; import { hmacId, loadOrCreateKey, sanitizeText } from "../security/privacy.js"; import { isSafeTelemetryMetadata } from "./safe-metadata.js"; const OMIT = /(?:raw|payload|tool(?:args?|output)?|stdout|stderr|output|reasoning|chain.?of.?thought|path|scope|command|environment|secret|token|password)/i; const TEXT = /(?:request|objective|summary|claim|description|message|url|repository|repo)/i; const SENSITIVE_ERROR_FIELDS = new Set(["error", "errormessage", "stack", "stacktrace", "partialtext", "partialresult"]); function keyId(key: string): string { return key.replace(/[^a-z0-9]/gi, "").toLowerCase(); } function sanitizeFields(value: unknown, secret: Buffer, key = ""): unknown { if (SENSITIVE_ERROR_FIELDS.has(keyId(key)) || (OMIT.test(key) && !isSafeTelemetryMetadata(key, value))) return undefined; if (isSafeTelemetryMetadata(key, value)) return value; if (typeof value === "string") return TEXT.test(key) ? sanitizeText(value, "restricted", secret) : sanitizeText(value, "internal", secret); if (Array.isArray(value)) return value.map((entry) => sanitizeFields(entry, secret, key)).filter((entry) => entry !== undefined); if (value && typeof value === "object") return Object.fromEntries(Object.entries(value as Record).flatMap(([childKey, childValue]) => { const sanitized = sanitizeFields(childValue, secret, childKey); return sanitized === undefined ? [] : [[childKey, sanitized]]; })); return value; } export class TelemetryCollector { private readonly store: EventStore; private readonly projector: SqliteProjector; constructor(eventsDir: string, database: string, private readonly hmacKey: string, private readonly config: UltraConfig, private readonly profile: PrivacyProfile) { this.store = new EventStore(eventsDir); this.projector = new SqliteProjector(database); } async record(identity: EventIdentity, eventType: UltraEventType, fields: Record = {}, now = new Date()): Promise { const secret = await loadOrCreateKey(this.hmacKey); const sanitized = sanitizeFields(fields, secret) as Record; const entry = event(this.config, this.profile, identity, eventType, sanitized, now); await this.store.append(entry); this.projector.project(entry); return entry; } async requestHash(request: string): Promise { return hmacId(await loadOrCreateKey(this.hmacKey), request, "REQUEST"); } async pruneAnalytics(retentionDays: number, now = Date.now()): Promise { const deleted = await this.store.prune(retentionDays, now); this.projector.rebuild(await this.store.all()); return deleted; } async rebuildProjection(): Promise { this.projector.rebuild(await this.store.all()); } close(): void { this.projector.close(); } }