Files
GitLite/src/lib/telemetry.ts
T
Christoph Brandau f621638eb3
publish / Build and publish Ubuntu AppImage (release) Successful in 22m40s
publish / Build and publish Windows installer (release) Successful in 31m3s
publish / Build and publish gitty-desktop to AUR (release) Successful in 50m26s
feat(system): enable window destroy capability and stabilize shutdown
Adds a new window capability to allow destroying the window.
It also changes shutdown handling in telemetry to avoid errors.

- Add core:window:allow-destroy to default capabilities
- Avoid shutdown errors by returning a pending promise in tracedInvoke
- Improve stability during shutdown for in-flight telemetry calls
2026-08-13 22:11:45 +02:00

86 lines
3.2 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) {
// 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<T>(() => {});
}
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");
});
}