import { invoke } from "@tauri-apps/api/core"; type TelemetryLevel = "info" | "warn" | "error"; const MAX_MESSAGE_LENGTH = 2_048; let telemetryEnabled = false; let configuration: Promise = Promise.resolve(); let frontendShuttingDown = false; export function beginFrontendShutdown() { frontendShuttingDown = true; } // Telemetry must never contain repository contents or identifying local data. function sanitize(message: string): string { return message .replace(/https?:\/\/\S+/gi, "[url]") .replace(/(?:[A-Za-z]:\\|\/)(?:[^\s<>:"|?*]+[\\/])*[^\s<>:"|?*]*/g, "[path]") .replace(/(token|password|secret|authorization|credential)\s*[:=]\s*\S+/gi, "$1=[redacted]") .replace(/[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}/g, "[email]") .slice(0, MAX_MESSAGE_LENGTH); } export function setTelemetryEnabled(enabled: boolean) { telemetryEnabled = enabled; configuration = invoke("set_telemetry_enabled", { enabled }).catch(() => {}); } export function telemetryLog(level: TelemetryLevel, message: string, eventName?: string) { if (!telemetryEnabled) return; void configuration.then(() => invoke("emit_frontend_log", { log: { level, message: sanitize(message), eventName, }, })) .catch(() => { // Observability must never interrupt the Git workflow. }); } function randomHex(bytes: number): string { const values = new Uint8Array(bytes); crypto.getRandomValues(values); return Array.from(values, (value) => value.toString(16).padStart(2, "0")).join(""); } export async function tracedInvoke(command: string, args?: Record): Promise { if (frontendShuttingDown) { // The window is on its way out and nothing calls preventDefault() on the // close event, so it will finish closing regardless. Never resolving // avoids surfacing a spurious "shutting down" error in whatever UI catch // block happens to be awaiting an in-flight operation (e.g. a pull/push // that makes several sequential calls) when the close request lands. return new Promise(() => {}); } if (!telemetryEnabled) return invoke(command, args); const startedAtMs = Date.now(); const started = performance.now(); const traceId = randomHex(16); const spanId = randomHex(8); try { const result = await invoke(command, args); void configuration.then(() => invoke("emit_frontend_span", { span: { name: command, traceId, spanId, startedAtMs, durationMs: performance.now() - started, success: true }, })).catch(() => {}); return result; } catch (error) { void configuration.then(() => invoke("emit_frontend_span", { span: { name: command, traceId, spanId, startedAtMs, durationMs: performance.now() - started, success: false }, })).catch(() => {}); throw error; } } export function installGlobalErrorTelemetry() { window.addEventListener("error", (event) => { telemetryLog("error", event.message || "Unhandled frontend error", "frontend.unhandled_error"); }); window.addEventListener("unhandledrejection", (event) => { const message = event.reason instanceof Error ? event.reason.message : String(event.reason ?? "Unhandled promise rejection"); telemetryLog("error", message, "frontend.unhandled_rejection"); }); }