|
| 1 | +# Crash reporter |
| 2 | + |
| 3 | +Lightweight, privacy-respecting crash reporting. Captures crash data locally, then offers to send it on next launch. |
| 4 | + |
| 5 | +## Architecture |
| 6 | + |
| 7 | +Two capture paths handle different crash types: |
| 8 | + |
| 9 | +- **Panic hook** (`mod.rs`): Catches Rust `panic!`/`unwrap()`/`expect()` failures. Runs in normal Rust code, so it has |
| 10 | + full stdlib access. Captures `std::backtrace::Backtrace`, sanitized panic message, thread info, and app metadata. |
| 11 | + Writes a JSON crash file to the app data dir. |
| 12 | +- **Signal handler** (`mod.rs`): Catches SIGSEGV, SIGBUS, SIGABRT (like stack overflows). Runs in an async-signal-safe |
| 13 | + context, so it can only capture raw instruction pointer addresses and write them as raw bytes to a pre-opened fd. |
| 14 | + Symbolication happens on next launch. |
| 15 | + |
| 16 | +Both paths write to `crash-report.json` in the app data dir (same dir as `settings.json`, resolved by |
| 17 | +`resolved_app_data_dir()`). |
| 18 | + |
| 19 | +## Crash file lifecycle |
| 20 | + |
| 21 | +1. App crashes, handler writes `crash-report.json` |
| 22 | +2. Next launch: `check_pending_crash_report` finds the file, parses it (defensively, discards if corrupt) |
| 23 | +3. If `updates.crashReports` is `true`: auto-send and show a toast |
| 24 | +4. Otherwise: show a dialog letting the user inspect and choose to send or dismiss |
| 25 | +5. File is deleted after send or dismiss |
| 26 | + |
| 27 | +## Key design decisions |
| 28 | + |
| 29 | +- **Opt-in only** (`updates.crashReports` defaults to `false`). Consistent with the "no telemetry" stance. |
| 30 | +- **No PII, ever.** Panic messages are sanitized to strip file paths before writing. No file names, usernames, device |
| 31 | + IDs, or license keys are included. |
| 32 | +- **Dev mode: capture only, never send.** Crash files are written (useful for testing), but the send path is skipped to |
| 33 | + avoid polluting production data. |
| 34 | +- **Crash loop protection**: If the crash file's timestamp is less than five seconds before the current launch, skip |
| 35 | + auto-send and show the dialog instead. |
| 36 | +- **Radical transparency**: The dialog shows the exact JSON payload before sending. |
| 37 | + |
| 38 | +## What we send |
| 39 | + |
| 40 | +- Full symbolicated backtrace (function names + offsets, not file paths) |
| 41 | +- Exception type + signal, faulting address |
| 42 | +- App version, macOS version, CPU architecture |
| 43 | +- App uptime, thread count |
| 44 | +- Sanitized panic message |
| 45 | +- Active feature flags (booleans/enums only: `indexing.enabled`, `ai.provider`, `developer.mcpEnabled`, |
| 46 | + `developer.verboseLogging`) |
| 47 | + |
| 48 | +## What we never send |
| 49 | + |
| 50 | +- File paths, volume names, environment variables, window titles |
| 51 | +- License key, transaction ID, device ID |
| 52 | +- Register dump, heap contents |
| 53 | + |
| 54 | +## Files |
| 55 | + |
| 56 | +| File | Purpose | |
| 57 | +| ------------------ | -------------------------------------------------------------------- | |
| 58 | +| `mod.rs` | Panic hook, signal handler registration, crash file read/write | |
| 59 | +| `symbolicate.rs` | Next-launch symbolication of raw addresses from signal handler | |
| 60 | +| `tests.rs` | Unit + integration tests for crash file I/O, sanitization, signals | |
| 61 | + |
| 62 | +### Commands and frontend (milestone 2) |
| 63 | + |
| 64 | +| File | Purpose | |
| 65 | +| -------------------------------------------------- | ---------------------------------------------------------------- | |
| 66 | +| `commands/crash_reporter.rs` | Tauri commands: check, dismiss, send crash reports | |
| 67 | +| `src/lib/tauri-commands/crash-reporter.ts` | TypeScript wrappers for the three Tauri commands | |
| 68 | +| `src/lib/crash-reporter/CrashReportDialog.svelte` | Dialog: shows report, expandable details, send/dismiss buttons | |
| 69 | +| `src/lib/crash-reporter/CrashReportToastContent.svelte` | Toast content for auto-sent crash reports | |
| 70 | + |
| 71 | +The startup flow in `(main)/+layout.svelte` calls `checkPendingCrashReport` after settings are loaded. If |
| 72 | +`updates.crashReports` is true and it's not a crash loop, it auto-sends and shows a toast. Otherwise, it shows the |
| 73 | +dialog. |
| 74 | + |
| 75 | +## Gotchas |
| 76 | + |
| 77 | +- `unwrap()` on `io::Error` embeds the file path in the panic message. The sanitizer must strip path-like patterns |
| 78 | + (`/Users/...`, `C:\...`, home dir prefixes) before writing. |
| 79 | +- The signal handler's pre-opened fd becomes stale if the app data dir is deleted while the app runs. This is acceptable |
| 80 | + since it requires deliberate user action and only loses one crash report. |
| 81 | +- Raw addresses from the signal handler are only useful for symbolication if the app version hasn't changed between crash |
| 82 | + and relaunch. If versions differ, send raw addresses only (still useful for grouping). |
| 83 | +- Signal handler raw addresses are absolute virtual addresses, randomized by ASLR on each launch. True symbolication |
| 84 | + would require storing the binary's image base address in the crash file, which isn't done yet. For now, raw addresses |
| 85 | + are formatted as hex and are useful for grouping identical crash sites across reports. |
| 86 | +- The signal handler uses `execinfo.h`'s `backtrace()` which is async-signal-safe on macOS. On Linux, glibc's |
| 87 | + implementation is also safe in practice but not guaranteed by POSIX. |
0 commit comments