const DEBUG_LOG_ENABLED_KEY = "dbx-debug-logging-enabled"; const DEBUG_LOG_ENTRIES_KEY = "dbx-debug-log-entries"; const MAX_DEBUG_LOG_ENTRIES = 1500; const MAX_TEXT_LENGTH = 4000; const MAX_LABEL_LENGTH = 120; type DebugLogLevel = "debug" | "info" | "log" | "warn" | "error"; interface DebugLogEntry { timestamp: string; level: DebugLogLevel; message: string; } export interface BrowserMemorySnapshot { jsHeapUsedBytes?: number; jsHeapTotalBytes?: number; jsHeapLimitBytes?: number; deviceMemoryGiB?: number; hardwareConcurrency?: number; } export interface NativeProcessMemorySnapshot { residentBytes: number; virtualBytes: number; totalMemoryBytes: number; usedMemoryBytes: number; } let installed = false; let originalConsole: Partial void>> = {}; function safeLocalStorageGet(key: string): string | null { try { return localStorage.getItem(key); } catch { return null; } } function safeLocalStorageSet(key: string, value: string) { try { localStorage.setItem(key, value); } catch { // Ignore quota or unavailable storage errors. The app should keep running. } } function safeLocalStorageRemove(key: string) { try { localStorage.removeItem(key); } catch {} } export function isDebugLoggingEnabled(): boolean { return safeLocalStorageGet(DEBUG_LOG_ENABLED_KEY) === "1"; } function readEntries(): DebugLogEntry[] { const raw = safeLocalStorageGet(DEBUG_LOG_ENTRIES_KEY); if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.slice(-MAX_DEBUG_LOG_ENTRIES) : []; } catch { return []; } } function redactSensitiveText(value: string): string { return value.replace(/("[^"]*(?:password|passphrase|api[_-]?key|token|secret|jwt|connection[_-]?string)[^"]*"\s*:\s*")([^"]*)(")/gi, "$1[redacted]$3").replace(/\b([^=&\s]*(?:password|passphrase|api[_-]?key|token|secret|jwt)[^=&\s]*)=([^&\s]+)/gi, "$1=[redacted]"); } function formatValue(value: unknown, seen = new WeakSet()): string { if (value instanceof Error) { return redactSensitiveText([value.name, value.message, value.stack].filter(Boolean).join(": ")); } if (typeof value === "string") return redactSensitiveText(value); if (typeof value !== "object" || value === null) return String(value); if (seen.has(value)) return "[Circular]"; seen.add(value); try { return redactSensitiveText( JSON.stringify(value, (_key, item) => { if (typeof item === "function") return `[Function ${item.name || "anonymous"}]`; return item; }), ); } catch { return redactSensitiveText(String(value)); } } function formatArgs(args: unknown[]): string { const message = args.map((arg) => formatValue(arg)).join(" "); return message.length > MAX_TEXT_LENGTH ? `${message.slice(0, MAX_TEXT_LENGTH)}...` : message; } // Pads date/time parts to keep timestamps width-stable. export function padNumber(value: number, length = 2): string { return String(value).padStart(length, "0"); } // Formats the local UTC offset in RFC 3339 style. export function formatLocalTimezoneOffset(date: Date): string { const offsetMinutes = -date.getTimezoneOffset(); const sign = offsetMinutes >= 0 ? "+" : "-"; const absoluteMinutes = Math.abs(offsetMinutes); return `${sign}${padNumber(Math.floor(absoluteMinutes / 60))}:${padNumber(absoluteMinutes % 60)}`; } // Formats a local timestamp with milliseconds and timezone. export function formatLocalTimestamp(date = new Date()): string { return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(date.getMilliseconds(), 3)}${formatLocalTimezoneOffset(date)}`; } // Converts the local timestamp into a Windows-safe filename token. export function formatLocalTimestampForFilename(date = new Date()): string { return formatLocalTimestamp(date).replace(/[:.]/g, "-"); } export function appendDebugLog(level: DebugLogLevel, ...args: unknown[]) { if (!isDebugLoggingEnabled()) return; const entries = readEntries(); entries.push({ timestamp: formatLocalTimestamp(), level, message: formatArgs(args), }); safeLocalStorageSet(DEBUG_LOG_ENTRIES_KEY, JSON.stringify(entries.slice(-MAX_DEBUG_LOG_ENTRIES))); } export function getBrowserMemorySnapshot(): BrowserMemorySnapshot { const snapshot: BrowserMemorySnapshot = {}; const memory = (typeof performance !== "undefined" ? performance : undefined) as | (Performance & { memory?: { usedJSHeapSize?: number; totalJSHeapSize?: number; jsHeapSizeLimit?: number }; }) | undefined; if (memory?.memory?.usedJSHeapSize !== undefined) snapshot.jsHeapUsedBytes = memory.memory.usedJSHeapSize; if (memory?.memory?.totalJSHeapSize !== undefined) snapshot.jsHeapTotalBytes = memory.memory.totalJSHeapSize; if (memory?.memory?.jsHeapSizeLimit !== undefined) snapshot.jsHeapLimitBytes = memory.memory.jsHeapSizeLimit; if (typeof navigator !== "undefined") { const deviceMemory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory; if (deviceMemory !== undefined) snapshot.deviceMemoryGiB = deviceMemory; if (navigator.hardwareConcurrency) snapshot.hardwareConcurrency = navigator.hardwareConcurrency; } return snapshot; } export async function appendNativeProcessMemoryLog(event: string, context: Record = {}) { if (!isDebugLoggingEnabled() || !isTauriRuntimeLike()) return; try { const { invoke } = await import("@tauri-apps/api/core"); const memory = await invoke("get_process_memory_info"); appendDebugLog("info", `[DBX][native-memory:${event}]`, { ...context, ...memory }); } catch (error) { appendDebugLog("warn", `[DBX][native-memory:${event}:error]`, { ...context, error }); } } export function setDebugLoggingEnabled(enabled: boolean) { safeLocalStorageSet(DEBUG_LOG_ENABLED_KEY, enabled ? "1" : "0"); if (enabled) { appendDebugLog("info", "[DBX][debug-log] enabled", { url: location.href, viewport: `${window.innerWidth}x${window.innerHeight}`, devicePixelRatio: window.devicePixelRatio, language: navigator.language, online: navigator.onLine, }); } } export function clearDebugLogs() { safeLocalStorageRemove(DEBUG_LOG_ENTRIES_KEY); } export function getDebugLogText(): string { const entries = readEntries(); const header = [`DBX debug log`, `Exported: ${formatLocalTimestamp()}`, `User agent: ${navigator.userAgent}`, `Platform: ${navigator.platform}`, `Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown"}`, ""]; const body = entries.map((entry) => `[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.message}`); return [...header, ...body].join("\n"); } export async function downloadDebugLogs() { const text = await getDebugLogBundleText(); const filename = `dbx-debug-log-${formatLocalTimestampForFilename()}.txt`; if (typeof window !== "undefined" && isTauriRuntimeLike()) { const [{ save }, { writeTextFile }] = await Promise.all([import("@tauri-apps/plugin-dialog"), import("@tauri-apps/plugin-fs")]); const path = await save({ defaultPath: filename, filters: [{ name: "Text", extensions: ["txt", "log"] }], }); if (!path) return false; await writeTextFile(path, text); return true; } const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); return true; } export async function getDebugLogBundleText(): Promise { const sections = [getDebugLogText()]; const nativeLogs = await loadNativeDebugLogs().catch((error) => `Failed to load native logs: ${formatValue(error)}`); if (nativeLogs) { sections.push(["", "===== Native / Tauri logs =====", nativeLogs].join("\n")); } return sections.join("\n"); } async function loadNativeDebugLogs(): Promise { if (!isTauriRuntimeLike()) return ""; const { invoke } = await import("@tauri-apps/api/core"); return invoke("load_native_debug_logs"); } function isTauriRuntimeLike(): boolean { return "__TAURI_INTERNALS__" in globalThis || "__TAURI__" in globalThis; } export function installDebugLogCapture() { if (installed) return; installed = true; originalConsole = { debug: console.debug.bind(console), info: console.info.bind(console), log: console.log.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), }; (Object.keys(originalConsole) as DebugLogLevel[]).forEach((level) => { console[level] = (...args: unknown[]) => { appendDebugLog(level, ...args); originalConsole[level]?.(...args); }; }); window.addEventListener("error", (event) => { const target = event.target; if (target instanceof HTMLElement) { appendDebugLog("error", "[window:resource-error]", describeElement(target)); return; } appendDebugLog("error", "[window:error]", event.error ?? event.message); }); window.addEventListener("unhandledrejection", (event) => { appendDebugLog("error", "[window:unhandledrejection]", event.reason); }); window.addEventListener("online", () => appendDebugLog("info", "[window:online]")); window.addEventListener("offline", () => appendDebugLog("warn", "[window:offline]")); window.addEventListener("hashchange", () => appendDebugLog("info", "[window:hashchange]", location.hash)); document.addEventListener("visibilitychange", () => { appendDebugLog("info", "[document:visibilitychange]", document.visibilityState); }); document.addEventListener( "click", (event) => { const target = event.target instanceof HTMLElement ? interactiveElement(event.target) : null; if (!target) return; appendDebugLog("debug", "[ui:click]", describeElement(target)); }, true, ); document.addEventListener( "change", (event) => { const target = event.target instanceof HTMLElement ? event.target : null; if (!target || !isFormControl(target)) return; appendDebugLog("debug", "[ui:change]", describeElement(target)); }, true, ); installPerformanceCapture(); } function installPerformanceCapture() { if (typeof PerformanceObserver === "undefined") return; try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.duration >= 250) { appendDebugLog("warn", "[performance:long-task]", { name: entry.name, durationMs: Math.round(entry.duration), startTimeMs: Math.round(entry.startTime), }); } } }); observer.observe({ entryTypes: ["longtask"] }); } catch { // Long task entries are not available in every WebView. } } function interactiveElement(target: HTMLElement): HTMLElement | null { return target.closest(["button", "a[href]", "input", "select", "textarea", "[role='button']", "[role='menuitem']", "[role='tab']", "[data-debug-id]"].join(",")); } function isFormControl(target: HTMLElement): boolean { return target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target.getAttribute("role") === "switch"; } function describeElement(element: HTMLElement): Record { const label = element.getAttribute("data-debug-id") || element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || ""; const attrs: Record = { tag: element.tagName.toLowerCase(), }; if (element.id) attrs.id = element.id; const role = element.getAttribute("role"); if (role) attrs.role = role; const type = element.getAttribute("type"); if (type) attrs.type = type; const safeLabel = redactSensitiveText(label.trim().replace(/\s+/g, " ")); if (safeLabel) attrs.label = safeLabel.slice(0, MAX_LABEL_LENGTH); const testId = element.getAttribute("data-testid"); if (testId) attrs.testId = testId; return attrs; }