From c0674b1cfde1601ab8d27982baa3b6955dd8711c Mon Sep 17 00:00:00 2001 From: Rhys Sullivan Date: Thu, 11 Jun 2026 14:03:18 -0700 Subject: [PATCH 1/5] 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. --- .github/workflows/publish-desktop.yml | 5 + apps/desktop/electron.vite.config.ts | 7 + apps/desktop/package.json | 2 + apps/desktop/src/main/diagnostics.ts | 192 ++++++++++++++++++ apps/desktop/src/main/global.d.ts | 6 + apps/desktop/src/main/index.ts | 12 ++ apps/desktop/src/main/sidecar.ts | 51 ++++- apps/desktop/src/preload/index.ts | 7 + bun.lock | 134 ++++++++++++ .../plugins/desktop-settings/src/client.tsx | 64 ++++++ 10 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/main/diagnostics.ts create mode 100644 apps/desktop/src/main/global.d.ts diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index f77dce784..19dc4af89 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -107,6 +107,11 @@ jobs: working-directory: apps/desktop - name: Build Electron main/preload/renderer + env: + # Crash-report DSN baked into the main bundle (see the define in + # electron.vite.config.ts). Unset (forks, local) → crash reporting + # is compiled out and dumps stay local. + SENTRY_DSN: ${{ vars.SENTRY_DSN }} run: bunx --bun electron-vite build working-directory: apps/desktop diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 97c243c8b..c24e348ed 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -14,11 +14,18 @@ const ELECTRON_EXTERNALS = [ "electron-store", "electron-updater", "electron-window-state", + "@sentry/electron", + "@sentry/electron/main", ]; export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], + define: { + // Crash-report DSN baked in at build time (publish-desktop.yml). + // Empty in local/dev builds → Sentry stays fully disabled. + __EXECUTOR_SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN ?? ""), + }, build: { rollupOptions: { input: { index: "src/main/index.ts" }, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 425911a8d..a5a2a89d6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -21,6 +21,7 @@ "typecheck:slow": "tsc --noEmit" }, "dependencies": { + "@sentry/electron": "^7.13.0", "electron-log": "^5", "electron-store": "^10", "electron-updater": "^6", @@ -41,6 +42,7 @@ "@jitl/quickjs-wasmfile-release-sync": "catalog:", "@modelcontextprotocol/sdk": "^1.29.0", "@types/node": "catalog:", + "@zip.js/zip.js": "^2.8.26", "bun-types": "catalog:", "electron": "41.2.1", "electron-builder": "^26", diff --git a/apps/desktop/src/main/diagnostics.ts b/apps/desktop/src/main/diagnostics.ts new file mode 100644 index 000000000..26c797fc0 --- /dev/null +++ b/apps/desktop/src/main/diagnostics.ts @@ -0,0 +1,192 @@ +/** + * Crash reporting + diagnostics export for the Electron main process. + * + * Error reporting is Sentry-backed and gated entirely on a DSN being baked + * in at build time (publish-desktop.yml exports SENTRY_DSN; see the define + * in electron.vite.config.ts). Local/dev builds have no DSN, so nothing is + * ever sent — instead Electron's native crash reporter still writes + * minidumps locally so they ride along in the diagnostics zip. + * + * What reaches Sentry when enabled: + * - main-process uncaught exceptions / unhandled rejections + * - native minidumps (main, renderer, GPU) via the Crashpad integration + * - renderer/child process terminations (render-process-gone et al.) + * - sidecar crashes after a successful boot, with a stderr tail + * + * What never leaves the machine: executor data (~/.executor — data.db holds + * user secrets) and the desktop settings password. The diagnostics zip only + * packs log files, crash dumps, and a redacted manifest. + */ + +import { readdirSync, statSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { app, crashReporter, dialog, shell } from "electron"; +import log from "electron-log/main.js"; +import * as Sentry from "@sentry/electron/main"; +import { getServerSettings } from "./settings"; + +const sentryDsn = __EXECUTOR_SENTRY_DSN__; + +export const errorReportingEnabled = sentryDsn.length > 0; + +/** + * Must run before `app.whenReady()` so the Crashpad handler attaches to + * every child process Electron spawns. + */ +export const initErrorReporting = () => { + if (errorReportingEnabled) { + Sentry.init({ + dsn: sentryDsn, + release: `executor-desktop@${app.getVersion()}`, + environment: app.isPackaged ? "production" : "development", + initialScope: { + tags: { + platform: process.platform, + arch: process.arch, + }, + }, + }); + } else { + // No DSN baked in — keep native crash dumps local so a user-reported + // crash still leaves minidumps for the diagnostics zip to collect. + crashReporter.start({ uploadToServer: false, compress: true }); + } + + // Persist process-death signals to main.log regardless of Sentry — these + // are the events a "the app just disappeared" report hinges on. Sentry's + // ChildProcess integration reports them upstream; this keeps a local copy. + app.on("child-process-gone", (_event, details) => { + log.error("[crash] child process gone", details); + }); + app.on("render-process-gone", (_event, webContents, details) => { + log.error("[crash] render process gone", { url: webContents.getURL(), ...details }); + }); + + // Main-process uncaught errors: electron-log writes them to main.log and + // keeps the process alive (matching its default), Sentry (when enabled) + // captures them via its own integrations. + log.errorHandler.startCatching({ showDialog: false }); +}; + +/** + * Report a sidecar crash that happened after a successful boot. The startup + * path already surfaces its own dialog; this covers the "server died under + * a running window" case, which is otherwise invisible. + */ +export const reportSidecarCrash = (message: string, stderrTail: string) => { + // No-op when Sentry isn't initialized — captures are dropped client-side. + Sentry.captureMessage(message, { + level: "error", + extra: { stderrTail }, + }); +}; + +// --------------------------------------------------------------------------- +// Diagnostics export — one zip in ~/Downloads a user can attach to a report. +// --------------------------------------------------------------------------- + +const MAX_EXPORT_FILE_BYTES = 50 * 1024 * 1024; +const EXPORT_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000; + +interface ZipEntry { + readonly name: string; + readonly path: string; +} + +/** Recursively list files under `dir`, capped by size and age. */ +const collectFiles = (dir: string, prefix: string): ZipEntry[] => { + // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: fs probing of optional directories (crash dumps may not exist) + try { + const cutoff = Date.now() - EXPORT_MAX_AGE_MS; + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = join(dir, entry.name); + if (entry.isDirectory()) return collectFiles(full, `${prefix}/${entry.name}`); + if (!entry.isFile()) return []; + const info = statSync(full); + if (info.size > MAX_EXPORT_FILE_BYTES) return []; + if (info.mtimeMs < cutoff) return []; + return [{ name: `${prefix}/${entry.name}`, path: full }]; + }); + } catch { + return []; + } +}; + +const exportStamp = () => + new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace(/\.\d+Z$/, "Z"); + +const buildManifest = () => { + const settings = getServerSettings(); + return { + generated: new Date().toISOString(), + app: app.getName(), + version: app.getVersion(), + packaged: app.isPackaged, + platform: process.platform, + arch: process.arch, + versions: process.versions, + uptimeSeconds: Math.round(process.uptime()), + errorReportingEnabled, + paths: { + userData: app.getPath("userData"), + logs: dirname(log.transports.file.getFile().path), + crashDumps: app.getPath("crashDumps"), + }, + // Redacted on purpose: the Basic-auth password never leaves the machine. + serverSettings: { + port: settings.port, + requireAuth: settings.requireAuth, + }, + }; +}; + +/** + * Pack manifest + electron-log files + sidecar log + crash dumps into + * `~/Downloads/executor-diagnostics-.zip` and reveal it in the file + * manager. Returns the zip path. + */ +export const exportDiagnostics = async (): Promise => { + const { TextReader, Uint8ArrayReader, Uint8ArrayWriter, ZipWriter } = + await import("@zip.js/zip.js"); + const { readFile } = await import("node:fs/promises"); + + const logsDir = dirname(log.transports.file.getFile().path); + const entries: ZipEntry[] = [ + ...collectFiles(logsDir, "logs"), + ...collectFiles(app.getPath("crashDumps"), "crash-dumps"), + ]; + + const writer = new ZipWriter(new Uint8ArrayWriter()); + await writer.add("manifest.json", new TextReader(JSON.stringify(buildManifest(), null, 2))); + for (const entry of entries) { + await writer.add(entry.name, new Uint8ArrayReader(new Uint8Array(await readFile(entry.path)))); + } + const zipped = await writer.close(); + + const output = join(app.getPath("downloads"), `executor-diagnostics-${exportStamp()}.zip`); + writeFileSync(output, zipped); + log.info("[diagnostics] exported", { output, files: entries.length }); + shell.showItemInFolder(output); + return output; +}; + +/** Menu-item wrapper: surface failures in a dialog instead of dying silently. */ +export const exportDiagnosticsInteractive = async () => { + // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: user-initiated export surfaces failures in a native dialog + try { + await exportDiagnostics(); + } catch (error) { + log.error("[diagnostics] export failed", error); + // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: fs/zip failures arrive as plain Node errors and render in a native dialog + const detail = error instanceof Error ? (error.stack ?? error.message) : String(error); + await dialog.showMessageBox({ + type: "error", + title: "Diagnostics export failed", + message: "Couldn't write the diagnostics zip.", + detail: `${detail.slice(0, 1200)}\n\nLogs live at: ${dirname(log.transports.file.getFile().path)}`, + }); + } +}; diff --git a/apps/desktop/src/main/global.d.ts b/apps/desktop/src/main/global.d.ts new file mode 100644 index 000000000..49dd3f44b --- /dev/null +++ b/apps/desktop/src/main/global.d.ts @@ -0,0 +1,6 @@ +/** + * Build-time constants injected by electron-vite's `define` (see + * electron.vite.config.ts). Empty string when the env var is unset — + * which disables Sentry entirely (local/dev builds). + */ +declare const __EXECUTOR_SENTRY_DSN__: string; diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 2340668c7..70066c4a1 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -23,6 +23,7 @@ import { SidecarPortInUseError, type SidecarConnection, } from "./sidecar"; +import { exportDiagnostics, exportDiagnosticsInteractive, initErrorReporting } from "./diagnostics"; import { getServerProfiles, getServerSettings, @@ -47,6 +48,12 @@ app.setPath("userData", join(app.getPath("appData"), "Executor")); log.initialize({ preload: true }); log.transports.file.level = "info"; +// Crash reporting must attach before app.whenReady() so Crashpad covers +// every child process (renderer, GPU). Sentry-backed only when a DSN was +// baked in at build time; otherwise dumps stay local for the diagnostics +// export. +initErrorReporting(); + let mainWindow: BrowserWindow | null = null; let connection: SidecarConnection | null = null; let authHeaderUnsubscribe: (() => void) | null = null; @@ -311,6 +318,7 @@ const registerIpcHandlers = () => { setServerProfiles(value); }); ipcMain.handle("executor:server:restart", () => restartSidecarAndReload()); + ipcMain.handle("executor:diagnostics:export", () => exportDiagnostics()); ipcMain.handle("executor:shell:open-external", async (_evt, rawUrl: unknown) => { if (typeof rawUrl !== "string") return; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: untrusted renderer string, URL ctor throws on malformed input @@ -472,6 +480,10 @@ const installApplicationMenu = () => { label: "Check for Updates…", click: () => void runUpdateCheck({ alertOnFail: true }), }, + { + label: "Export Diagnostics…", + click: () => void exportDiagnosticsInteractive(), + }, { type: "separator" }, ...(isMac ? ([ diff --git a/apps/desktop/src/main/sidecar.ts b/apps/desktop/src/main/sidecar.ts index 6dc42e963..98132b34b 100644 --- a/apps/desktop/src/main/sidecar.ts +++ b/apps/desktop/src/main/sidecar.ts @@ -16,6 +16,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node import { homedir } from "node:os"; import { resolve, join } from "node:path"; import { app } from "electron"; +import log from "electron-log/main.js"; import { Option, Schema } from "effect"; import { normalizeExecutorServerConnection, @@ -23,8 +24,35 @@ import { serializeExecutorLocalServerManifest, } from "@executor-js/sdk/shared"; import { getServerSettings } from "./settings"; +import { reportSidecarCrash } from "./diagnostics"; import { SERVER_SETTINGS_USERNAME, type DesktopServerSettings } from "../shared/server-settings"; +// Sidecar output is echoed to the terminal (visible when Electron is run +// from a shell) AND persisted to main.log under the "sidecar" scope — the +// log file is what a user can actually send us after a crash. +const sidecarLog = log.scope("sidecar"); + +// Rolling stderr tail attached to crash reports. Bounded so a chatty +// sidecar can't grow it unbounded over a long session. +const STDERR_TAIL_LIMIT = 8 * 1024; + +// Children deliberately stopped via stopSidecar (quit, restart, update) — +// their exits are expected and must not be reported as crashes. +const expectedExits = new WeakSet(); + +/** Buffer chunked output into whole lines before handing them to `write`. */ +const makeLineSplitter = (write: (line: string) => void) => { + let buffer = ""; + return (text: string) => { + buffer += text; + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + if (line.trim().length > 0) write(line); + } + }; +}; + export interface SidecarConnection { readonly baseUrl: string; readonly hostname: string; @@ -271,6 +299,9 @@ export async function startSidecar(options: StartOptions = {}): Promise sidecarLog.info(line)); + const logStderrLine = makeLineSplitter((line) => sidecarLog.error(line)); + const reject = (err: Error) => { if (resolved || rejected) return; rejected = true; @@ -282,6 +313,7 @@ export async function startSidecar(options: StartOptions = {}): Promise { const text = chunk.toString("utf8"); process.stdout.write(`[executor-sidecar] ${text}`); + logStdoutLine(text); const match = text.match(/EXECUTOR_READY:(\d+)/); if (match && !resolved) { if (!child.pid) { @@ -315,12 +347,26 @@ export async function startSidecar(options: StartOptions = {}): Promise { const text = chunk.toString("utf8"); - stderrBuffer += text; + stderrBuffer = (stderrBuffer + text).slice(-STDERR_TAIL_LIMIT); process.stderr.write(`[executor-sidecar] ${text}`); + logStderrLine(text); }; const onExit = (code: number | null, signal: NodeJS.Signals | null) => { - if (resolved || rejected) return; + if (resolved) { + // Post-boot exit: expected when we stopped it ourselves (quit, + // restart, update); anything else is a sidecar crash under a live + // window — log it and report upstream with the stderr tail. + if (expectedExits.has(child)) { + sidecarLog.info(`exited (code=${code} signal=${signal})`); + return; + } + const message = `Sidecar exited unexpectedly (code=${code} signal=${signal})`; + sidecarLog.error(message); + reportSidecarCrash(message, stderrBuffer); + return; + } + if (rejected) return; // Detect bind failure — the Node listener prints either "EADDRINUSE" or // "address already in use" on stderr before exiting non-zero. if (/EADDRINUSE|address already in use/i.test(stderrBuffer)) { @@ -339,6 +385,7 @@ export async function startSidecar(options: StartOptions = {}): Promise { + expectedExits.add(child); const cleanupManifest = () => { if (!child.pid) return; const dataDir = sidecarManifestPathByPid.get(child.pid); diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index d729039ec..e1a5cd110 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -40,6 +40,13 @@ const api = { openExternal(url: string): Promise { return ipcRenderer.invoke("executor:shell:open-external", url); }, + /** + * Pack logs + crash dumps + a redacted manifest into a zip in Downloads + * and reveal it in the file manager. Returns the zip path. + */ + exportDiagnostics(): Promise { + return ipcRenderer.invoke("executor:diagnostics:export"); + }, } as const; contextBridge.exposeInMainWorld("executor", api); diff --git a/bun.lock b/bun.lock index 5667e3b5e..a267f51dc 100644 --- a/bun.lock +++ b/bun.lock @@ -124,6 +124,7 @@ "name": "@executor-js/desktop", "version": "1.5.4", "dependencies": { + "@sentry/electron": "^7.13.0", "electron-log": "^5", "electron-store": "^10", "electron-updater": "^6", @@ -144,6 +145,7 @@ "@jitl/quickjs-wasmfile-release-sync": "catalog:", "@modelcontextprotocol/sdk": "^1.29.0", "@types/node": "catalog:", + "@zip.js/zip.js": "^2.8.26", "bun-types": "catalog:", "electron": "41.2.1", "electron-builder": "^26", @@ -1573,6 +1575,8 @@ "@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="], + "@fastify/otel": ["@fastify/otel@0.18.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], @@ -1859,10 +1863,54 @@ "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w=="], + + "@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.61.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q=="], + + "@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.57.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg=="], + + "@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.31.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g=="], + + "@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.33.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA=="], + + "@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow=="], + + "@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.62.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg=="], + + "@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.60.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w=="], + + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/instrumentation": "0.214.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg=="], + + "@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.62.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ=="], + + "@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.23.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ=="], + + "@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.58.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw=="], + + "@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.62.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA=="], + + "@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.58.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q=="], + + "@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.67.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ=="], + + "@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.60.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg=="], + + "@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.60.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ=="], + + "@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.60.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/sql-common": "^0.41.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw=="], + + "@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.66.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.2", "@types/pg": "8.15.6", "@types/pg-pool": "2.0.7" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA=="], + + "@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.62.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ=="], + + "@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.33.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w=="], + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], + "@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.3", "", {}, "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], @@ -1875,6 +1923,8 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], + "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], @@ -2063,6 +2113,8 @@ "@primer/octicons": ["@primer/octicons@19.24.1", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-vgtSHq8IIf3oo/HjPGj0B7NkeatSyyw5mupMjXByPI1gY6uRZ/UQdv7uSJnSOCJYQJF3lVDUwiwp9wM71MYIgA=="], + "@prisma/instrumentation": ["@prisma/instrumentation@7.6.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -2399,6 +2451,14 @@ "@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="], + "@sentry/electron": ["@sentry/electron@7.13.0", "", { "dependencies": { "@sentry/browser": "10.50.0", "@sentry/core": "10.50.0", "@sentry/node": "10.50.0" }, "peerDependencies": { "@sentry/node-native": "10.50.0" }, "optionalPeers": ["@sentry/node-native"] }, "sha512-zW/1c9fKafCZsvhRRp9mIDH9bSvzBiIUwoN087zDDHc0vatVsqqai8nUk0bUewwYZY4Inia7r5w+kPWi8LXZzg=="], + + "@sentry/node": ["@sentry/node@10.50.0", "", { "dependencies": { "@fastify/otel": "0.18.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", "@opentelemetry/instrumentation-connect": "0.57.0", "@opentelemetry/instrumentation-dataloader": "0.31.0", "@opentelemetry/instrumentation-fs": "0.33.0", "@opentelemetry/instrumentation-generic-pool": "0.57.0", "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", "@opentelemetry/instrumentation-ioredis": "0.62.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", "@opentelemetry/instrumentation-mongodb": "0.67.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", "@opentelemetry/instrumentation-pg": "0.66.0", "@opentelemetry/instrumentation-redis": "0.62.0", "@opentelemetry/instrumentation-tedious": "0.33.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.6.0", "@sentry/core": "10.50.0", "@sentry/node-core": "10.50.0", "@sentry/opentelemetry": "10.50.0", "import-in-the-middle": "^3.0.0" } }, "sha512-TvwzFQu8MGKzMQ2/tqxcNzFA8UG2kKTB+GDmA4uOzx3+GT849YZRRSJzEXCmYhk1teVd2fbmgqyYY2nyLF5a+Q=="], + + "@sentry/node-core": ["@sentry/node-core@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0", "@sentry/opentelemetry": "10.50.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw=="], + + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw=="], + "@sentry/react": ["@sentry/react@10.48.0", "", { "dependencies": { "@sentry/browser": "10.48.0", "@sentry/core": "10.48.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-uc93vKjmu6gNns+JAX4qquuxWpAMit0uGPA1TYlMjct9NG1uX3TkDPJAr9Pgd1lOXx8mKqCmj5fK33QeExMpPw=="], "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], @@ -2539,6 +2599,8 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], @@ -2633,6 +2695,8 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], @@ -2641,6 +2705,8 @@ "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + "@types/pg-pool": ["@types/pg-pool@2.0.7", "", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="], + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], "@types/prettier": ["@types/prettier@3.0.0", "", { "dependencies": { "prettier": "*" } }, "sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA=="], @@ -2653,6 +2719,8 @@ "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -2725,12 +2793,16 @@ "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.8.26", "", {}, "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA=="], + "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -3439,6 +3511,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + "fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="], "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], @@ -3587,6 +3661,8 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-in-the-middle": ["import-in-the-middle@3.0.2", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-LGLYRl0A2gtyUJb2WDliBHmk6TtlHwdDjxonacZ8QrEs/ZW+YDgNv2QAfjRQWpS8HqvNcq6GGnN6jrOa5FysDQ=="], + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], @@ -4047,6 +4123,8 @@ "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], "motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="], @@ -4505,6 +4583,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], @@ -5139,6 +5219,8 @@ "@executor-js/example-promise-sdk/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="], + "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="], "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@11.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag=="], @@ -5183,6 +5265,8 @@ "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], @@ -5203,6 +5287,8 @@ "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], + "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], @@ -5299,6 +5385,16 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@sentry/electron/@sentry/browser": ["@sentry/browser@10.50.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.50.0", "@sentry-internal/feedback": "10.50.0", "@sentry-internal/replay": "10.50.0", "@sentry-internal/replay-canvas": "10.50.0", "@sentry/core": "10.50.0" } }, "sha512-1f6rAvET6myiTaSeYqvaaBwvq1LfxqWjAPIoAW/NVC9bPMkeEcuvgDajHrnZMrBeWoJ81NMyoLkyX+iOc7MoFA=="], + + "@sentry/electron/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + + "@sentry/node/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + + "@sentry/node-core/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + + "@sentry/opentelemetry/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], @@ -5325,16 +5421,22 @@ "@types/cacheable-request/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/connect/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/fs-extra/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/keyv/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/mysql/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/pg/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/plist/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/responselike/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/tedious/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/ws/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/yauzl/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], @@ -5471,6 +5573,8 @@ "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "ink/type-fest": ["type-fest@5.5.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="], @@ -5773,6 +5877,10 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="], + + "@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -5793,6 +5901,8 @@ "@malept/flatpak-bundler/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], @@ -5809,20 +5919,38 @@ "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + + "@sentry/electron/@sentry/browser/@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" } }, "sha512-42bxyRTxnCmYlWnvz4CxikuQNanw8UNma2WJrtxJ0f1MAJV2GhQGSHDLnA+lvFlmiz6qct3pfen/NXGyOTegTA=="], + + "@sentry/electron/@sentry/browser/@sentry-internal/feedback": ["@sentry-internal/feedback@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" } }, "sha512-0k9XZF0wn86f77mIO2U3gNNyDZooy139CnEanRzHinrN106vVzvBZ6TUEQoHtoO1fqQxr+nWWVrqV/PXUqk47w=="], + + "@sentry/electron/@sentry/browser/@sentry-internal/replay": ["@sentry-internal/replay@10.50.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.50.0", "@sentry/core": "10.50.0" } }, "sha512-51FYNfnvVLAWw1rrEWPFfwHuMRb9mkVCFGA4J9/un7SpeGBsQDziGB0Di4fsCxI7+EdSBpfLHPF0csKtCCw0oQ=="], + + "@sentry/electron/@sentry/browser/@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.50.0", "", { "dependencies": { "@sentry-internal/replay": "10.50.0", "@sentry/core": "10.50.0" } }, "sha512-jx6RKBmcJSWdI92qDGS/sBv1w+7Cww879Z/moX7bw7ipHa/Ts3iDcB3rgZwvhmi17U+mvYsbJeL2DXkPo3TjPw=="], + "@types/better-sqlite3/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "@types/connect/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "@types/fs-extra/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@types/keyv/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "@types/mysql/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "@types/pg/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@types/plist/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@types/responselike/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "@types/tedious/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "@types/ws/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@types/yauzl/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], @@ -6237,6 +6365,8 @@ "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "@libsql/kysely-libsql/@libsql/client/@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -6257,6 +6387,10 @@ "@libsql/kysely-libsql/@libsql/client/libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "agents/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "agents/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], diff --git a/packages/plugins/desktop-settings/src/client.tsx b/packages/plugins/desktop-settings/src/client.tsx index 30ff386d3..05607f145 100644 --- a/packages/plugins/desktop-settings/src/client.tsx +++ b/packages/plugins/desktop-settings/src/client.tsx @@ -51,6 +51,8 @@ interface ExecutorBridge { ) => Promise; readonly regeneratePassword: () => Promise; readonly restartServer: () => Promise; + // Optional: present in desktop builds that ship the diagnostics export. + readonly exportDiagnostics?: () => Promise; } const readBridge = (): ExecutorBridge | null => { @@ -133,6 +135,23 @@ function SettingsPage() { [bridge, restartAndRefreshConnection], ); + const [diagnostics, setDiagnostics] = useState< + { readonly state: "idle" | "exporting" } | { readonly state: "done"; readonly path: string } + >({ state: "idle" }); + + const exportDiagnostics = useCallback(async () => { + if (!bridge?.exportDiagnostics) return; + setDiagnostics({ state: "exporting" }); + // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: renderer ↔ Electron IPC, errors surface in the form + try { + const path = await bridge.exportDiagnostics(); + setDiagnostics({ state: "done", path }); + } catch { + setError("Diagnostics export failed — check the desktop log for details."); + setDiagnostics({ state: "idle" }); + } + }, [bridge]); + const regenerate = useCallback(async () => { if (!bridge) return; setStatus("saving"); @@ -346,6 +365,51 @@ function SettingsPage() { {error} )} + + {bridge.exportDiagnostics && ( +
+
Diagnostics
+ + Packs app and server logs, crash dumps, and version info into a zip in your Downloads + folder — attach it when reporting a bug. Your sources, secrets, and passwords are not + included. + +
+ {/* oxlint-disable-next-line react/forbid-elements -- plugin component uses raw HTML controls per SDK convention */} + + {diagnostics.state === "done" && ( + + {diagnostics.path} + + )} +
+
+ )} ); From 83624b016a783a3f8a1689bf5022c98834956dbc Mon Sep 17 00:00:00 2001 From: Rhys Sullivan Date: Thu, 11 Jun 2026 16:05:54 -0700 Subject: [PATCH 2/5] Extend desktop crash reporting to renderer and sidecar, add Report a Problem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- apps/desktop/package.json | 1 + apps/desktop/src/main/diagnostics.ts | 87 +++++++++++++++++++++++++++- apps/desktop/src/main/index.ts | 13 ++++- apps/desktop/src/main/sidecar.ts | 6 +- apps/desktop/src/preload/index.ts | 13 +++++ apps/desktop/src/sidecar/server.ts | 24 ++++++++ bun.lock | 44 ++++++++++---- packages/app/package.json | 1 + packages/app/src/crash-reporting.ts | 54 +++++++++++++++++ packages/app/src/entry-client.tsx | 3 + 10 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 packages/app/src/crash-reporting.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a5a2a89d6..e9dd41d18 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -21,6 +21,7 @@ "typecheck:slow": "tsc --noEmit" }, "dependencies": { + "@sentry/bun": "^10.57.0", "@sentry/electron": "^7.13.0", "electron-log": "^5", "electron-store": "^10", diff --git a/apps/desktop/src/main/diagnostics.ts b/apps/desktop/src/main/diagnostics.ts index 26c797fc0..586f6dd07 100644 --- a/apps/desktop/src/main/diagnostics.ts +++ b/apps/desktop/src/main/diagnostics.ts @@ -29,6 +29,43 @@ const sentryDsn = __EXECUTOR_SENTRY_DSN__; export const errorReportingEnabled = sentryDsn.length > 0; +/** + * One id per app launch, shared by every process (main, renderer, sidecar) + * and stamped into the diagnostics manifest — lets a user-sent zip be + * matched to its Sentry events and vice versa. + */ +export const runId = crypto.randomUUID().replace(/-/g, "").slice(0, 12); + +const releaseTag = () => `executor-desktop@${app.getVersion()}`; +const environmentTag = () => (app.isPackaged ? "production" : "development"); + +/** + * Runtime crash-reporting config for the renderer (fetched over the preload + * bridge). The web UI is the same bundle `executor web` serves, so nothing + * is baked in at build time — outside the desktop app this returns null and + * the renderer never initializes Sentry. + */ +export const getCrashReportingConfig = () => + errorReportingEnabled + ? { + dsn: sentryDsn, + release: releaseTag(), + environment: environmentTag(), + runId, + } + : null; + +/** Env vars handed to the sidecar so its process reports under the same id. */ +export const sidecarCrashReportingEnv = (): Record => + errorReportingEnabled + ? { + EXECUTOR_SENTRY_DSN: sentryDsn, + EXECUTOR_SENTRY_RELEASE: releaseTag(), + EXECUTOR_SENTRY_ENVIRONMENT: environmentTag(), + EXECUTOR_RUN_ID: runId, + } + : {}; + /** * Must run before `app.whenReady()` so the Crashpad handler attaches to * every child process Electron spawns. @@ -37,12 +74,13 @@ export const initErrorReporting = () => { if (errorReportingEnabled) { Sentry.init({ dsn: sentryDsn, - release: `executor-desktop@${app.getVersion()}`, - environment: app.isPackaged ? "production" : "development", + release: releaseTag(), + environment: environmentTag(), initialScope: { tags: { platform: process.platform, arch: process.arch, + runId, }, }, }); @@ -66,6 +104,23 @@ export const initErrorReporting = () => { // keeps the process alive (matching its default), Sentry (when enabled) // captures them via its own integrations. log.errorHandler.startCatching({ showDialog: false }); + + // Every log line becomes a Sentry breadcrumb, so an error event arrives + // with the recent log context (sidecar restarts, update checks, …) instead + // of a bare stack. Hooked on the file transport only so each line is + // recorded once. No-ops when Sentry is disabled. + log.hooks.push((message, transport) => { + if (transport !== log.transports.file) return message; + Sentry.addBreadcrumb({ + category: message.scope ?? "main", + level: message.level === "warn" ? "warning" : message.level === "error" ? "error" : "info", + message: message.data + .map((part) => (typeof part === "string" ? part : JSON.stringify(part))) + .join(" ") + .slice(0, 1024), + }); + return message; + }); }; /** @@ -123,6 +178,7 @@ const buildManifest = () => { return { generated: new Date().toISOString(), app: app.getName(), + runId, version: app.getVersion(), packaged: app.isPackaged, platform: process.platform, @@ -173,6 +229,33 @@ export const exportDiagnostics = async (): Promise => { return output; }; +/** + * "Report a Problem…" menu flow: export the diagnostics zip, then open a + * prefilled GitHub issue. The zip is revealed in the file manager so the + * user can drag it onto the issue; nothing is uploaded automatically. + */ +export const reportAProblem = async () => { + await exportDiagnosticsInteractive(); + const body = [ + "", + "", + "", + "---", + "", + "| | |", + "|---|---|", + `| Version | ${app.getVersion()} |`, + `| OS | ${process.platform} ${process.arch} |`, + `| Run ID | ${runId} |`, + "", + "_A diagnostics zip was saved to your Downloads folder — please drag it into this issue._", + ].join("\n"); + const url = new URL("https://github.com/RhysSullivan/executor/issues/new"); + url.searchParams.set("title", "[desktop] "); + url.searchParams.set("body", body); + await shell.openExternal(url.toString()); +}; + /** Menu-item wrapper: surface failures in a dialog instead of dying silently. */ export const exportDiagnosticsInteractive = async () => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: user-initiated export surfaces failures in a native dialog diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 70066c4a1..a1abf9c72 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -23,7 +23,13 @@ import { SidecarPortInUseError, type SidecarConnection, } from "./sidecar"; -import { exportDiagnostics, exportDiagnosticsInteractive, initErrorReporting } from "./diagnostics"; +import { + exportDiagnostics, + exportDiagnosticsInteractive, + getCrashReportingConfig, + initErrorReporting, + reportAProblem, +} from "./diagnostics"; import { getServerProfiles, getServerSettings, @@ -319,6 +325,7 @@ const registerIpcHandlers = () => { }); ipcMain.handle("executor:server:restart", () => restartSidecarAndReload()); ipcMain.handle("executor:diagnostics:export", () => exportDiagnostics()); + ipcMain.handle("executor:crash-reporting:get", () => getCrashReportingConfig()); ipcMain.handle("executor:shell:open-external", async (_evt, rawUrl: unknown) => { if (typeof rawUrl !== "string") return; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: untrusted renderer string, URL ctor throws on malformed input @@ -484,6 +491,10 @@ const installApplicationMenu = () => { label: "Export Diagnostics…", click: () => void exportDiagnosticsInteractive(), }, + { + label: "Report a Problem…", + click: () => void reportAProblem(), + }, { type: "separator" }, ...(isMac ? ([ diff --git a/apps/desktop/src/main/sidecar.ts b/apps/desktop/src/main/sidecar.ts index 98132b34b..0abba986a 100644 --- a/apps/desktop/src/main/sidecar.ts +++ b/apps/desktop/src/main/sidecar.ts @@ -24,7 +24,7 @@ import { serializeExecutorLocalServerManifest, } from "@executor-js/sdk/shared"; import { getServerSettings } from "./settings"; -import { reportSidecarCrash } from "./diagnostics"; +import { reportSidecarCrash, sidecarCrashReportingEnv } from "./diagnostics"; import { SERVER_SETTINGS_USERNAME, type DesktopServerSettings } from "../shared/server-settings"; // Sidecar output is echoed to the terminal (visible when Electron is run @@ -286,6 +286,10 @@ export async function startSidecar(options: StartOptions = {}): Promise { return ipcRenderer.invoke("executor:diagnostics:export"); }, + /** + * Crash-reporting config for the renderer. Null unless this desktop build + * shipped with a DSN baked in — the shared web UI only initializes its + * error reporting when this returns a config. + */ + getCrashReporting(): Promise<{ + readonly dsn: string; + readonly release: string; + readonly environment: string; + readonly runId: string; + } | null> { + return ipcRenderer.invoke("executor:crash-reporting:get"); + }, } as const; contextBridge.exposeInMainWorld("executor", api); diff --git a/apps/desktop/src/sidecar/server.ts b/apps/desktop/src/sidecar/server.ts index 392acd67b..9c5b5119a 100644 --- a/apps/desktop/src/sidecar/server.ts +++ b/apps/desktop/src/sidecar/server.ts @@ -39,6 +39,30 @@ if (typeof Bun !== "undefined" && (await Bun.file(wasmOnDisk).exists())) { setQuickJSModule(mod); } +// Crash reporting — only when the Electron main process handed us a DSN +// (desktop builds with SENTRY_DSN baked in). `executor web` and self-host +// never set these env vars, so this stays inert everywhere else. Captures +// uncaught exceptions / unhandled rejections in the server process; the +// shared runId ties events to the main process and diagnostics zip. +const sentryDsn = process.env.EXECUTOR_SENTRY_DSN; +if (sentryDsn) { + const Sentry = await import("@sentry/bun"); + Sentry.init({ + dsn: sentryDsn, + release: process.env.EXECUTOR_SENTRY_RELEASE, + environment: process.env.EXECUTOR_SENTRY_ENVIRONMENT ?? "production", + tracesSampleRate: 0, + initialScope: { + tags: { + process: "sidecar", + platform: process.platform, + arch: process.arch, + ...(process.env.EXECUTOR_RUN_ID ? { runId: process.env.EXECUTOR_RUN_ID } : {}), + }, + }, + }); +} + import { startServer } from "@executor-js/local"; const requestedPort = parseInt(process.env.EXECUTOR_PORT ?? "0", 10); diff --git a/bun.lock b/bun.lock index 9a07ea3e6..6d28bd9d6 100644 --- a/bun.lock +++ b/bun.lock @@ -123,6 +123,7 @@ "name": "@executor-js/desktop", "version": "1.5.4", "dependencies": { + "@sentry/bun": "^10.57.0", "@sentry/electron": "^7.13.0", "electron-log": "^5", "electron-store": "^10", @@ -402,6 +403,7 @@ "@executor-js/react": "workspace:*", "@executor-js/sdk": "workspace:*", "@executor-js/vite-plugin": "workspace:*", + "@sentry/browser": "^10.57.0", "@tanstack/react-router": "catalog:", "effect": "catalog:", "lucide-react": "^1.7.0", @@ -2450,27 +2452,31 @@ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.48.0", "", { "dependencies": { "@sentry/core": "10.48.0" } }, "sha512-SCiTLBXzugFKxev6NoKYBIhQoDk0gUh0AVVVepCBqfCJiWBG01Zvv0R5tCVohr4cWRllkQ8mlBdNQd/I7s9tdA=="], + "@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0" } }, "sha512-tXObp954rMTSYKlbftjVXHtNl4t/6ssks3jkqyzmKb+PDPWzabGQO7sWwqVuTjT8Kx/8A3FmriS1bGmqxiJy3A=="], - "@sentry-internal/feedback": ["@sentry-internal/feedback@10.48.0", "", { "dependencies": { "@sentry/core": "10.48.0" } }, "sha512-tGkEyOM1HDS9qebDphUMEnyk3qq/50AnuTBiFmMJyjNzowylVGmRRk0sr3xkmbVHCDXQCiYnDmSVlJ2x4SDMrQ=="], + "@sentry-internal/feedback": ["@sentry-internal/feedback@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0" } }, "sha512-ZcF4QhkqGX3iiQSXB2N0N3Awp+j5iqnDRu6PA/qyLFrWqH5ZiiAAgu59OLD9E6XAdg6iFtLYw19MAMZVK8qNOQ=="], - "@sentry-internal/replay": ["@sentry-internal/replay@10.48.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.48.0", "@sentry/core": "10.48.0" } }, "sha512-sevRTePfuk4PNuz9KAKpmTZEomAU0aLXyIhOwA0OnUDdxPhkY8kq5lwDbuxTHv6DQUjUX3YgFbY45VH1JEqHKA=="], + "@sentry-internal/replay": ["@sentry-internal/replay@10.57.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.57.0", "@sentry/core": "10.57.0" } }, "sha512-Wmnx/6ABynVH1iwuoNUqJNyjIUqsqoGML7qsyivBRKb5Wo2YQtPOQlQYfxfZSvWzGpcoSVdInkRjDssUQxQEQg=="], - "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.48.0", "", { "dependencies": { "@sentry-internal/replay": "10.48.0", "@sentry/core": "10.48.0" } }, "sha512-9nWuN2z4O+iwbTfuYV5ZmngBgJU/ZxfOo47A5RJP3Nu/kl59aJ1lUhILYOKyeNOIC/JyeERmpIcTxnlPXQzZ3Q=="], + "@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.57.0", "", { "dependencies": { "@sentry-internal/replay": "10.57.0", "@sentry/core": "10.57.0" } }, "sha512-zsfa4JcfV0AEc9YhNxNabd5lSZL2Av84saAyexGAqcHs+67m9Gd0cGStOzMb/nCl7UAtmdP0aI+G7a3rcxxN/A=="], - "@sentry/browser": ["@sentry/browser@10.48.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.48.0", "@sentry-internal/feedback": "10.48.0", "@sentry-internal/replay": "10.48.0", "@sentry-internal/replay-canvas": "10.48.0", "@sentry/core": "10.48.0" } }, "sha512-4jt2zX2ExgFcNe2x+W+/k81fmDUsOrquGtt028CiGuDuma6kEsWBI4JbooT1jhj2T+eeUxe3YGbM23Zhh7Ghhw=="], + "@sentry-internal/server-utils": ["@sentry-internal/server-utils@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0" } }, "sha512-Qu8ETmX/ITzteG7Im46b9HOxKKzeaIeqNvftaIlFURu1RUQdHbtGerS7QOmXzwnhuqNGNeiCQYkduB798IfRqA=="], + + "@sentry/browser": ["@sentry/browser@10.57.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.57.0", "@sentry-internal/feedback": "10.57.0", "@sentry-internal/replay": "10.57.0", "@sentry-internal/replay-canvas": "10.57.0", "@sentry/core": "10.57.0" } }, "sha512-s36AQy/CKXTfyY9Z+qUhzNomntZXgfs0rbaK7q9ffnFkqcPwzE8qQtVs58y3Suut56u+AhwSztgQtERcuZ5VIA=="], + + "@sentry/bun": ["@sentry/bun@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0", "@sentry/node": "10.57.0" } }, "sha512-bh62U3UiXYs0Uu37DX0vfO+JdwXXG16KvMUUHi/KfQdFO5VMuCDTdtLEMt+1fEXnjOi7DfQaaItoNPkagQc52w=="], "@sentry/cloudflare": ["@sentry/cloudflare@10.48.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/core": "10.48.0" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-i02Ps4/cJjFpbcHLMhNEFXTeVqLB9XpB3+/OFQ9aMFV3yDcxlvHwe0oo7WZf41iroArvpysotLG8Y8NBOU9omA=="], - "@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="], + "@sentry/core": ["@sentry/core@10.57.0", "", {}, "sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg=="], "@sentry/electron": ["@sentry/electron@7.13.0", "", { "dependencies": { "@sentry/browser": "10.50.0", "@sentry/core": "10.50.0", "@sentry/node": "10.50.0" }, "peerDependencies": { "@sentry/node-native": "10.50.0" }, "optionalPeers": ["@sentry/node-native"] }, "sha512-zW/1c9fKafCZsvhRRp9mIDH9bSvzBiIUwoN087zDDHc0vatVsqqai8nUk0bUewwYZY4Inia7r5w+kPWi8LXZzg=="], - "@sentry/node": ["@sentry/node@10.50.0", "", { "dependencies": { "@fastify/otel": "0.18.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", "@opentelemetry/instrumentation-connect": "0.57.0", "@opentelemetry/instrumentation-dataloader": "0.31.0", "@opentelemetry/instrumentation-fs": "0.33.0", "@opentelemetry/instrumentation-generic-pool": "0.57.0", "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", "@opentelemetry/instrumentation-ioredis": "0.62.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", "@opentelemetry/instrumentation-mongodb": "0.67.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", "@opentelemetry/instrumentation-pg": "0.66.0", "@opentelemetry/instrumentation-redis": "0.62.0", "@opentelemetry/instrumentation-tedious": "0.33.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.6.0", "@sentry/core": "10.50.0", "@sentry/node-core": "10.50.0", "@sentry/opentelemetry": "10.50.0", "import-in-the-middle": "^3.0.0" } }, "sha512-TvwzFQu8MGKzMQ2/tqxcNzFA8UG2kKTB+GDmA4uOzx3+GT849YZRRSJzEXCmYhk1teVd2fbmgqyYY2nyLF5a+Q=="], + "@sentry/node": ["@sentry/node@10.57.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@sentry-internal/server-utils": "10.57.0", "@sentry/core": "10.57.0", "@sentry/node-core": "10.57.0", "@sentry/opentelemetry": "10.57.0", "import-in-the-middle": "^3.0.0" } }, "sha512-7KEStrJ97wPf1fA5nU5ONeTTcIIlh7oT8OMffEVA1PXmlhFoXhcQZVzr4rM+zj9tfMWT01og5Ng/Grgh3dN+FA=="], - "@sentry/node-core": ["@sentry/node-core@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0", "@sentry/opentelemetry": "10.50.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw=="], + "@sentry/node-core": ["@sentry/node-core@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0", "@sentry/opentelemetry": "10.57.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-2v2IF6MfTiu7pimWEq2rYhZsmlwyNbs3bHUsrYFPeP/Rpa6ObDuUWPdVEzJjfyK+AqqYZYxZdV0l3+B13kTEmQ=="], - "@sentry/opentelemetry": ["@sentry/opentelemetry@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw=="], + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.57.0", "", { "dependencies": { "@sentry/core": "10.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-iwRz8cEK0GOISG34aJRO8GdYOk3nfpuT6dT2GDQrxw8f7JjkJKx9LPU8MaenOFa4MhY+Z02hI6NNcrbsoI3cXg=="], "@sentry/react": ["@sentry/react@10.48.0", "", { "dependencies": { "@sentry/browser": "10.48.0", "@sentry/core": "10.48.0" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-uc93vKjmu6gNns+JAX4qquuxWpAMit0uGPA1TYlMjct9NG1uX3TkDPJAr9Pgd1lOXx8mKqCmj5fK33QeExMpPw=="], @@ -5430,15 +5436,17 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@sentry/cloudflare/@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="], + "@sentry/electron/@sentry/browser": ["@sentry/browser@10.50.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.50.0", "@sentry-internal/feedback": "10.50.0", "@sentry-internal/replay": "10.50.0", "@sentry-internal/replay-canvas": "10.50.0", "@sentry/core": "10.50.0" } }, "sha512-1f6rAvET6myiTaSeYqvaaBwvq1LfxqWjAPIoAW/NVC9bPMkeEcuvgDajHrnZMrBeWoJ81NMyoLkyX+iOc7MoFA=="], "@sentry/electron/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], - "@sentry/node/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + "@sentry/electron/@sentry/node": ["@sentry/node@10.50.0", "", { "dependencies": { "@fastify/otel": "0.18.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", "@opentelemetry/instrumentation-connect": "0.57.0", "@opentelemetry/instrumentation-dataloader": "0.31.0", "@opentelemetry/instrumentation-fs": "0.33.0", "@opentelemetry/instrumentation-generic-pool": "0.57.0", "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", "@opentelemetry/instrumentation-ioredis": "0.62.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", "@opentelemetry/instrumentation-mongodb": "0.67.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", "@opentelemetry/instrumentation-pg": "0.66.0", "@opentelemetry/instrumentation-redis": "0.62.0", "@opentelemetry/instrumentation-tedious": "0.33.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.6.0", "@sentry/core": "10.50.0", "@sentry/node-core": "10.50.0", "@sentry/opentelemetry": "10.50.0", "import-in-the-middle": "^3.0.0" } }, "sha512-TvwzFQu8MGKzMQ2/tqxcNzFA8UG2kKTB+GDmA4uOzx3+GT849YZRRSJzEXCmYhk1teVd2fbmgqyYY2nyLF5a+Q=="], - "@sentry/node-core/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + "@sentry/react/@sentry/browser": ["@sentry/browser@10.48.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.48.0", "@sentry-internal/feedback": "10.48.0", "@sentry-internal/replay": "10.48.0", "@sentry-internal/replay-canvas": "10.48.0", "@sentry/core": "10.48.0" } }, "sha512-4jt2zX2ExgFcNe2x+W+/k81fmDUsOrquGtt028CiGuDuma6kEsWBI4JbooT1jhj2T+eeUxe3YGbM23Zhh7Ghhw=="], - "@sentry/opentelemetry/@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], + "@sentry/react/@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], @@ -6022,6 +6030,18 @@ "@sentry/electron/@sentry/browser/@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.50.0", "", { "dependencies": { "@sentry-internal/replay": "10.50.0", "@sentry/core": "10.50.0" } }, "sha512-jx6RKBmcJSWdI92qDGS/sBv1w+7Cww879Z/moX7bw7ipHa/Ts3iDcB3rgZwvhmi17U+mvYsbJeL2DXkPo3TjPw=="], + "@sentry/electron/@sentry/node/@sentry/node-core": ["@sentry/node-core@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0", "@sentry/opentelemetry": "10.50.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw=="], + + "@sentry/electron/@sentry/node/@sentry/opentelemetry": ["@sentry/opentelemetry@10.50.0", "", { "dependencies": { "@sentry/core": "10.50.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw=="], + + "@sentry/react/@sentry/browser/@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.48.0", "", { "dependencies": { "@sentry/core": "10.48.0" } }, "sha512-SCiTLBXzugFKxev6NoKYBIhQoDk0gUh0AVVVepCBqfCJiWBG01Zvv0R5tCVohr4cWRllkQ8mlBdNQd/I7s9tdA=="], + + "@sentry/react/@sentry/browser/@sentry-internal/feedback": ["@sentry-internal/feedback@10.48.0", "", { "dependencies": { "@sentry/core": "10.48.0" } }, "sha512-tGkEyOM1HDS9qebDphUMEnyk3qq/50AnuTBiFmMJyjNzowylVGmRRk0sr3xkmbVHCDXQCiYnDmSVlJ2x4SDMrQ=="], + + "@sentry/react/@sentry/browser/@sentry-internal/replay": ["@sentry-internal/replay@10.48.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.48.0", "@sentry/core": "10.48.0" } }, "sha512-sevRTePfuk4PNuz9KAKpmTZEomAU0aLXyIhOwA0OnUDdxPhkY8kq5lwDbuxTHv6DQUjUX3YgFbY45VH1JEqHKA=="], + + "@sentry/react/@sentry/browser/@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.48.0", "", { "dependencies": { "@sentry-internal/replay": "10.48.0", "@sentry/core": "10.48.0" } }, "sha512-9nWuN2z4O+iwbTfuYV5ZmngBgJU/ZxfOo47A5RJP3Nu/kl59aJ1lUhILYOKyeNOIC/JyeERmpIcTxnlPXQzZ3Q=="], + "@types/better-sqlite3/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], diff --git a/packages/app/package.json b/packages/app/package.json index e14e9e288..479d51e67 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -22,6 +22,7 @@ "@executor-js/react": "workspace:*", "@executor-js/sdk": "workspace:*", "@executor-js/vite-plugin": "workspace:*", + "@sentry/browser": "^10.57.0", "@tanstack/react-router": "catalog:", "effect": "catalog:", "lucide-react": "^1.7.0", diff --git a/packages/app/src/crash-reporting.ts b/packages/app/src/crash-reporting.ts new file mode 100644 index 000000000..e06146aa7 --- /dev/null +++ b/packages/app/src/crash-reporting.ts @@ -0,0 +1,54 @@ +/** + * Desktop-only renderer crash reporting. + * + * This bundle is served identically to `executor web`, self-host, and the + * desktop app, so nothing is baked in at build time. Inside the desktop app + * the preload bridge (`window.executor`) hands over a DSN at runtime — + * everywhere else the bridge is absent (or returns null in DSN-less builds) + * and Sentry is never imported, let alone initialized. + * + * Handled UI errors already flow through `globalThis.reportError` (see + * packages/react error-reporting), which Sentry's global handlers pick up + * once initialized — no reporter rewiring needed. + */ + +interface CrashReportingConfig { + readonly dsn: string; + readonly release: string; + readonly environment: string; + readonly runId: string; +} + +interface CrashReportingBridge { + readonly getCrashReporting?: () => Promise; +} + +export const initDesktopCrashReporting = (): void => { + if (typeof window === "undefined") return; + const bridge = (window as Window & { readonly executor?: CrashReportingBridge }).executor; + if (typeof bridge?.getCrashReporting !== "function") return; + const init = async () => { + // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: crash reporting must never take the app down with it + try { + const config = await bridge.getCrashReporting?.(); + if (!config?.dsn) return; + const Sentry = await import("@sentry/browser"); + Sentry.init({ + dsn: config.dsn, + release: config.release, + environment: config.environment, + sendDefaultPii: false, + tracesSampleRate: 0, + initialScope: { + tags: { + process: "renderer", + runId: config.runId, + }, + }, + }); + } catch { + // Reporting failures stay silent — there is nowhere left to report them. + } + }; + void init(); +}; diff --git a/packages/app/src/entry-client.tsx b/packages/app/src/entry-client.tsx index 21ffe64d6..437785c65 100644 --- a/packages/app/src/entry-client.tsx +++ b/packages/app/src/entry-client.tsx @@ -2,8 +2,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider } from "@tanstack/react-router"; import { getRouter } from "./router"; +import { initDesktopCrashReporting } from "./crash-reporting"; import "@executor-js/react/globals.css"; +initDesktopCrashReporting(); + const router = getRouter(); ReactDOM.createRoot(document.getElementById("root")!).render(); From ed9d2649d7bb1b221df5a9abe4e91caf70b2c431 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan Date: Thu, 11 Jun 2026 16:24:43 -0700 Subject: [PATCH 3/5] Honor DO_NOT_TRACK, add sidecar crash screen, prove the flow on camera MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- apps/desktop/src/main/crash-screen.ts | 85 ++++++++++++++++ apps/desktop/src/main/diagnostics.ts | 8 +- apps/desktop/src/main/index.ts | 12 +++ apps/desktop/src/main/sidecar.ts | 9 ++ e2e/desktop/sidecar-crash-screen.test.ts | 124 +++++++++++++++++++++++ e2e/package.json | 3 +- e2e/setup/desktop.globalsetup.ts | 21 ++++ e2e/targets/desktop.ts | 17 ++++ e2e/targets/registry.ts | 2 + e2e/vitest.config.ts | 10 ++ 10 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/main/crash-screen.ts create mode 100644 e2e/desktop/sidecar-crash-screen.test.ts create mode 100644 e2e/setup/desktop.globalsetup.ts create mode 100644 e2e/targets/desktop.ts diff --git a/apps/desktop/src/main/crash-screen.ts b/apps/desktop/src/main/crash-screen.ts new file mode 100644 index 000000000..fd64c95cd --- /dev/null +++ b/apps/desktop/src/main/crash-screen.ts @@ -0,0 +1,85 @@ +/** + * In-window screen shown when the sidecar dies under a running window. + * Replaces the dead web UI (which would otherwise sit there failing every + * fetch) with an explanation and a recovery path. Rendered as a data: URL + * in the existing BrowserWindow, so the preload bridge stays available — + * the buttons drive the same `window.executor` IPC the settings page uses. + */ + +export interface CrashScreenOptions { + /** Whether a crash report was sent upstream (DSN build, not opted out). */ + readonly reported: boolean; +} + +export const sidecarCrashHtml = ({ reported }: CrashScreenOptions): string => ` + + + + Executor + + + +
+
⚠️
+

The local Executor server stopped unexpectedly

+

+ Your data is safe.${reported ? " A crash report was sent automatically so this can get fixed." : ""} + Restart the server to keep working. +

+
+ + +
+

+
+ + +`; diff --git a/apps/desktop/src/main/diagnostics.ts b/apps/desktop/src/main/diagnostics.ts index 586f6dd07..98cdb8b13 100644 --- a/apps/desktop/src/main/diagnostics.ts +++ b/apps/desktop/src/main/diagnostics.ts @@ -27,7 +27,13 @@ import { getServerSettings } from "./settings"; const sentryDsn = __EXECUTOR_SENTRY_DSN__; -export const errorReportingEnabled = sentryDsn.length > 0; +// The informal cross-tool opt-out (consoledonottrack.com). Checked before +// any SDK initializes, and it covers all three processes because the +// renderer and sidecar both receive their config from this module. +const doNotTrack = + process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK?.toLowerCase() === "true"; + +export const errorReportingEnabled = sentryDsn.length > 0 && !doNotTrack; /** * One id per app launch, shared by every process (main, renderer, sidecar) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index a1abf9c72..7afb623b1 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -20,16 +20,19 @@ type UpdateInfo = { readonly version: string }; import { startSidecar, stopSidecar, + onUnexpectedSidecarExit, SidecarPortInUseError, type SidecarConnection, } from "./sidecar"; import { + errorReportingEnabled, exportDiagnostics, exportDiagnosticsInteractive, getCrashReportingConfig, initErrorReporting, reportAProblem, } from "./diagnostics"; +import { sidecarCrashHtml } from "./crash-screen"; import { getServerProfiles, getServerSettings, @@ -524,6 +527,15 @@ const boot = async () => { installApplicationMenu(); setupAutoUpdater(); registerIpcHandlers(); + // A sidecar that dies under a live window would leave the web UI failing + // every request with no explanation. Swap in the crash screen — its + // buttons drive the regular preload bridge (restart / export diagnostics). + onUnexpectedSidecarExit(() => { + const window = liveMainWindow(); + if (!window) return; + const html = sidecarCrashHtml({ reported: errorReportingEnabled }); + void window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); + }); connection = await startWithCurrentSettings(); if (!connection) { // Port conflicts already showed their dialog inside diff --git a/apps/desktop/src/main/sidecar.ts b/apps/desktop/src/main/sidecar.ts index 0abba986a..78ea39746 100644 --- a/apps/desktop/src/main/sidecar.ts +++ b/apps/desktop/src/main/sidecar.ts @@ -40,6 +40,14 @@ const STDERR_TAIL_LIMIT = 8 * 1024; // their exits are expected and must not be reported as crashes. const expectedExits = new WeakSet(); +// Main/index.ts subscribes to swap the dead web UI for the in-window crash +// screen. A callback (not an import) keeps this module free of window +// concerns. +let unexpectedExitListener: (() => void) | null = null; +export const onUnexpectedSidecarExit = (listener: () => void) => { + unexpectedExitListener = listener; +}; + /** Buffer chunked output into whole lines before handing them to `write`. */ const makeLineSplitter = (write: (line: string) => void) => { let buffer = ""; @@ -368,6 +376,7 @@ export async function startSidecar(options: StartOptions = {}): Promise run(runDir)); + }), +); + +const run = async (runDir: string) => { + // Throwaway HOME = fresh ~/.executor data dir, fresh electron-store + // settings, no collision with a real desktop install on this machine. + const home = mkdtempSync(join(tmpdir(), "executor-desktop-e2e-")); + const videoTmp = join(runDir, ".video-tmp"); + let stepIndex = 0; + + const app = await _electron.launch({ + executablePath: electronBinary, + args: [appDir], + cwd: appDir, + env: { ...process.env, HOME: home }, + recordVideo: { dir: videoTmp, size: { width: 1280, height: 800 } }, + timeout: 120_000, + }); + + try { + // firstWindow resolves only after the sidecar boots (the window is + // created with the server's URL) — this wait IS the boot assertion. + const page = await app.firstWindow({ timeout: 120_000 }); + const step = async (label: string, body: () => Promise) => { + await body(); + stepIndex += 1; + const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + await page.screenshot({ + path: join(runDir, `${String(stepIndex).padStart(2, "0")}-${slug}.png`), + }); + }; + + await step("app boots into the web console", async () => { + await page.getByText("Settings").first().waitFor({ timeout: 120_000 }); + }); + + let sidecarPid = 0; + await step("the local server is killed (SIGKILL)", async () => { + const manifestPath = join(home, ".executor/server-control/server.json"); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { pid: number }; + sidecarPid = manifest.pid; + expect(sidecarPid, "sidecar pid recorded in the server manifest").toBeGreaterThan(0); + process.kill(sidecarPid, "SIGKILL"); + await page.getByText("stopped unexpectedly").waitFor({ timeout: 30_000 }); + }); + + await step("crash screen offers restart and diagnostics", async () => { + await page.locator("#restart").waitFor({ timeout: 5_000 }); + await page.locator("#export").waitFor({ timeout: 5_000 }); + }); + + await step("restart server heals the app", async () => { + await page.locator("#restart").click(); + await page.getByText("Settings").first().waitFor({ timeout: 120_000 }); + }); + + const healedManifest = JSON.parse( + readFileSync(join(home, ".executor/server-control/server.json"), "utf8"), + ) as { pid: number }; + expect(healedManifest.pid, "restarted sidecar is a new process").not.toBe(sidecarPid); + } finally { + const page = app.windows()[0]; + const video = page?.video(); + await app.close().catch(() => {}); + const recordedPath = await video?.path().catch(() => undefined); + if (recordedPath && existsSync(recordedPath)) { + // mp4 plays everywhere (Safari/iOS don't do webm) — same treatment as + // the browser surface. + await promisify(execFile)("ffmpeg", [ + "-y", + "-i", + recordedPath, + "-c:v", + "libx264", + "-preset", + "veryfast", + "-crf", + "26", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + join(runDir, "session.mp4"), + ]).catch(() => {}); + } + rmSync(videoTmp, { recursive: true, force: true }); + rmSync(home, { recursive: true, force: true }); + } +}; diff --git a/e2e/package.json b/e2e/package.json index c204e9a3a..6cd994567 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -10,7 +10,8 @@ "test:watch": "vitest", "viewer:build": "bun scripts/rebuild-viewer.ts", "serve": "bun scripts/rebuild-viewer.ts && bun scripts/serve.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test:desktop": "vitest run --project desktop" }, "dependencies": { "@executor-js/api": "workspace:*", diff --git a/e2e/setup/desktop.globalsetup.ts b/e2e/setup/desktop.globalsetup.ts new file mode 100644 index 000000000..ab2d74a2d --- /dev/null +++ b/e2e/setup/desktop.globalsetup.ts @@ -0,0 +1,21 @@ +// Desktop project setup: make sure the bits the Electron app loads at +// runtime exist — the web UI bundle (served by the sidecar) and the +// electron-vite main/preload output. Always rebuilt so a run never tests +// stale code; both builds are incremental-fast. No server to boot: each +// scenario launches its own app process against a throwaway HOME. +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const repoRoot = fileURLToPath(new URL("../../", import.meta.url)); +const appsDesktop = fileURLToPath(new URL("../../apps/desktop/", import.meta.url)); + +export default function setup() { + execFileSync("bun", ["run", "--filter", "@executor-js/local", "build"], { + cwd: repoRoot, + stdio: "inherit", + }); + execFileSync("bunx", ["--bun", "electron-vite", "build"], { + cwd: appsDesktop, + stdio: "inherit", + }); +} diff --git a/e2e/targets/desktop.ts b/e2e/targets/desktop.ts new file mode 100644 index 000000000..722ca08ac --- /dev/null +++ b/e2e/targets/desktop.ts @@ -0,0 +1,17 @@ +// The Electron desktop app as a target. Unlike cloud/selfhost there is no +// long-lived instance to point a browser at — each scenario launches its own +// app process (Playwright's electron driver) against a throwaway HOME, so +// the target carries no capabilities and scenarios under e2e/desktop/ drive +// the app themselves. Identity is the local OS user; there is nothing to +// mint. +import { Effect } from "effect"; + +import type { Target } from "../src/target"; + +export const desktopTarget = (): Target => ({ + name: "desktop", + baseUrl: "", + mcpUrl: "", + capabilities: new Set(), + newIdentity: () => Effect.succeed({ label: "desktop-local-user" }), +}); diff --git a/e2e/targets/registry.ts b/e2e/targets/registry.ts index d1bfb0df4..342c48513 100644 --- a/e2e/targets/registry.ts +++ b/e2e/targets/registry.ts @@ -3,11 +3,13 @@ // vitest.config.ts + a globalsetup that boots (or attaches to) the instance. import type { Target } from "../src/target"; import { cloudTarget } from "./cloud"; +import { desktopTarget } from "./desktop"; import { selfhostTarget } from "./selfhost"; const factories: Record Target> = { cloud: cloudTarget, selfhost: selfhostTarget, + desktop: desktopTarget, }; let current: Target | undefined; diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index 47ab689c8..ae65a01f7 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -26,6 +26,16 @@ export default defineConfig({ // selfhost identities are the shared bootstrap admin for now — run files // serially until per-test invite-signup isolation lands. project("selfhost", { fileParallelism: false }), + // The Electron desktop app. Only desktop/** scenarios — the desktop + // target provides none of the standard surfaces (each scenario + // launches its own app via Playwright's electron driver), so running + // the cross-target suite here would just emit a page of skips. Needs + // a display; not part of the default `npm run test` chain. + project("desktop", { + include: ["desktop/**/*.test.ts"], + fileParallelism: false, + testTimeout: 300_000, + }), ], }, }); From 87c472ad13b2dfeb62d6d88fdffb2005eec8749d Mon Sep 17 00:00:00 2001 From: Rhys Sullivan Date: Thu, 11 Jun 2026 16:56:18 -0700 Subject: [PATCH 4/5] 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. --- .github/workflows/publish-desktop.yml | 2 +- apps/desktop/electron.vite.config.ts | 2 +- apps/desktop/src/main/diagnostics.ts | 2 +- apps/desktop/src/sidecar/server.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 19dc4af89..49ad7affc 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -111,7 +111,7 @@ jobs: # Crash-report DSN baked into the main bundle (see the define in # electron.vite.config.ts). Unset (forks, local) → crash reporting # is compiled out and dumps stay local. - SENTRY_DSN: ${{ vars.SENTRY_DSN }} + DESKTOP_SENTRY_DSN: ${{ vars.DESKTOP_SENTRY_DSN }} run: bunx --bun electron-vite build working-directory: apps/desktop diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index c24e348ed..9367117cc 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -24,7 +24,7 @@ export default defineConfig({ define: { // Crash-report DSN baked in at build time (publish-desktop.yml). // Empty in local/dev builds → Sentry stays fully disabled. - __EXECUTOR_SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN ?? ""), + __EXECUTOR_SENTRY_DSN__: JSON.stringify(process.env.DESKTOP_SENTRY_DSN ?? ""), }, build: { rollupOptions: { diff --git a/apps/desktop/src/main/diagnostics.ts b/apps/desktop/src/main/diagnostics.ts index 98cdb8b13..ea02eb2b4 100644 --- a/apps/desktop/src/main/diagnostics.ts +++ b/apps/desktop/src/main/diagnostics.ts @@ -2,7 +2,7 @@ * Crash reporting + diagnostics export for the Electron main process. * * Error reporting is Sentry-backed and gated entirely on a DSN being baked - * in at build time (publish-desktop.yml exports SENTRY_DSN; see the define + * in at build time (publish-desktop.yml exports DESKTOP_SENTRY_DSN; see the define * in electron.vite.config.ts). Local/dev builds have no DSN, so nothing is * ever sent — instead Electron's native crash reporter still writes * minidumps locally so they ride along in the diagnostics zip. diff --git a/apps/desktop/src/sidecar/server.ts b/apps/desktop/src/sidecar/server.ts index 9c5b5119a..99baac858 100644 --- a/apps/desktop/src/sidecar/server.ts +++ b/apps/desktop/src/sidecar/server.ts @@ -40,7 +40,7 @@ if (typeof Bun !== "undefined" && (await Bun.file(wasmOnDisk).exists())) { } // Crash reporting — only when the Electron main process handed us a DSN -// (desktop builds with SENTRY_DSN baked in). `executor web` and self-host +// (desktop builds with DESKTOP_SENTRY_DSN baked in). `executor web` and self-host // never set these env vars, so this stays inert everywhere else. Captures // uncaught exceptions / unhandled rejections in the server process; the // shared runId ties events to the main process and diagnostics zip. From ce1cc451670384c1ace26d5c37ef86b0c5cd3ffe Mon Sep 17 00:00:00 2001 From: Rhys Sullivan Date: Thu, 11 Jun 2026 16:59:35 -0700 Subject: [PATCH 5/5] Crash screen can check for updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/desktop/src/main/crash-screen.ts | 12 ++++++++++++ apps/desktop/src/main/index.ts | 8 ++++++++ apps/desktop/src/preload/index.ts | 8 ++++++++ e2e/desktop/sidecar-crash-screen.test.ts | 3 ++- 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/crash-screen.ts b/apps/desktop/src/main/crash-screen.ts index fd64c95cd..a208e1876 100644 --- a/apps/desktop/src/main/crash-screen.ts +++ b/apps/desktop/src/main/crash-screen.ts @@ -41,6 +41,7 @@ export const sidecarCrashHtml = ({ reported }: CrashScreenOptions): string => `< font: inherit; font-size: 0.875rem; cursor: pointer; + white-space: nowrap; } button.secondary { background: transparent; color: #fafafa; border-color: #3f3f46; } #status { margin-top: 1.25rem; min-height: 1.2em; font-size: 0.8rem; color: #a1a1aa; } @@ -56,6 +57,7 @@ export const sidecarCrashHtml = ({ reported }: CrashScreenOptions): string => `<

+

@@ -71,6 +73,16 @@ export const sidecarCrashHtml = ({ reported }: CrashScreenOptions): string => `< status.textContent = "Restart failed \\u2014 try quitting and reopening Executor."; } }); + document.getElementById("update").addEventListener("click", async () => { + status.textContent = "Checking for updates\\u2026"; + try { + // Outcomes surface as native dialogs (install prompt / no updates). + await window.executor.checkForUpdates(); + status.textContent = ""; + } catch { + status.textContent = "Update check failed \\u2014 check your network."; + } + }); document.getElementById("export").addEventListener("click", async () => { status.textContent = "Exporting\\u2026"; try { diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 7afb623b1..9c91efa01 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -329,6 +329,10 @@ const registerIpcHandlers = () => { ipcMain.handle("executor:server:restart", () => restartSidecarAndReload()); ipcMain.handle("executor:diagnostics:export", () => exportDiagnostics()); ipcMain.handle("executor:crash-reporting:get", () => getCrashReportingConfig()); + // Crash-screen escape hatch: a recurring sidecar crash may already be + // fixed upstream. Reuses the menu flow — staged updates prompt to install, + // "no updates" / failures surface in their own dialogs. + ipcMain.handle("executor:updates:check", () => runUpdateCheck({ alertOnFail: true })); ipcMain.handle("executor:shell:open-external", async (_evt, rawUrl: unknown) => { if (typeof rawUrl !== "string") return; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: untrusted renderer string, URL ctor throws on malformed input @@ -535,6 +539,10 @@ const boot = async () => { if (!window) return; const html = sidecarCrashHtml({ reported: errorReportingEnabled }); void window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); + // A crashing sidecar may be a broken release — quietly stage any + // available update so the install prompt appears on its own (same + // self-heal as the fatal startup path). + void runUpdateCheck({ alertOnFail: false }); }); connection = await startWithCurrentSettings(); if (!connection) { diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index cbad3f85d..85ccc551c 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -47,6 +47,14 @@ const api = { exportDiagnostics(): Promise { return ipcRenderer.invoke("executor:diagnostics:export"); }, + /** + * Run an interactive update check (menu-flow semantics: native dialogs + * for "update ready", "no updates", and failures). Used by the crash + * screen so a broken release can heal itself. + */ + checkForUpdates(): Promise { + return ipcRenderer.invoke("executor:updates:check"); + }, /** * Crash-reporting config for the renderer. Null unless this desktop build * shipped with a DSN baked in — the shared web UI only initializes its diff --git a/e2e/desktop/sidecar-crash-screen.test.ts b/e2e/desktop/sidecar-crash-screen.test.ts index 4991953f7..66433064c 100644 --- a/e2e/desktop/sidecar-crash-screen.test.ts +++ b/e2e/desktop/sidecar-crash-screen.test.ts @@ -79,8 +79,9 @@ const run = async (runDir: string) => { await page.getByText("stopped unexpectedly").waitFor({ timeout: 30_000 }); }); - await step("crash screen offers restart and diagnostics", async () => { + await step("crash screen offers restart, update, and diagnostics", async () => { await page.locator("#restart").waitFor({ timeout: 5_000 }); + await page.locator("#update").waitFor({ timeout: 5_000 }); await page.locator("#export").waitFor({ timeout: 5_000 }); });