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
86 lines
3.2 KiB
TypeScript
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");
|
|
});
|
|
}
|