Files
GitLite/src/lib/telemetry.ts
T
Christoph Brandau ac7cacd687 feat(telemetry): handle application shutdown process
This update introduces a structured shutdown process for the application. It ensures that background tasks are properly terminated and telemetry is notified when the frontend is shutting down, preventing any ongoing operations from continuing during this state.

- Added event listeners to manage app shutdown events
- Implemented a shutdown handler to clear timers and notify telemetry
- Updated background task checks to respect the shutdown state
2026-08-10 17:29:03 +02:00

81 lines
2.9 KiB
TypeScript

import { invoke } from "@tauri-apps/api/core";
type TelemetryLevel = "info" | "warn" | "error";
const MAX_MESSAGE_LENGTH = 2_048;
let telemetryEnabled = false;
let configuration: Promise<unknown> = 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<T>(command: string, args?: Record<string, unknown>): Promise<T> {
if (frontendShuttingDown) {
throw new Error("Application is shutting down");
}
if (!telemetryEnabled) return invoke<T>(command, args);
const startedAtMs = Date.now();
const started = performance.now();
const traceId = randomHex(16);
const spanId = randomHex(8);
try {
const result = await invoke<T>(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");
});
}