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 <help@stably.ai>
This commit is contained in:
Neil
2026-08-01 04:40:57 -07:00
co-authored by Orca
parent e928bed289
commit b8e597bc20
2 changed files with 44 additions and 1 deletions
@@ -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)
@@ -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)