Repository navigation
Conversation
gaearon
force-pushed
the
gaearon-debuginfo-tweaks
branch
23 times, most recently
from
July 26, 2026 13:53
965ee29 to
5e11c1d
Compare
gaearon
force-pushed
the
gaearon-debuginfo-tweaks
branch
3 times, most recently
from
July 26, 2026 14:12
9f7170e to
53d0486
Compare
gaearon
force-pushed
the
gaearon-debuginfo-tweaks
branch
5 times, most recently
from
July 26, 2026 17:12
82badec to
6aadee2
Compare
gaearon
marked this pull request as ready for review
July 26, 2026 17:44
gaearon
requested review from
eps1lon and
unstubbable
and removed request for
eps1lon
July 26, 2026 17:44
This was referenced Jul 26, 2026
The await stack parse skipped a fixed 5 frames of async_hooks dispatch, and 3 for non-Promise resources. Those counts are wrong for some resource types: I/O started from an fs callback was attributed one frame too deep, naming its io entries after whatever frame sat there. They are also artifacts of Node's dispatch internals rather than a contract: if anything else in the process registers v8.promiseHooks, Node dispatches through node:internal/promise_hooks as well and every captured stack grows a frame. Instead, skip our own callback frame and then the leading frames that belong to node:internal/async_hooks and node:internal/promise_hooks. The skip happens inside prepareStackTrace before the frames are collected, so the dispatch frames are still never materialized; the only added cost is reading the script name of the skipped frames. Only those modules are skipped, and only at the top of the stack: an await can legitimately sit inside other node internals and those frames are what classifies it as not being in user space, and node:internal/async_hooks itself shows up mid-stack legitimately through AsyncResource.runInAsyncScope. The new test pins the fs naming. The fuzzer checks every io entry's name and top stack frame against the leaf's recorded kind; with the fixed counts that check fails on 5 of the 20 seeds. Co-authored-by: Claude Fable 5 <[email protected]>
…ed rejections In DEV with enableAsyncDebugInfo, ReactPromise.prototype.then wraps every subscription in a native Promise so that awaits on chunks get picked up by the async tracking, and the wrapper unconditionally registers a rejection listener. A subscriber that didn't pass a rejection callback (chunk.then(fn)) then produces a derived native promise that rejects with no handler and no way to attach one, since our .then() doesn't return a chained promise. Under Node's default --unhandled-rejections=throw this kills the process. In prod the same subscription is a no-op. Our own ReadableStream and async iterable paths subscribe without rejection callbacks, and so can anything holding a chunk. The usual way to reach an errored chunk in an otherwise healthy render is the debug info of a rejection that a Server Component caught itself: the render succeeded, but inspecting the awaited value with a fulfillment-only subscription kills the process. Subscriptions must all defer through the wrapper so they fire in registration order regardless of whether they passed a rejection callback (the stream paths mix both on the same chunk), so the fix is to swallow the wrapper's rejection when the subscriber didn't pass a rejection callback, matching prod. The fuzzer now inspects every awaited debug value with a fulfillment-only subscription; without the fix that crashes the test process on any seed with a caught rejection. The new test reads the debug info of a caught rejection back through getDebugInfo, whose bare then() is such a subscription, which also reverts the earlier workaround there. Co-authored-by: Claude Fable 5 <[email protected]>
gaearon
force-pushed
the
gaearon-debuginfo-tweaks
branch
2 times, most recently
from
July 27, 2026 03:33
8a6a949 to
72aba02
Compare
A prop whose serialization into the debug channel makes JSON.stringify throw makes the server degrade the component's outlined debug info to a placeholder string. JSON.stringify probes toJSON on every raw object before the replacer runs. Our own ClientReference proxies exempt that probe specifically, but userland reference proxies of the same shape throw on it, and so does a throwing toJSON getter. If the component also awaits I/O, the client then initializes an awaited debug entry whose owner is that string, throws while writing to it, and rejected the still-pending data chunk. When the chunk's real model row arrived, resolveModelChunk assumed a second row must be a stream chunk and crashed the whole stream with TypeError: controller.enqueueModel is not a function. Debug info must never change the data it describes: - A failure to initialize a debug chunk drops the entry instead of rejecting the data chunk it belongs to. The entry's reserved slot is filled with a minimal component info, like errors are represented in debug info elsewhere, so the array doesn't end up with a hole that index-based consumers would trip over. - initializeFakeStack tolerates an owner that was degraded to a string, and initializeIOInfo tolerates a whole io model that was. The io info path runs synchronously inside row processing with no error boundary, so before this a degraded io model took down the entire stream. - Fizz's server component stack walk tolerates holes in the debug info array. A slot stays a hole until its debug entry arrives (e.g. over a slow debug channel), so an SSR pass rendering the object mid-stream can observe one. Co-authored-by: Claude Fable 5 <[email protected]>
When JSON.stringify of an outlined debug model threw, the whole model was replaced with a placeholder string. The row's type changed out from under everything that referenced it: rows that named the component as an owner now pointed at a string, references into the row's properties pointed into JSON that was never written since the references are registered while the failed pass runs, and the client threw away the rest of that component's debug info trying to initialize them. JSON.stringify reads every property of the objects the replacer returns and probes each object property's toJSON before the replacer can substitute it, so a value whose property access throws unwound the entire stringify before the replacer's per-property error handling could run. Our own ClientReference proxies exempt the toJSON probe specifically, but userland reference proxies of the same shape throw on it. The debug path already reads the original values to bypass toJSON (#34759); the probe was the remaining way for a user value to take down the whole model. Run the same probes over each object the replacer returns before JSON.stringify does. The common case is a scan that allocates nothing and returns the model as is; only a model that would have thrown gets copied, with the values that can't be probed replaced by the placeholder string, per value instead of per model. Since the pass never fails, no references are ever registered against JSON that doesn't get written. The fuzzer now exercises this: promise props are sometimes delivered behind a proxy that throws for any property access outside the thenable protocol. Values that reach a component through a userland thenable are recorded as such, since the io entry that unblocks them describes whatever context settled the thenable rather than the leaf that produced the value, and React cannot see through the thenable to forward the real chain. Co-authored-by: Claude Fable 5 <[email protected]>
… oracle The computed oracle derives three more expectations from what each seed's program recorded: - For a combinator over plain single-leaf parts, the part whose promise completed last (for all) or first (for race) is the one whose io unblocked it, and that leaf must be attributed in the initiating component's render; the other parts stay exempt. Completion order is recorded live because a part may take extra microtask hops after its leaf settles, so leaf settle order is not completion order. - A single-leaf promise passed down as a prop is awaited by the child, so it must be attributed inside the child's render, owned by the child. - Rejected awaits must attribute their io as precisely as resolved ones, and a leaf must settle the way it was created to settle. Awaited entries also carry an ownership expectation now: for an unshared fetch the entry must belong to the task of the component the generator recorded invoking the source. Two behaviors the checks have to model: a debug value that is deferred without a debug channel stays pending forever, so such an entry counts as a wildcard sighting against its owner instead of being indistinguishable from missing attribution; and a derived chain nested inside another composite attributes whichever io the enclosing await chain surfaces, not necessarily its own, so those leaves are exempt from completeness like combinator losers.
gaearon
force-pushed
the
gaearon-debuginfo-tweaks
branch
from
July 27, 2026 04:04
72aba02 to
af31235
Compare
dianatofficial
left a comment
There was a problem hiding this comment.
Clean implementation. Types and docs are up to date.
hoxyq
pushed a commit
that referenced
this pull request
Sep 13, 2026
…me names (#37608) ## Summary V8 prefixes async call sites with `async ` when it prints a stack, like this: ``` Error: boom at inner (/tmp/asy.js:1:44) at async outerName (/tmp/asy.js:2:30) ``` `parseStackTraceFromChromeStack` captures `async outerName` as the frame name and strips the prefix here: ```js } else if (name.startsWith('async ')) { name = name.slice(5); isAsync = true; } ``` `'async '` is six characters, so `slice(5)` leaves the space behind and the frame name comes back as `' outerName'` rather than `'outerName'`. I noticed it while reading the parser, and it is not purely cosmetic: that name is the first element of the `ReactFunctionLocation` returned by `extractLocationFromComponentStack` and `extractLocationFromOwnerStack`, which `backend/fiber/renderer.js` stores as `instance.source`. Any async component whose frame reaches that path is recorded under a name with a stray leading space. The same line exists in `packages/react-server/src/ReactFlightStackConfigV8.js`, which the DevTools file is a copy of. After review I fixed it in this PR as well, in a second commit. There it only matters on the fallback path that parses an already formatted stack string, when the error's `stack` was read or assigned before React reaches it. #37130 is open on that file too, but it does not touch these lines. ## How did you test this change? I first confirmed the format V8 actually emits, rather than assuming it: ``` $ node -e 'async function inner(){await null;throw new Error("boom")} async function outerName(){await inner()} outerName().catch(e=>console.log(e.stack))' Error: boom at inner ([eval]:1:52) at async outerName ([eval]:2:33) ``` Then I added a case to the existing `extractLocationFromComponentStack` block in `utils-test.js`. Against `main` it fails with exactly the leading space: ``` ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name - Expected - 1 + Received + 1 Array [ - "Comments", + " Comments", "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js", 5, 9236, ] ``` With the one-character fix applied: ``` $ yarn test --build --project=devtools -r=experimental utils-test PASS packages/react-devtools-shared/src/__tests__/utils-test.js Tests: 63 passed, 63 total ``` I also ran the whole DevTools project before and after to check I was not moving anything else. Both runs end at `9 failed, 4 failed suites`, the same test names each time (`componentStacks`, `console`, `inspectedElement`, `legacy/inspectElement`), so those failures are pre-existing on `main` in my environment and unrelated to this change. The only difference between the two runs is my new test: 586 passed before, 587 after. For the Flight side I added `ReactFlightStackConfigV8-test.js`, which assigns a formatted stack to an error and checks what `parseStackTrace` returns. Against `main` it fails with the same leading space (`" outerName"`); with the fix it passes on stable and experimental in development. It is gated to `__DEV__` because that fallback goes through the DEV-only stack cache. `ReactFlightServer-test` and `ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests). `prettier` and `eslint` are clean on all changed files. `yarn flow dom-node` reported no errors for the DevTools commit; I did not rerun Flow after the one-character Flight change. AI tools used
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 13, 2026
…me names (#37608) ## Summary V8 prefixes async call sites with `async ` when it prints a stack, like this: ``` Error: boom at inner (/tmp/asy.js:1:44) at async outerName (/tmp/asy.js:2:30) ``` `parseStackTraceFromChromeStack` captures `async outerName` as the frame name and strips the prefix here: ```js } else if (name.startsWith('async ')) { name = name.slice(5); isAsync = true; } ``` `'async '` is six characters, so `slice(5)` leaves the space behind and the frame name comes back as `' outerName'` rather than `'outerName'`. I noticed it while reading the parser, and it is not purely cosmetic: that name is the first element of the `ReactFunctionLocation` returned by `extractLocationFromComponentStack` and `extractLocationFromOwnerStack`, which `backend/fiber/renderer.js` stores as `instance.source`. Any async component whose frame reaches that path is recorded under a name with a stray leading space. The same line exists in `packages/react-server/src/ReactFlightStackConfigV8.js`, which the DevTools file is a copy of. After review I fixed it in this PR as well, in a second commit. There it only matters on the fallback path that parses an already formatted stack string, when the error's `stack` was read or assigned before React reaches it. #37130 is open on that file too, but it does not touch these lines. ## How did you test this change? I first confirmed the format V8 actually emits, rather than assuming it: ``` $ node -e 'async function inner(){await null;throw new Error("boom")} async function outerName(){await inner()} outerName().catch(e=>console.log(e.stack))' Error: boom at inner ([eval]:1:52) at async outerName ([eval]:2:33) ``` Then I added a case to the existing `extractLocationFromComponentStack` block in `utils-test.js`. Against `main` it fails with exactly the leading space: ``` ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name - Expected - 1 + Received + 1 Array [ - "Comments", + " Comments", "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js", 5, 9236, ] ``` With the one-character fix applied: ``` $ yarn test --build --project=devtools -r=experimental utils-test PASS packages/react-devtools-shared/src/__tests__/utils-test.js Tests: 63 passed, 63 total ``` I also ran the whole DevTools project before and after to check I was not moving anything else. Both runs end at `9 failed, 4 failed suites`, the same test names each time (`componentStacks`, `console`, `inspectedElement`, `legacy/inspectElement`), so those failures are pre-existing on `main` in my environment and unrelated to this change. The only difference between the two runs is my new test: 586 passed before, 587 after. For the Flight side I added `ReactFlightStackConfigV8-test.js`, which assigns a formatted stack to an error and checks what `parseStackTrace` returns. Against `main` it fails with the same leading space (`" outerName"`); with the fix it passes on stable and experimental in development. It is gated to `__DEV__` because that fallback goes through the DEV-only stack cache. `ReactFlightServer-test` and `ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests). `prettier` and `eslint` are clean on all changed files. `yarn flow dom-node` reported no errors for the DevTools commit; I did not rerun Flow after the one-character Flight change. AI tools used DiffTrain build for [ccea5fd](ccea5fd)
github-actions Bot
pushed a commit
to code/lib-react
that referenced
this pull request
Sep 13, 2026
…me names (react#37608) ## Summary V8 prefixes async call sites with `async ` when it prints a stack, like this: ``` Error: boom at inner (/tmp/asy.js:1:44) at async outerName (/tmp/asy.js:2:30) ``` `parseStackTraceFromChromeStack` captures `async outerName` as the frame name and strips the prefix here: ```js } else if (name.startsWith('async ')) { name = name.slice(5); isAsync = true; } ``` `'async '` is six characters, so `slice(5)` leaves the space behind and the frame name comes back as `' outerName'` rather than `'outerName'`. I noticed it while reading the parser, and it is not purely cosmetic: that name is the first element of the `ReactFunctionLocation` returned by `extractLocationFromComponentStack` and `extractLocationFromOwnerStack`, which `backend/fiber/renderer.js` stores as `instance.source`. Any async component whose frame reaches that path is recorded under a name with a stray leading space. The same line exists in `packages/react-server/src/ReactFlightStackConfigV8.js`, which the DevTools file is a copy of. After review I fixed it in this PR as well, in a second commit. There it only matters on the fallback path that parses an already formatted stack string, when the error's `stack` was read or assigned before React reaches it. react#37130 is open on that file too, but it does not touch these lines. ## How did you test this change? I first confirmed the format V8 actually emits, rather than assuming it: ``` $ node -e 'async function inner(){await null;throw new Error("boom")} async function outerName(){await inner()} outerName().catch(e=>console.log(e.stack))' Error: boom at inner ([eval]:1:52) at async outerName ([eval]:2:33) ``` Then I added a case to the existing `extractLocationFromComponentStack` block in `utils-test.js`. Against `main` it fails with exactly the leading space: ``` ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name - Expected - 1 + Received + 1 Array [ - "Comments", + " Comments", "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js", 5, 9236, ] ``` With the one-character fix applied: ``` $ yarn test --build --project=devtools -r=experimental utils-test PASS packages/react-devtools-shared/src/__tests__/utils-test.js Tests: 63 passed, 63 total ``` I also ran the whole DevTools project before and after to check I was not moving anything else. Both runs end at `9 failed, 4 failed suites`, the same test names each time (`componentStacks`, `console`, `inspectedElement`, `legacy/inspectElement`), so those failures are pre-existing on `main` in my environment and unrelated to this change. The only difference between the two runs is my new test: 586 passed before, 587 after. For the Flight side I added `ReactFlightStackConfigV8-test.js`, which assigns a formatted stack to an error and checks what `parseStackTrace` returns. Against `main` it fails with the same leading space (`" outerName"`); with the fix it passes on stable and experimental in development. It is gated to `__DEV__` because that fallback goes through the DEV-only stack cache. `ReactFlightServer-test` and `ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests). `prettier` and `eslint` are clean on all changed files. `yarn flow dom-node` reported no errors for the DevTools commit; I did not rerun Flow after the one-character Flight change. AI tools used DiffTrain build for [ccea5fd](react@ccea5fd)
github-actions Bot
pushed a commit
to code/lib-react
that referenced
this pull request
Sep 13, 2026
…me names (react#37608) ## Summary V8 prefixes async call sites with `async ` when it prints a stack, like this: ``` Error: boom at inner (/tmp/asy.js:1:44) at async outerName (/tmp/asy.js:2:30) ``` `parseStackTraceFromChromeStack` captures `async outerName` as the frame name and strips the prefix here: ```js } else if (name.startsWith('async ')) { name = name.slice(5); isAsync = true; } ``` `'async '` is six characters, so `slice(5)` leaves the space behind and the frame name comes back as `' outerName'` rather than `'outerName'`. I noticed it while reading the parser, and it is not purely cosmetic: that name is the first element of the `ReactFunctionLocation` returned by `extractLocationFromComponentStack` and `extractLocationFromOwnerStack`, which `backend/fiber/renderer.js` stores as `instance.source`. Any async component whose frame reaches that path is recorded under a name with a stray leading space. The same line exists in `packages/react-server/src/ReactFlightStackConfigV8.js`, which the DevTools file is a copy of. After review I fixed it in this PR as well, in a second commit. There it only matters on the fallback path that parses an already formatted stack string, when the error's `stack` was read or assigned before React reaches it. react#37130 is open on that file too, but it does not touch these lines. ## How did you test this change? I first confirmed the format V8 actually emits, rather than assuming it: ``` $ node -e 'async function inner(){await null;throw new Error("boom")} async function outerName(){await inner()} outerName().catch(e=>console.log(e.stack))' Error: boom at inner ([eval]:1:52) at async outerName ([eval]:2:33) ``` Then I added a case to the existing `extractLocationFromComponentStack` block in `utils-test.js`. Against `main` it fails with exactly the leading space: ``` ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name - Expected - 1 + Received + 1 Array [ - "Comments", + " Comments", "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js", 5, 9236, ] ``` With the one-character fix applied: ``` $ yarn test --build --project=devtools -r=experimental utils-test PASS packages/react-devtools-shared/src/__tests__/utils-test.js Tests: 63 passed, 63 total ``` I also ran the whole DevTools project before and after to check I was not moving anything else. Both runs end at `9 failed, 4 failed suites`, the same test names each time (`componentStacks`, `console`, `inspectedElement`, `legacy/inspectElement`), so those failures are pre-existing on `main` in my environment and unrelated to this change. The only difference between the two runs is my new test: 586 passed before, 587 after. For the Flight side I added `ReactFlightStackConfigV8-test.js`, which assigns a formatted stack to an error and checks what `parseStackTrace` returns. Against `main` it fails with the same leading space (`" outerName"`); with the fix it passes on stable and experimental in development. It is gated to `__DEV__` because that fallback goes through the DEV-only stack cache. `ReactFlightServer-test` and `ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests). `prettier` and `eslint` are clean on all changed files. `yarn flow dom-node` reported no errors for the DevTools commit; I did not rerun Flow after the one-character Flight change. AI tools used DiffTrain build for [ccea5fd](react@ccea5fd)
github-actions Bot
pushed a commit
to seshan18/react
that referenced
this pull request
Sep 13, 2026
…me names (react#37608) ## Summary V8 prefixes async call sites with `async ` when it prints a stack, like this: ``` Error: boom at inner (/tmp/asy.js:1:44) at async outerName (/tmp/asy.js:2:30) ``` `parseStackTraceFromChromeStack` captures `async outerName` as the frame name and strips the prefix here: ```js } else if (name.startsWith('async ')) { name = name.slice(5); isAsync = true; } ``` `'async '` is six characters, so `slice(5)` leaves the space behind and the frame name comes back as `' outerName'` rather than `'outerName'`. I noticed it while reading the parser, and it is not purely cosmetic: that name is the first element of the `ReactFunctionLocation` returned by `extractLocationFromComponentStack` and `extractLocationFromOwnerStack`, which `backend/fiber/renderer.js` stores as `instance.source`. Any async component whose frame reaches that path is recorded under a name with a stray leading space. The same line exists in `packages/react-server/src/ReactFlightStackConfigV8.js`, which the DevTools file is a copy of. After review I fixed it in this PR as well, in a second commit. There it only matters on the fallback path that parses an already formatted stack string, when the error's `stack` was read or assigned before React reaches it. react#37130 is open on that file too, but it does not touch these lines. ## How did you test this change? I first confirmed the format V8 actually emits, rather than assuming it: ``` $ node -e 'async function inner(){await null;throw new Error("boom")} async function outerName(){await inner()} outerName().catch(e=>console.log(e.stack))' Error: boom at inner ([eval]:1:52) at async outerName ([eval]:2:33) ``` Then I added a case to the existing `extractLocationFromComponentStack` block in `utils-test.js`. Against `main` it fails with exactly the leading space: ``` ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name - Expected - 1 + Received + 1 Array [ - "Comments", + " Comments", "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js", 5, 9236, ] ``` With the one-character fix applied: ``` $ yarn test --build --project=devtools -r=experimental utils-test PASS packages/react-devtools-shared/src/__tests__/utils-test.js Tests: 63 passed, 63 total ``` I also ran the whole DevTools project before and after to check I was not moving anything else. Both runs end at `9 failed, 4 failed suites`, the same test names each time (`componentStacks`, `console`, `inspectedElement`, `legacy/inspectElement`), so those failures are pre-existing on `main` in my environment and unrelated to this change. The only difference between the two runs is my new test: 586 passed before, 587 after. For the Flight side I added `ReactFlightStackConfigV8-test.js`, which assigns a formatted stack to an error and checks what `parseStackTrace` returns. Against `main` it fails with the same leading space (`" outerName"`); with the fix it passes on stable and experimental in development. It is gated to `__DEV__` because that fallback goes through the DEV-only stack cache. `ReactFlightServer-test` and `ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests). `prettier` and `eslint` are clean on all changed files. `yarn flow dom-node` reported no errors for the DevTools commit; I did not rerun Flow after the one-character Flight change. AI tools used DiffTrain build for [ccea5fd](react@ccea5fd)
github-actions Bot
pushed a commit
to seshan18/react
that referenced
this pull request
Sep 13, 2026
…me names (react#37608) ## Summary V8 prefixes async call sites with `async ` when it prints a stack, like this: ``` Error: boom at inner (/tmp/asy.js:1:44) at async outerName (/tmp/asy.js:2:30) ``` `parseStackTraceFromChromeStack` captures `async outerName` as the frame name and strips the prefix here: ```js } else if (name.startsWith('async ')) { name = name.slice(5); isAsync = true; } ``` `'async '` is six characters, so `slice(5)` leaves the space behind and the frame name comes back as `' outerName'` rather than `'outerName'`. I noticed it while reading the parser, and it is not purely cosmetic: that name is the first element of the `ReactFunctionLocation` returned by `extractLocationFromComponentStack` and `extractLocationFromOwnerStack`, which `backend/fiber/renderer.js` stores as `instance.source`. Any async component whose frame reaches that path is recorded under a name with a stray leading space. The same line exists in `packages/react-server/src/ReactFlightStackConfigV8.js`, which the DevTools file is a copy of. After review I fixed it in this PR as well, in a second commit. There it only matters on the fallback path that parses an already formatted stack string, when the error's `stack` was read or assigned before React reaches it. react#37130 is open on that file too, but it does not touch these lines. ## How did you test this change? I first confirmed the format V8 actually emits, rather than assuming it: ``` $ node -e 'async function inner(){await null;throw new Error("boom")} async function outerName(){await inner()} outerName().catch(e=>console.log(e.stack))' Error: boom at inner ([eval]:1:52) at async outerName ([eval]:2:33) ``` Then I added a case to the existing `extractLocationFromComponentStack` block in `utils-test.js`. Against `main` it fails with exactly the leading space: ``` ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name - Expected - 1 + Received + 1 Array [ - "Comments", + " Comments", "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js", 5, 9236, ] ``` With the one-character fix applied: ``` $ yarn test --build --project=devtools -r=experimental utils-test PASS packages/react-devtools-shared/src/__tests__/utils-test.js Tests: 63 passed, 63 total ``` I also ran the whole DevTools project before and after to check I was not moving anything else. Both runs end at `9 failed, 4 failed suites`, the same test names each time (`componentStacks`, `console`, `inspectedElement`, `legacy/inspectElement`), so those failures are pre-existing on `main` in my environment and unrelated to this change. The only difference between the two runs is my new test: 586 passed before, 587 after. For the Flight side I added `ReactFlightStackConfigV8-test.js`, which assigns a formatted stack to an error and checks what `parseStackTrace` returns. Against `main` it fails with the same leading space (`" outerName"`); with the fix it passes on stable and experimental in development. It is gated to `__DEV__` because that fallback goes through the DEV-only stack cache. `ReactFlightServer-test` and `ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests). `prettier` and `eslint` are clean on all changed files. `yarn flow dom-node` reported no errors for the DevTools commit; I did not rerun Flow after the one-character Flight change. AI tools used DiffTrain build for [ccea5fd](react@ccea5fd)
This branch has not been deployed
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.
Stacked on #37129.
I'm not sure these are important and show up in practice vs which are purely test artifacts. This is the stuff found by the fuzzer so I figured we might want to fix these. See individual commits.
Strip hook dispatch stack frames by module instead of position
The await stack parse skipped a fixed count of
async_hooksdispatch frames. This is a bit fragile (it is already wrong for plainfscallbacks). The proposal is to skip our own frame by count and the dispatch frames by module instead. The fuzzer now checks IO frame names; without the fix that fails on 5 of 20 seeds.Don't turn fulfillment-only chunk subscriptions into unhandled rejections
In DEV, calling
chunk.then(onFulfill)with no rejection callback still wires up a real native Promise inside the wrapper. If the chunk errors, that Promise rejects, and nothing can catch it: the caller didn't pass a rejection callback, and our.then()returns undefined so there's nothing to attach one to. Node then kills the process for an unhandled rejection. In practice only our test utils subscribe like this today, so this mostly makes tests around rejections not blow up. The fix is to only forward the rejection when the caller passed a rejection callback, which is what prod does.Don't let debug info failures corrupt the data they describe
The server degrades a debug model to a placeholder string when it fails to serialize, and a prop that throws on property access triggers that today. The client then threw while initializing entries that reference the degraded model — writing a fake stack location onto an owner that's now a string — and the throw rejected the entry's pending data chunk. When the chunk's real row arrived,
resolveModelChunkassumed that this means it's a stream chunk (because the chunk was no longer pending) and crashed the whole stream withcontroller.enqueueModel is not a function.Now the failed entry is dropped instead (its slot gets a minimal component info). The next commit fixes the trigger, so this is not strictly necessary for the tests to pass, but it prevents similar crashes if there are more bugs like it.
Keep the shape of debug models that fail to serialize
JSON.stringifyprobestoJSONon every raw object before the replacer can substitute it, so a value whose property access throws replaced the whole model with a placeholder string, breaking every reference into it.Probe each object the replacer returns first. If it throws, copy with a placeholder per broken value. The fuzzer now delivers promise props behind throwing proxies; without the fix that fails on 3 of 20 seeds.