fix(editor): stop filing crash reports for expected lazy-chunk swaps

RichMarkdownErrorBoundary reported every caught error as a react-error-boundary
crash, including the LazyChunkLoadError sentinel that lazy-with-retry throws
after it has already exhausted its retries and its one guarded reload. That
sentinel means "the chunk hash changed under a running window" (an app update),
which is deliberate graceful degradation, not a crash.

RecoverableRenderErrorBoundary already skips reporting it (#6206); this boundary
was never updated. Crash b860def2 is exactly that path: a lazy_chunk_reload
breadcrumb ("Unexpected token ':'") fires first, then the post-reload attempt
surfaces LazyChunkLoadError and files a report.

The fallback UI is unchanged, so the pane stays usable and offers retry.
This commit is contained in:
Neil
2026-08-01 02:39:52 -07:00
parent edb5607e28
commit 7fbf3130ac
2 changed files with 119 additions and 0 deletions
@@ -0,0 +1,112 @@
// @vitest-environment happy-dom
import { Suspense, act, type ReactElement, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { lazyWithRetry } from '@/lib/lazy-with-retry'
import { RichMarkdownErrorBoundary } from './RichMarkdownErrorBoundary'
const reportCrashMock = vi.hoisted(() => vi.fn())
vi.mock('@/lib/react-error-boundary-reporting', () => ({
reportReactErrorBoundaryCrash: reportCrashMock
}))
const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
function createContainer(): { container: HTMLDivElement; root: Root } {
const container = document.createElement('div')
document.body.appendChild(container)
return { container, root: createRoot(container) }
}
// Mirrors EditorContent: the Suspense sits above the boundary, which wraps the lazy editor.
function BoundaryHarness({ children }: { children: ReactNode }): ReactElement {
return (
<Suspense fallback={<div>Loading...</div>}>
<RichMarkdownErrorBoundary fileId="file-1">{children}</RichMarkdownErrorBoundary>
</Suspense>
)
}
async function flushReactWork(): Promise<void> {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})
}
describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
let root: Root | null = null
let container: HTMLDivElement | null = null
let consoleError: ReturnType<typeof vi.spyOn>
beforeEach(() => {
reportCrashMock.mockReset()
window.sessionStorage.clear()
consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
})
afterEach(() => {
if (root) {
act(() => root?.unmount())
}
container?.remove()
root = null
container = null
window.sessionStorage.clear()
consoleError.mockRestore()
})
// Repro for crash b860def2: an app update swapped chunk hashes, lazy-with-retry
// burned its one guarded reload, then surfaced LazyChunkLoadError here.
it('renders the fallback without reporting after guarded dynamic import exhaustion', async () => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
const LazyRejectingImport = lazyWithRetry(
() => Promise.reject(new SyntaxError("Unexpected token ':'")),
{ retries: 0 }
)
;({ container, root } = createContainer())
await act(async () => {
root?.render(
<BoundaryHarness>
<LazyRejectingImport />
</BoundaryHarness>
)
})
await flushReactWork()
await flushReactWork()
expect(container?.textContent).toContain('rich markdown editor')
expect(reportCrashMock).not.toHaveBeenCalled()
})
it('still reports ordinary render errors', async () => {
const error = new Error('ordinary render failure')
function BrokenEditor(): ReactElement {
throw error
}
;({ container, root } = createContainer())
await act(async () => {
root?.render(
<BoundaryHarness>
<BrokenEditor />
</BoundaryHarness>
)
})
expect(container?.textContent).toContain('rich markdown editor')
expect(reportCrashMock).toHaveBeenCalledTimes(1)
expect(reportCrashMock).toHaveBeenCalledWith(
expect.objectContaining({
boundaryId: 'editor.rich-markdown',
surface: 'rich-markdown-editor',
error
})
)
})
})
@@ -1,4 +1,5 @@
import React from 'react'
import { isLazyChunkLoadError } from '@/lib/lazy-with-retry'
import { reportReactErrorBoundaryCrash } from '@/lib/react-error-boundary-reporting'
import { translate } from '@/i18n/i18n'
@@ -38,6 +39,12 @@ export class RichMarkdownErrorBoundary extends React.Component<Props, State> {
componentDidCatch(error: Error, info: React.ErrorInfo): void {
console.error('[RichMarkdownEditor] render crash contained by boundary', error, info)
// Why: lazy-with-retry already spent its retries and its one guarded reload, so a
// chunk-hash swap after an app update is expected degradation, not a crash. The
// fallback below still renders (matches RecoverableRenderErrorBoundary).
if (isLazyChunkLoadError(error)) {
return
}
void reportReactErrorBoundaryCrash({
boundaryId: 'editor.rich-markdown',
surface: 'rich-markdown-editor',