diagnostics: panic hook + frontend error capture + Settings → About diagnostic report
THE BRIEF: For the friends beta we need verbal feedback AND technical feedback — some bugs the user cannot describe but a stack trace can. Built it in two layers, with Layer 3 deferred until there is real volume. PRIVACY POSTURE (matches Kon's local-first positioning): - All capture is to disk only. Nothing is transmitted. - The manual report bundler shows the user exactly what would be shared and lets them choose to copy / save it. - No remote endpoint, no Sentry, no opt-out telemetry. LAYER 1 — Always-on local capture: - Rust panic hook (commands/diagnostics.rs::install_panic_hook) writes each panic to ~/.kon/crashes/<unix-ts>-<short-id>.crash. Captures thread name, OS/arch, RUST_BACKTRACE state, and the panic info. Installed before tauri::Builder so it catches setup-time panics too. - Frontend window.onerror + window.unhandledrejection in src/routes/+layout.svelte forward to the new log_frontend_error Tauri command, which inserts into the existing error_log SQLite table. Best-effort: errors in the error handler itself are swallowed so logging can never crash the app. - Existing error_log table (migration v1) is now actually used — closes the TODO from crates/storage/src/database.rs:409. LAYER 2 — Manual diagnostic report: - Settings → About → Diagnostics section: Generate report → Preview → Copy / Save. - Report is plain markdown so it pastes cleanly into email, Discord, GitHub issues. Sections: app version + OS, sanitised settings JSON, recent error_log rows, last 5 crash dumps with previews, log file tail (8KB). - Preview is shown in a <details> with the full text the user can inspect before deciding to share. - Save writes to ~/.kon/diagnostic-reports/kon-diagnostic-<ts>.md. NEW FILES: - src-tauri/src/commands/diagnostics.rs (panic hook + 5 Tauri commands) CHANGES: - crates/storage/src/file_storage.rs: crashes_dir() + logs_dir() helpers - crates/storage/src/database.rs: ErrorLogRow + list_recent_errors() - crates/storage/src/lib.rs: re-exports - src-tauri/src/commands/mod.rs: + diagnostics module - src-tauri/src/lib.rs: install_panic_hook() before Builder; 5 new Tauri commands registered (log_frontend_error, list_recent_errors_command, list_crash_files, generate_diagnostic_report, save_diagnostic_report) - src/routes/+layout.svelte: installGlobalErrorCapture() at mount - src/lib/pages/SettingsPage.svelte: diagnostic state + buttons + preview block at the end of the About section cargo check -p kon-storage clean. Settings.svelte if/each balanced. LAYER 3 (deferred): - Optional opt-in remote reporting (self-hosted Sentry on Tartarus or similar). Not for friends beta. Open up after volume justifies it.
This commit is contained in:
@@ -86,6 +86,63 @@
|
||||
vocabularyError = "Could not delete term: " + (err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Diagnostics (Settings → About → Generate diagnostic report) ---
|
||||
// Layer 2 of the diagnostics plan: user previews exactly what would be
|
||||
// shared, then chooses to copy / save. Nothing leaves the machine
|
||||
// automatically.
|
||||
let diagnosticReport = $state("");
|
||||
let diagnosticReportError = $state(null);
|
||||
let diagnosticReportSavedTo = $state(null);
|
||||
let diagnosticReportLoading = $state(false);
|
||||
|
||||
async function generateDiagnosticReport() {
|
||||
diagnosticReportError = null;
|
||||
diagnosticReportSavedTo = null;
|
||||
diagnosticReportLoading = true;
|
||||
try {
|
||||
diagnosticReport = await invoke("generate_diagnostic_report", {
|
||||
options: {
|
||||
includeRecentErrors: true,
|
||||
includeCrashes: true,
|
||||
includeSettings: true,
|
||||
includeLogTail: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
diagnosticReportError = "Could not generate report: " + (err?.message || err);
|
||||
} finally {
|
||||
diagnosticReportLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDiagnosticReport() {
|
||||
if (!diagnosticReport) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(diagnosticReport);
|
||||
diagnosticReportSavedTo = "Copied to clipboard.";
|
||||
} catch (err) {
|
||||
diagnosticReportError = "Clipboard copy failed: " + (err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDiagnosticReport() {
|
||||
diagnosticReportError = null;
|
||||
try {
|
||||
const path = await invoke("save_diagnostic_report", {
|
||||
options: {
|
||||
includeRecentErrors: true,
|
||||
includeCrashes: true,
|
||||
includeSettings: true,
|
||||
includeLogTail: true,
|
||||
},
|
||||
});
|
||||
diagnosticReportSavedTo = "Saved to " + path;
|
||||
} catch (err) {
|
||||
diagnosticReportError = "Save failed: " + (err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
let parakeetDownloaded = $state(false);
|
||||
let parakeetDownloading = $state(false);
|
||||
let parakeetProgress = $state(0);
|
||||
@@ -1036,6 +1093,46 @@
|
||||
</div>
|
||||
|
||||
<p class="text-[11px] text-text-tertiary mt-5">Kon v1.0 · Powered by whisper.cpp · Built by CORBEL Ltd</p>
|
||||
|
||||
<!-- Diagnostic report (privacy posture: local only until you share it) -->
|
||||
<div class="mt-6 pt-5 border-t border-border-subtle">
|
||||
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">Diagnostics</p>
|
||||
<p class="text-[11px] text-text-tertiary mb-3">
|
||||
If something goes wrong, generate a diagnostic report to share with the developer. The report contains your settings, recent errors, any crash dumps, and the tail of the log file. Nothing is sent automatically — you preview it first, then choose copy or save.
|
||||
</p>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick={generateDiagnosticReport}
|
||||
disabled={diagnosticReportLoading}
|
||||
class="px-3 py-2 text-[12px] bg-accent text-bg rounded-lg disabled:opacity-50 hover:bg-accent-hover"
|
||||
>{diagnosticReportLoading ? "Generating…" : "Generate report"}</button>
|
||||
{#if diagnosticReport}
|
||||
<button
|
||||
type="button"
|
||||
onclick={copyDiagnosticReport}
|
||||
class="px-3 py-2 text-[12px] text-text border border-border rounded-lg hover:border-accent hover:text-accent"
|
||||
>Copy to clipboard</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={saveDiagnosticReport}
|
||||
class="px-3 py-2 text-[12px] text-text border border-border rounded-lg hover:border-accent hover:text-accent"
|
||||
>Save as file</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if diagnosticReportError}
|
||||
<p class="text-[11px] text-error mb-2">{diagnosticReportError}</p>
|
||||
{/if}
|
||||
{#if diagnosticReportSavedTo}
|
||||
<p class="text-[11px] text-success mb-2">{diagnosticReportSavedTo}</p>
|
||||
{/if}
|
||||
{#if diagnosticReport}
|
||||
<details class="text-[11px]">
|
||||
<summary class="cursor-pointer text-text-secondary hover:text-text">Preview report ({diagnosticReport.length} chars)</summary>
|
||||
<pre class="mt-2 p-3 bg-bg-input border border-border rounded-lg text-[10px] text-text-secondary overflow-auto max-h-[400px] whitespace-pre-wrap font-mono">{diagnosticReport}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import TaskSidebar from "$lib/components/TaskSidebar.svelte";
|
||||
import Titlebar from "$lib/components/Titlebar.svelte";
|
||||
import ToastViewport from "$lib/components/ToastViewport.svelte";
|
||||
import { hasTauriRuntime } from "$lib/utils/runtime.js";
|
||||
import { page, settings, saveSettings } from "$lib/stores/page.svelte.js";
|
||||
import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js";
|
||||
import { hasTauriRuntime } from "$lib/utils/runtime.js";
|
||||
@@ -165,11 +166,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Capture global frontend errors and forward to the Rust error_log via
|
||||
// log_frontend_error. Best-effort: never let the error handler itself
|
||||
// throw, never crash the app over a logging failure.
|
||||
// (Diagnostics layer 1 — local only, never transmitted)
|
||||
function installGlobalErrorCapture() {
|
||||
if (!hasTauriRuntime()) return;
|
||||
|
||||
const safeLog = (context, message, stack) => {
|
||||
try {
|
||||
invoke("log_frontend_error", { context, message: String(message ?? ""), stack: stack ?? null })
|
||||
.catch(() => { /* swallow — diagnostic logging must never throw */ });
|
||||
} catch { /* same */ }
|
||||
};
|
||||
|
||||
window.addEventListener("error", (ev) => {
|
||||
safeLog(
|
||||
"window.onerror",
|
||||
ev?.message || ev?.error?.message || "Unknown error",
|
||||
ev?.error?.stack || null,
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("unhandledrejection", (ev) => {
|
||||
const reason = ev?.reason;
|
||||
const msg = (reason && (reason.message || String(reason))) || "Unhandled rejection";
|
||||
safeLog("unhandledrejection", msg, reason?.stack || null);
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Auto-collapse if window is already narrow on first load
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Diagnostics: capture every uncaught frontend error to error_log.
|
||||
installGlobalErrorCapture();
|
||||
|
||||
if (!tauriRuntimeAvailable) {
|
||||
hotkeyBackend = "unavailable";
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user