Skip to content

Commit e3c49ef

Browse files
authored
Desktop crash reporting + diagnostics export (#961)
* Add crash reporting and one-click diagnostics export to the desktop app - Sentry in the Electron main process, gated on a DSN baked in at build time (SENTRY_DSN env -> define). No DSN (local/dev/fork builds) means nothing is ever sent; native crash dumps are still written locally via crashReporter for the diagnostics export. - Sidecar stdout/stderr now persist to main.log under a 'sidecar' scope (previously terminal-only, so server crashes left no trace on disk). Unexpected sidecar exits after a successful boot are logged and reported with a bounded stderr tail. - render-process-gone / child-process-gone are logged locally and captured upstream; main-process uncaught errors land in main.log. - Export Diagnostics: app menu item + Settings-page button pack a redacted manifest (version, OS, paths, port - never passwords or ~/.executor data), recent logs, and crash dumps into a zip in Downloads and reveal it in the file manager. - publish-desktop.yml passes vars.SENTRY_DSN into the electron-vite build step. * Extend desktop crash reporting to renderer and sidecar, add Report a Problem - Renderer: the shared web bundle initializes browser error reporting only when the desktop preload bridge hands it a DSN at runtime — nothing is baked into the bundle, so executor web / self-host / cloud stay inert. Handled UI errors already route through reportError and are picked up by the global handlers. - Sidecar: env-gated init in the sidecar entry (DSN passed by the main process, desktop-with-DSN builds only). Captures uncaught exceptions and unhandled rejections in the server process. The CLI never sets the env vars, keeping executor web telemetry-free. - One runId per launch tags events from all three processes and is stamped into the diagnostics-zip manifest, so a user-sent zip and its Sentry events can be cross-referenced. - electron-log lines become Sentry breadcrumbs (file transport hook), so events arrive with recent log context. - 'Report a Problem…' menu item exports the diagnostics zip and opens a prefilled GitHub issue (version, OS, runId). Verified against a local fake ingest server: session/event envelopes received from all three SDKs (browser on boot, electron on forced sidecar SIGKILL with breadcrumbs attached, bun on forced bind failure). Compiled-sidecar smoke test passes with @sentry/bun bundled. * Honor DO_NOT_TRACK, add sidecar crash screen, prove the flow on camera - DO_NOT_TRACK=1 (or true) disables crash reporting in all three processes — checked once in the main process, which is the single source of the renderer's and sidecar's reporting config. - When the sidecar dies under a live window the app now swaps the dead web UI for an in-window crash screen (data: URL, preload bridge intact) with Restart server / Export diagnostics actions. Restart drives the existing sidecar restart IPC and reloads the console. - New e2e desktop target + scenario: launches the real Electron app in a throwaway HOME via Playwright's electron driver, SIGKILLs the sidecar, asserts the crash screen, recovers via its Restart button, and asserts the healed sidecar is a new pid. Produces session.mp4 + per-step screenshots under e2e/runs/desktop/. Run with `npm run test:desktop` in e2e/ (needs a display). * Rename the desktop crash-report DSN variable to DESKTOP_SENTRY_DSN Repo-level SENTRY_DSN was ambiguous next to cloud's own Sentry config; the variable now names its surface. * Crash screen can check for updates A recurring sidecar crash may already be fixed upstream — the crash screen gets a Check for updates button (reuses the menu update flow), and showing the screen quietly stages any available update so the install prompt appears on its own, mirroring the fatal-startup self-heal.
1 parent 7162793 commit e3c49ef

21 files changed

Lines changed: 1015 additions & 9 deletions

.github/workflows/publish-desktop.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ jobs:
107107
working-directory: apps/desktop
108108

109109
- name: Build Electron main/preload/renderer
110+
env:
111+
# Crash-report DSN baked into the main bundle (see the define in
112+
# electron.vite.config.ts). Unset (forks, local) → crash reporting
113+
# is compiled out and dumps stay local.
114+
DESKTOP_SENTRY_DSN: ${{ vars.DESKTOP_SENTRY_DSN }}
110115
run: bunx --bun electron-vite build
111116
working-directory: apps/desktop
112117

apps/desktop/electron.vite.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,18 @@ const ELECTRON_EXTERNALS = [
1414
"electron-store",
1515
"electron-updater",
1616
"electron-window-state",
17+
"@sentry/electron",
18+
"@sentry/electron/main",
1719
];
1820

1921
export default defineConfig({
2022
main: {
2123
plugins: [externalizeDepsPlugin()],
24+
define: {
25+
// Crash-report DSN baked in at build time (publish-desktop.yml).
26+
// Empty in local/dev builds → Sentry stays fully disabled.
27+
__EXECUTOR_SENTRY_DSN__: JSON.stringify(process.env.DESKTOP_SENTRY_DSN ?? ""),
28+
},
2229
build: {
2330
rollupOptions: {
2431
input: { index: "src/main/index.ts" },

apps/desktop/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"typecheck:slow": "tsc --noEmit"
2222
},
2323
"dependencies": {
24+
"@sentry/bun": "^10.57.0",
25+
"@sentry/electron": "^7.13.0",
2426
"electron-log": "^5",
2527
"electron-store": "^10",
2628
"electron-updater": "^6",
@@ -41,6 +43,7 @@
4143
"@jitl/quickjs-wasmfile-release-sync": "catalog:",
4244
"@modelcontextprotocol/sdk": "^1.29.0",
4345
"@types/node": "catalog:",
46+
"@zip.js/zip.js": "^2.8.26",
4447
"bun-types": "catalog:",
4548
"electron": "41.2.1",
4649
"electron-builder": "^26",
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* In-window screen shown when the sidecar dies under a running window.
3+
* Replaces the dead web UI (which would otherwise sit there failing every
4+
* fetch) with an explanation and a recovery path. Rendered as a data: URL
5+
* in the existing BrowserWindow, so the preload bridge stays available —
6+
* the buttons drive the same `window.executor` IPC the settings page uses.
7+
*/
8+
9+
export interface CrashScreenOptions {
10+
/** Whether a crash report was sent upstream (DSN build, not opted out). */
11+
readonly reported: boolean;
12+
}
13+
14+
export const sidecarCrashHtml = ({ reported }: CrashScreenOptions): string => `<!doctype html>
15+
<html>
16+
<head>
17+
<meta charset="utf-8" />
18+
<title>Executor</title>
19+
<style>
20+
:root { color-scheme: dark; }
21+
body {
22+
margin: 0;
23+
min-height: 100vh;
24+
display: grid;
25+
place-items: center;
26+
background: #0a0a0a;
27+
color: #fafafa;
28+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
29+
}
30+
.card { max-width: 26rem; padding: 2rem; text-align: center; }
31+
.icon { font-size: 2rem; margin-bottom: 0.75rem; }
32+
h1 { font-size: 1.15rem; font-weight: 600; margin: 0 0 0.5rem; }
33+
p { font-size: 0.875rem; color: #a1a1aa; line-height: 1.5; margin: 0 0 1.5rem; }
34+
.row { display: flex; gap: 0.6rem; justify-content: center; }
35+
button {
36+
padding: 0.55rem 1.1rem;
37+
border-radius: 6px;
38+
border: 1px solid transparent;
39+
background: #fafafa;
40+
color: #0a0a0a;
41+
font: inherit;
42+
font-size: 0.875rem;
43+
cursor: pointer;
44+
white-space: nowrap;
45+
}
46+
button.secondary { background: transparent; color: #fafafa; border-color: #3f3f46; }
47+
#status { margin-top: 1.25rem; min-height: 1.2em; font-size: 0.8rem; color: #a1a1aa; }
48+
</style>
49+
</head>
50+
<body>
51+
<main class="card">
52+
<div class="icon">&#9888;&#65039;</div>
53+
<h1>The local Executor server stopped unexpectedly</h1>
54+
<p>
55+
Your data is safe.${reported ? " A crash report was sent automatically so this can get fixed." : ""}
56+
Restart the server to keep working.
57+
</p>
58+
<div class="row">
59+
<button id="restart">Restart server</button>
60+
<button id="update" class="secondary">Check for updates</button>
61+
<button id="export" class="secondary">Export diagnostics</button>
62+
</div>
63+
<p id="status"></p>
64+
</main>
65+
<script>
66+
const status = document.getElementById("status");
67+
document.getElementById("restart").addEventListener("click", async () => {
68+
status.textContent = "Restarting\\u2026";
69+
try {
70+
// Main restarts the sidecar and reloads this window on success.
71+
await window.executor.restartServer();
72+
} catch {
73+
status.textContent = "Restart failed \\u2014 try quitting and reopening Executor.";
74+
}
75+
});
76+
document.getElementById("update").addEventListener("click", async () => {
77+
status.textContent = "Checking for updates\\u2026";
78+
try {
79+
// Outcomes surface as native dialogs (install prompt / no updates).
80+
await window.executor.checkForUpdates();
81+
status.textContent = "";
82+
} catch {
83+
status.textContent = "Update check failed \\u2014 check your network.";
84+
}
85+
});
86+
document.getElementById("export").addEventListener("click", async () => {
87+
status.textContent = "Exporting\\u2026";
88+
try {
89+
await window.executor.exportDiagnostics();
90+
status.textContent = "Diagnostics saved to Downloads.";
91+
} catch {
92+
status.textContent = "Export failed \\u2014 see the log file.";
93+
}
94+
});
95+
</script>
96+
</body>
97+
</html>`;

0 commit comments

Comments
 (0)