fix(crash-reporting): keep error-boundary reports out of the sibling sweep

Relation matching ignores processType, so a react-render report was swept the
moment it shared reason and exitCode with the anchor. Unreachable today only
because the boundary reason literal never collides with an Electron exit
reason — an invariant held 200 lines away with nothing pinning it.

Restates the anchor pass's own exclusion rather than reusing
isReactErrorBoundaryReport(), which tests that same reason literal. Both
regression tests now use a crash-shaped reason/exitCode so they fail without
the guard instead of passing on the mismatch.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-01 05:09:25 -07:00
co-authored by Orca
parent 77705fba8c
commit d85576e50b
3 changed files with 30 additions and 2 deletions
@@ -110,6 +110,21 @@ describe('CrashReportStore', () => {
await expect(store.getLatestPending()).resolves.toBeNull()
})
it('never sweeps a React error-boundary report, even when it matches the crash event', async () => {
const { store } = await createStore()
// Same reason/exitCode as the process crash: the guard must not depend on
// the boundary reason literal staying distinct from Electron's exit reasons.
const boundary = await store.record({ ...input(), processType: 'react-render' })
await store.record(input())
await store.markRendererCrashesAutoRecovered(Date.parse(boundary.createdAt) - 1_000)
await expect(store.getLatestPending()).resolves.toMatchObject({
id: boundary.id,
status: 'pending'
})
})
it('dismisses sibling pending records after one crash report is sent', async () => {
const { store } = await createStore()
await store.record(input())
@@ -166,7 +166,13 @@ export class CrashReportStore {
// healed crash still reaches the user, just wearing the sibling's report.
const anchors = [...resolved]
const sweptReports = nextReports.map((report) => {
if (report.status !== 'pending') {
// Why: relation matching ignores processType, so re-state the anchor
// pass's exclusion — a renderer-sourced report that is not the renderer
// process is the error boundary reporting itself, not a burst sibling.
if (
report.status !== 'pending' ||
(report.source === 'renderer' && report.processType !== 'renderer')
) {
return report
}
if (!anchors.some((anchor) => isRelatedCrashEvent(anchor, report))) {
@@ -199,7 +199,14 @@ describe('auto-recovered renderer crash reporting', () => {
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())
// Why the crash-shaped reason/exitCode: relation matching ignores
// processType, so a boundary report only survives the sweep on its own
// guard — not on the reason literal happening not to collide.
const boundary = await store.record({
...reactErrorBoundaryCrash(),
reason: 'killed',
exitCode: 9
})
await store.record(killedRendererCrash())
noteRendererRecoveryReloadIssued()