@samitouri / QOS-React / commits / 4a523489b7

Get Server Component Function Location for Parent Stacks using Child's Owner Stack (#33629)

This is using the same trick as #30798 but for runtime code too. It's essential zero cost. This lets us include a source location for parent stacks of Server Components when it has an owned child's location. Either from JSX or I/O. Ironically, a Component that throws an error will likely itself not get the stack because it won't have any JSX rendered yet.

Sebastian Markbåge committed Jun 24, 2025 at 16:35 UTC 4a523489b7dc64cd397f619e50223edda1b9a321
8 files changed +47 -9
packages/react-client/src/ReactFlightClient.js
+8 -2
@@ -2739,9 +2739,15 @@ function initializeFakeStack(
2739 // $FlowFixMe[cannot-write]
2740 debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
2741 }
2742 - if (debugInfo.owner != null) {
2742 + const owner = debugInfo.owner;
2743 + if (owner != null) {
2744 // Initialize any owners not yet initialized.
2744 - initializeFakeStack(response, debugInfo.owner);
2745 + initializeFakeStack(response, owner);
2746 + if (owner.debugLocation === undefined && debugInfo.debugStack != null) {
2747 + // If we are the child of this owner, then the owner should be the bottom frame
2748 + // our stack. We can use it as the implied location of the owner.
2749 + owner.debugLocation = debugInfo.debugStack;
2750 + }
2751 }
2752 }
2753
packages/react-client/src/__tests__/ReactFlight-test.js
+1 -1
@@ -69,7 +69,7 @@ function getErrorForJestMatcher(error) {
69
70 function normalizeComponentInfo(debugInfo) {
71 if (Array.isArray(debugInfo.stack)) {
72 - const {debugTask, debugStack, ...copy} = debugInfo;
72 + const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
73 copy.stack = formatV8Stack(debugInfo.stack);
74 if (debugInfo.owner) {
75 copy.owner = normalizeComponentInfo(debugInfo.owner);
packages/react-devtools-shared/src/backend/fiber/renderer.js
+9 -1
@@ -5831,13 +5831,21 @@ export function attach(
5831 }
5832
5833 function getSourceForInstance(instance: DevToolsInstance): Source | null {
5834 - const unresolvedSource = instance.source;
5834 + let unresolvedSource = instance.source;
5835 if (unresolvedSource === null) {
5836 // We don't have any source yet. We can try again later in case an owned child mounts later.
5837 // TODO: We won't have any information here if the child is filtered.
5838 return null;
5839 }
5840
5841 + if (instance.kind === VIRTUAL_INSTANCE) {
5842 + // We might have found one on the virtual instance.
5843 + const debugLocation = instance.data.debugLocation;
5844 + if (debugLocation != null) {
5845 + unresolvedSource = debugLocation;
5846 + }
5847 + }
5848 +
5849 // If we have the debug stack (the creation stack of the JSX) for any owned child of this
5850 // component, then at the bottom of that stack will be a stack frame that is somewhere within
5851 // the component's function body. Typically it would be the callsite of the JSX unless there's
packages/react-reconciler/src/ReactFiberComponentStack.js
+5 -1
@@ -79,7 +79,11 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
79 for (let i = debugInfo.length - 1; i >= 0; i--) {
80 const entry = debugInfo[i];
81 if (typeof entry.name === 'string') {
82 - info += describeDebugInfoFrame(entry.name, entry.env);
82 + info += describeDebugInfoFrame(
83 + entry.name,
84 + entry.env,
85 + entry.debugLocation,
86 + );
87 }
88 }
89 }
packages/react-server/src/ReactFizzComponentStack.js
+1 -1
@@ -86,7 +86,7 @@ function describeComponentStackByType(
86 }
87 }
88 if (typeof type.name === 'string') {
89 - return describeDebugInfoFrame(type.name, type.env);
89 + return describeDebugInfoFrame(type.name, type.env, type.debugLocation);
90 }
91 }
92 switch (type) {
packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js
+2 -2
@@ -37,7 +37,7 @@ function normalizeStack(stack) {
37 }
38
39 function normalizeIOInfo(ioInfo) {
40 - const {debugTask, debugStack, ...copy} = ioInfo;
40 + const {debugTask, debugStack, debugLocation, ...copy} = ioInfo;
41 if (ioInfo.stack) {
42 copy.stack = normalizeStack(ioInfo.stack);
43 }
@@ -72,7 +72,7 @@ function normalizeIOInfo(ioInfo) {
72
73 function normalizeDebugInfo(debugInfo) {
74 if (Array.isArray(debugInfo.stack)) {
75 - const {debugTask, debugStack, ...copy} = debugInfo;
75 + const {debugTask, debugStack, debugLocation, ...copy} = debugInfo;
76 copy.stack = normalizeStack(debugInfo.stack);
77 if (debugInfo.owner) {
78 copy.owner = normalizeDebugInfo(debugInfo.owner);
packages/shared/ReactComponentStackFrame.js
+20 -1
@@ -13,6 +13,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
13
14 import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace';
15
16 +import {formatOwnerStack} from './ReactOwnerStackFrames';
17 +
18 let prefix;
19 let suffix;
20 export function describeBuiltInComponentFrame(name: string): string {
@@ -38,7 +40,24 @@ export function describeBuiltInComponentFrame(name: string): string {
40 return '\n' + prefix + name + suffix;
41 }
42
41 -export function describeDebugInfoFrame(name: string, env: ?string): string {
43 +export function describeDebugInfoFrame(
44 + name: string,
45 + env: ?string,
46 + location: ?Error,
47 +): string {
48 + if (location != null) {
49 + // If we have a location, it's the child's owner stack. Treat the bottom most frame as
50 + // the location of this function.
51 + const childStack = formatOwnerStack(location);
52 + const idx = childStack.lastIndexOf('\n');
53 + const lastLine = idx === -1 ? childStack : childStack.slice(idx + 1);
54 + if (lastLine.indexOf(name) !== -1) {
55 + // For async stacks it's possible we don't have the owner on it. As a precaution only
56 + // use this frame if it has the name of the function in it.
57 + return '\n' + lastLine;
58 + }
59 + }
60 +
61 return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : ''));
62 }
63
packages/shared/ReactTypes.js
+1
@@ -209,6 +209,7 @@ export type ReactComponentInfo = {
209 // Stashed Data for the Specific Execution Environment. Not part of the transport protocol
210 +debugStack?: null | Error,
211 +debugTask?: null | ConsoleTask,
212 + debugLocation?: null | Error,
213 };
214
215 export type ReactEnvironmentInfo = {