From b8e597bc20284fb5a0bf736caab59262c0db86e0 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 1 Aug 2026 04:40:57 -0700 Subject: [PATCH] fix(crash-reporting): scope auto-recovery resolution to renderer process crashes React error-boundary reports share source 'renderer' but are produced by the renderer itself, not by a dead process, so a recovery reload must not dismiss them. Co-authored-by: Orca --- .../crash-reporting/crash-report-store.ts | 9 ++++- ...eporting-recovered-renderer-prompt.test.ts | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/main/crash-reporting/crash-report-store.ts b/src/main/crash-reporting/crash-report-store.ts index 99280007b79..af4050c185b 100644 --- a/src/main/crash-reporting/crash-report-store.ts +++ b/src/main/crash-reporting/crash-report-store.ts @@ -136,7 +136,14 @@ export class CrashReportStore { return this.withWrite(async (reports) => { const resolved: CrashReportRecord[] = [] const nextReports = reports.map((report) => { - if (report.status !== 'pending' || report.source !== 'renderer') { + // Why: `source: 'renderer'` also covers React error-boundary reports + // (processType 'react-render'), which the recovery reload did not cause + // and must keep prompting on their own evidence. + if ( + report.status !== 'pending' || + report.source !== 'renderer' || + report.processType !== 'renderer' + ) { return report } const createdAtMs = Date.parse(report.createdAt) diff --git a/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts b/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts index 7d60d1d6fc7..e6aae3b26da 100644 --- a/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts +++ b/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts @@ -90,6 +90,18 @@ function killedRendererCrash(exitCode = 9): CrashReportCreateInput { } } +// Same `source`, different producer: reported by the renderer's own error +// boundary, not by a dead renderer process. +function reactErrorBoundaryCrash(): CrashReportCreateInput { + return { + ...killedRendererCrash(), + processType: 'react-render', + reason: 'react-error-boundary', + exitCode: null, + details: { processType: 'react-render' } + } +} + function emitRendererBootstrapRendered(): void { listeners.get('crashReports:recordBreadcrumb')?.(null, { name: 'renderer_bootstrap_rendered' }) } @@ -156,6 +168,30 @@ describe('auto-recovered renderer crash reporting', () => { expect(sendable?.breadcrumbs).toEqual(recorded.breadcrumbs) }) + it('still prompts for a React error-boundary crash caught in the recovery window', async () => { + const store = await createStore() + registerCrashReportingHandlers(store) + const recorded = await store.record(reactErrorBoundaryCrash()) + + noteRendererRecoveryReloadIssued() + emitRendererBootstrapRendered() + + await expect(getLatestPending()).resolves.toMatchObject({ id: recorded.id, status: 'pending' }) + expect((await getLatestReport())?.details.rendererAutoRecovered).toBeUndefined() + }) + + it('resolves the process crash without sweeping a React error-boundary crash beside it', async () => { + const store = await createStore() + registerCrashReportingHandlers(store) + const boundary = await store.record(reactErrorBoundaryCrash()) + await store.record(killedRendererCrash()) + + noteRendererRecoveryReloadIssued() + emitRendererBootstrapRendered() + + await expect(getLatestPending()).resolves.toMatchObject({ id: boundary.id, status: 'pending' }) + }) + it('still prompts when the renderer booted without an auto-recovery reload', async () => { const store = await createStore() registerCrashReportingHandlers(store)