fix(runner): promote React 18 boundary logs so they carry a component stack (DEV-2875) - #335
Merged
Merged
Conversation
… stack (DEV-2875) Sentry DEMOS-4P arrives stackless and its issues shard per component. React 18 passes ONE joined string to console.error -- react-dom@18.3.1 cjs/react-dom.development.js:18689-18704 builds componentNameMessage + "\n" + componentStack + "\n\n" + errorBoundaryMessage -- so the component stack lives inside the message. `send` truncates messages at MONITOR_MESSAGE_MAX (500), which is why recognition has to happen in-page: parent-side, the stack would already be gone. `reactBoundaryReport` runs after `errorArgReport`, so React 19 keeps winning (it passes the Error as an argument and is already re-homed). Promotion requires four things, and the fourth is the safety property: one string argument, React's prefix at index 0, "error boundary" in the tail, and at least one `at `-form frame. No frames means no promotion, so React prose alone stays on the console channel. `at `-form only -- `sentry.ts` synthesises `DemoError: <msg>\n<stack>` for Sentry's Chrome parser, and the legacy `in X (at file:line)` form parses to zero frames, which is worse than staying put. The real defect is not the duplication. Today the companion's fingerprint contains the component stack, so it mints a new issue per component, file and line; the constant message makes it one permanent bucket. The pair itself cannot be collapsed client-side -- React logs the companion after the browser already saw the error, and it carries no error message to correlate on. The trace id is the pivot, and it already works. Two events per fault, never three: the branch is exclusive. The component name is elided to <component> in-page rather than in `normalizeMonitorMessage`, which feeds only the fingerprint -- a rule there would fix grouping and leave the title flapping per visitor-chosen component name, which is the DEV-2854 defect. Two corrections to the plan, both caught by the tests: - It claimed the stack is "capped at MONITOR_STACK_MAX". `truncate` appends a 3-char ellipsis, so the real bound is STACK_MAX + 3 (2003). Asserted exactly, because a loose `< 3000` would not notice the cap being removed. - Its stack-cap test wanted the stack to both exceed the cap and retain the last frame. Those cannot both hold -- truncation drops around frame 36 of 40. The test now asserts the bound and the truncation marker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sentry DEMOS-4P —
The above error occurred in the <ExampleComponent> component:arrives stackless, and its issues shard per component. Deferred by DEV-2858 becausemonitor.tswas owned by DEV-2853 at the time; that merged, so it is unblocked.Diagnostic-quality, not user-facing. Nothing a visitor experiences changes. The value is that errors in previewed demos currently arrive without a stack, which makes them untriageable.
Three premise corrections up front
The original ticket got the mechanism wrong three ways, and each one changed the design:
1. React 18 passes ONE joined string, not a two-part call.
react-dom@18.3.1/cjs/react-dom.development.js:18689-18704buildscomponentNameMessage + "\n" + componentStack + "\n\n" + errorBoundaryMessageand passes it as a single argument. DEMOS-4P's event body confirms it — the relayed message is the whole block, component stack included, and Sentry shows only its first line as the title.That is why recognition must be in-page: the stack lives inside the message, which
sendtruncates atMONITOR_MESSAGE_MAX(500). A parent-side rule would only ever see the truncated remains.2. "No sibling exception exists and none ever will" was false, and I had stated it as structural fact — including in a Sentry comment, now corrected. DEMOS-77 is
kind: errorwith a full JS stack, sharing DEMOS-76's trace. React 18 dev surfaces the throw towindow.onerrorvia the guarded-invoke fake-event trick, which is exactly why React's own wording is "The above error occurred".3. React 19 needs no fix. It calls
console.error("%o\n\n%s\n\n%s\n", error, …), so the Error is an argument anderrorArgReportalready re-homes it. Scope is Tier-1 docs examples pinned toreact-dom: '18.x'. Thereact 19.2.7context tag on these events is the authoring app's React, set parent-side.The fix
reactBoundaryReportin the injected reporter, placed aftererrorArgReportso React 19 keeps winning. Four required conditions, and the fourth is the safety property:%oform is excluded by arity alonethe <NAME> component:andone of your React components:"error boundary"present in the tailat-form frameCondition 4 means promotion is conditional on carrying the component stack that justifies it, so React prose with no frames stays exactly where it is today.
at-form only, deliberately:sentry.tssynthesisesDemoError: <msg>\n<stack>for Sentry's Chrome parser, and the legacyin X (at file:line)form parses to zero frames — worse than staying on the console channel.Promoted as
kind: "error"with no newMonitorKind. That is the only lever reachingcaptureException, which is the sole path by which a relayed stack reaches Sentry's parser at all —captureMessagedrops it.The component name is elided to
<component>in-page, not innormalizeMonitorMessage. That function feeds only the fingerprint, so a rule there would fix grouping while leaving the title flapping per visitor-chosen component name — precisely the DEV-2854 defect. In-page fixes both with zero parent-side edits. The name is not lost: it is the first component-stack frame.What actually improves
The duplication is not the defect. Today the companion's fingerprint is
[demo-runtime, console-error, normalizeMonitorMessage(whole block)]— and that block contains the component stack, so it mints a new issue per component, file and line. After this, it is one permanent bucket.Per-fault event count is unchanged at two (the real error plus the companion), never three — the branch is exclusive, so one
console.erroryields one relay. And the ticket's hope that "the pairing should collapse" cannot be satisfied client-side: React logs the companion after the browser saw the error, and it carries no error message to correlate on. Suppressing it when an error was relayed recently would delete the component stack exactly where it is most useful. The trace id is the pivot, and it already works.Relay budget is never worse, sometimes better.
console-erroranderrorshare one counter, so promotion spends the same slot it spends today. On repeats it is cheaper: today's dedupe key contains the up-to-500-char block, so a boundary looping over different components mints a new key each time; the constant message bounds that to component count.Two corrections the tests caught
MONITOR_STACK_MAX".truncateappends a 3-char ellipsis, so the real bound isSTACK_MAX + 3(2003). Asserted exactly — a loose< 3000would not notice the cap being removed, which is the thing worth pinning.Verification
pipeline/monitor-inject.test.mjs— 75/75 (66 pre-existing + 9 new)pnpm test—1148 tests / 1146 pass / 0 fail / 2 todo(todos pre-existing)pnpm typecheck— 0 errorsREPORTER_MODULE_LINEmeasured at 14.9 KB, matching the updated comments inmonitor.tsandsandpack.tsRevert-check, rebuilding between every run — that spec imports from
packages/runtime/dist/, so a targeted run against a staledistpasses while exercising the old reporter, and the check would prove nothing. Removing thereactBoundaryReportcall: 71 pass / 4 fail, and the four are exactly the fix-provers (promotion, stack-under-cap, host redaction, one-fault-twice). Guards stayed green. Restored and rebuilt: 75/75.The four guards pass either way and are labelled as guards in-file: an ordinary
console.erroris untouched; React prose with no frames is not promoted; React 19's Error-argument form still wins viaerrorArgReport; and a second argument declines promotion. The reporter also still satisfies the pre-existingacornES5 parse,</scriptand one-physical-line assertions.Every other PR in this batch went through an independent review agent. That budget is exhausted for the session, and the coding agent for this task died mid-run on the same limit — I finished the tests by hand. So the usual second pair of eyes is missing here, and Bugbot is the only independent gate on it. Given that Bugbot caught a test of mine earlier today that passed for the wrong reason, I would treat its verdict as load-bearing rather than a formality, and I am happy to run a proper review pass once the limit resets.
After deploy
The new fingerprint opens a new issue, so resolve DEMOS-4P as fixed and record the successor bucket. DEMOS-76 stays a duplicate of DEMOS-77 and will not recur in that sharded form.
Two things stated out loud because no test can catch them: this population moves from
warningtoerrorlevel, so it will fire once against any level-keyed alert rule; and if the docs pin ever moves toreact-dom: 19.x, this rule goes dead silently (harmless — 19 needs no fix — but nothing would fail).🤖 Generated with Claude Code
Note
Medium Risk
Changes demo-runtime telemetry classification and Sentry grouping for a narrow React 18 console shape; mis-detection could re-home or mis-fingerprint errors, but guards limit scope and there is no user-facing or auth impact.
Overview
Promotes React 18 dev error-boundary
console.errorlogs fromconsole-errortoerrorso Sentry gets a real component stack instead of a truncated one-line message (DEV-2875 / DEMOS-4P).The injected preview reporter gains
reactBoundaryReport, run only aftererrorArgReportdeclines: it matches React 18’s single-string boundary log (prefix,"error boundary", and at least oneatframe), splits frames intostack, and replaces the visitor component name with<component>for stable issue titles and fingerprints (DEV-2854). Ordinaryconsole.errorcalls and React 19’s Error-argument form are unchanged.sandpack.ts/monitor.tscomments are updated for the larger one-line reporter (~14.9 KB). Nine pipeline tests cover promotion, caps, redaction, dedupe, and guard rails.Reviewed by Cursor Bugbot for commit 87ed830. Bugbot is set up for automated code reviews on this repo. Configure here.