@samitouri / QOS-React-1 / commits / 151cce3740

Track Stack of JSX Calls (#29032)

This is the first step to experimenting with a new type of stack traces behind the `enableOwnerStacks` flag - in DEV only. The idea is to generate stacks that are more like if the JSX was a direct call even though it's actually a lazy call. Not only can you see which exact JSX call line number generated the erroring component but if that's inside an abstraction function, which function called that function and if it's a component, which component generated that component. For this to make sense it really need to be the "owner" stack rather than the parent stack like we do for other component stacks. On one hand it has more precise information but on the other hand it also loses context. For most types of problems the owner stack is the most useful though since it tells you which component rendered this component. The problem with the platform in its current state is that there's two ways to deal with stacks: 1) `new Error().stack` 2) `console.createTask()` The nice thing about `new Error().stack` is that we can extract the frames and piece them together in whatever way we want. That is great for constructing custom UIs like error dialogs. Unfortunately, we can't take custom stacks and set them in the native UIs like Chrome DevTools. The nice thing about `console.createTask()` is that the resulting stacks are natively integrated into the Chrome DevTools in the console and the breakpoint debugger. They also automatically follow source mapping and ignoreLists. The downside is that there's no way to extract the async stack outside the native UI itself so this information cannot be used for custom UIs like errors dialogs. It also means we can't collect this on the server and then pass it to the client for server components. The solution here is that we use both techniques and collect both an `Error` object and a `Task` object for every JSX call. The main concern about this approach is the performance so that's the main thing to test. It's certainly too slow for production but it might also be too slow even for DEV. This first PR doesn't actually use the stacks yet. It just collects them as the first step. The next step is to start utilizing this information in error printing etc. For RSC we pass the stack along across over the wire. This can be concatenated on the client following the owner path to create an owner stack leading back into the server. We'll later use this information to restore fake frames on the client for native integration. Since this information quickly gets pretty heavy if we include all frames, we strip out the top frame. We also strip out everything below the functions that call into user space in the Flight runtime. To do this we need to figure out the frames that represents calling out into user space. The resulting stack is typically just the one frame inside the owner component's JSX callsite. I also eagerly strip out things we expect to be ignoreList:ed anyway - such as `node_modules` and Node.js internals.

Sebastian Markbåge committed May 9, 2024 at 12:23 UTC 151cce37401dc2ff609701119d61a17d92fce4ab
17 files changed +483 -70
packages/react-client/src/ReactFlightClient.js
+20
@@ -43,6 +43,7 @@ import {
43 enablePostpone,
44 enableRefAsProp,
45 enableFlightReadableStream,
46 + enableOwnerStacks,
47 } from 'shared/ReactFeatureFlags';
48
49 import {
@@ -563,6 +564,7 @@ function createElement(
564 key: mixed,
565 props: mixed,
566 owner: null | ReactComponentInfo, // DEV-only
567 + stack: null | string, // DEV-only
568 ): React$Element<any> {
569 let element: any;
570 if (__DEV__ && enableRefAsProp) {
@@ -623,6 +625,23 @@ function createElement(
625 writable: true,
626 value: null,
627 });
628 + if (enableOwnerStacks) {
629 + Object.defineProperty(element, '_debugStack', {
630 + configurable: false,
631 + enumerable: false,
632 + writable: true,
633 + value: {stack: stack},
634 + });
635 + Object.defineProperty(element, '_debugTask', {
636 + configurable: false,
637 + enumerable: false,
638 + writable: true,
639 + value: null,
640 + });
641 + }
642 + // TODO: We should be freezing the element but currently, we might write into
643 + // _debugInfo later. We could move it into _store which remains mutable.
644 + Object.freeze(element.props);
645 }
646 return element;
647 }
@@ -1003,6 +1022,7 @@ function parseModelTuple(
1022 tuple[2],
1023 tuple[3],
1024 __DEV__ ? (tuple: any)[4] : null,
1025 + __DEV__ && enableOwnerStacks ? (tuple: any)[5] : null,
1026 );
1027 }
1028 return value;
packages/react-client/src/__tests__/ReactFlight-test.js
+127 -24
@@ -21,12 +21,24 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
21 function normalizeCodeLocInfo(str) {
22 return (
23 str &&
24 - str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
25 - return '\n in ' + name + (/\d/.test(m) ? ' (at **)' : '');
24 + str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
25 + return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
26 })
27 );
28 }
29
30 +function getDebugInfo(obj) {
31 + const debugInfo = obj._debugInfo;
32 + if (debugInfo) {
33 + for (let i = 0; i < debugInfo.length; i++) {
34 + if (typeof debugInfo[i].stack === 'string') {
35 + debugInfo[i].stack = normalizeCodeLocInfo(debugInfo[i].stack);
36 + }
37 + }
38 + }
39 + return debugInfo;
40 +}
41 +
42 const heldValues = [];
43 let finalizationCallback;
44 function FinalizationRegistryMock(callback) {
@@ -221,8 +233,19 @@ describe('ReactFlight', () => {
233 await act(async () => {
234 const rootModel = await ReactNoopFlightClient.read(transport);
235 const greeting = rootModel.greeting;
224 - expect(greeting._debugInfo).toEqual(
225 - __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined,
236 + expect(getDebugInfo(greeting)).toEqual(
237 + __DEV__
238 + ? [
239 + {
240 + name: 'Greeting',
241 + env: 'Server',
242 + owner: null,
243 + stack: gate(flag => flag.enableOwnerStacks)
244 + ? ' in Object.<anonymous> (at **)'
245 + : undefined,
246 + },
247 + ]
248 + : undefined,
249 );
250 ReactNoop.render(greeting);
251 });
@@ -248,8 +271,19 @@ describe('ReactFlight', () => {
271
272 await act(async () => {
273 const promise = ReactNoopFlightClient.read(transport);
251 - expect(promise._debugInfo).toEqual(
252 - __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined,
274 + expect(getDebugInfo(promise)).toEqual(
275 + __DEV__
276 + ? [
277 + {
278 + name: 'Greeting',
279 + env: 'Server',
280 + owner: null,
281 + stack: gate(flag => flag.enableOwnerStacks)
282 + ? ' in Object.<anonymous> (at **)'
283 + : undefined,
284 + },
285 + ]
286 + : undefined,
287 );
288 ReactNoop.render(await promise);
289 });
@@ -2233,9 +2267,11 @@ describe('ReactFlight', () => {
2267 return <span>!</span>;
2268 }
2269
2236 - const lazy = React.lazy(async () => ({
2237 - default: <ThirdPartyLazyComponent />,
2238 - }));
2270 + const lazy = React.lazy(async function myLazy() {
2271 + return {
2272 + default: <ThirdPartyLazyComponent />,
2273 + };
2274 + });
2275
2276 function ThirdPartyComponent() {
2277 return <span>stranger</span>;
@@ -2269,31 +2305,61 @@ describe('ReactFlight', () => {
2305
2306 await act(async () => {
2307 const promise = ReactNoopFlightClient.read(transport);
2272 - expect(promise._debugInfo).toEqual(
2308 + expect(getDebugInfo(promise)).toEqual(
2309 __DEV__
2274 - ? [{name: 'ServerComponent', env: 'Server', owner: null}]
2310 + ? [
2311 + {
2312 + name: 'ServerComponent',
2313 + env: 'Server',
2314 + owner: null,
2315 + stack: gate(flag => flag.enableOwnerStacks)
2316 + ? ' in Object.<anonymous> (at **)'
2317 + : undefined,
2318 + },
2319 + ]
2320 : undefined,
2321 );
2322 const result = await promise;
2323 const thirdPartyChildren = await result.props.children[1];
2324 // We expect the debug info to be transferred from the inner stream to the outer.
2280 - expect(thirdPartyChildren[0]._debugInfo).toEqual(
2325 + expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
2326 __DEV__
2282 - ? [{name: 'ThirdPartyComponent', env: 'third-party', owner: null}]
2327 + ? [
2328 + {
2329 + name: 'ThirdPartyComponent',
2330 + env: 'third-party',
2331 + owner: null,
2332 + stack: gate(flag => flag.enableOwnerStacks)
2333 + ? ' in Object.<anonymous> (at **)'
2334 + : undefined,
2335 + },
2336 + ]
2337 : undefined,
2338 );
2285 - expect(thirdPartyChildren[1]._debugInfo).toEqual(
2339 + expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
2340 __DEV__
2287 - ? [{name: 'ThirdPartyLazyComponent', env: 'third-party', owner: null}]
2341 + ? [
2342 + {
2343 + name: 'ThirdPartyLazyComponent',
2344 + env: 'third-party',
2345 + owner: null,
2346 + stack: gate(flag => flag.enableOwnerStacks)
2347 + ? ' in myLazy (at **)\n in lazyInitializer (at **)'
2348 + : undefined,
2349 + },
2350 + ]
2351 : undefined,
2352 );
2290 - expect(thirdPartyChildren[2]._debugInfo).toEqual(
2353 + expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
2354 __DEV__
2355 ? [
2356 {
2357 name: 'ThirdPartyFragmentComponent',
2358 env: 'third-party',
2359 owner: null,
2360 + stack: gate(flag => flag.enableOwnerStacks)
2361 + ? ' in Object.<anonymous> (at **)'
2362 + : undefined,
2363 },
2364 ]
2365 : undefined,
@@ -2357,24 +2423,47 @@ describe('ReactFlight', () => {
2423
2424 await act(async () => {
2425 const promise = ReactNoopFlightClient.read(transport);
2360 - expect(promise._debugInfo).toEqual(
2426 + expect(getDebugInfo(promise)).toEqual(
2427 __DEV__
2362 - ? [{name: 'ServerComponent', env: 'Server', owner: null}]
2428 + ? [
2429 + {
2430 + name: 'ServerComponent',
2431 + env: 'Server',
2432 + owner: null,
2433 + stack: gate(flag => flag.enableOwnerStacks)
2434 + ? ' in Object.<anonymous> (at **)'
2435 + : undefined,
2436 + },
2437 + ]
2438 : undefined,
2439 );
2440 const result = await promise;
2441 const thirdPartyFragment = await result.props.children;
2367 - expect(thirdPartyFragment._debugInfo).toEqual(
2368 - __DEV__ ? [{name: 'Keyed', env: 'Server', owner: null}] : undefined,
2442 + expect(getDebugInfo(thirdPartyFragment)).toEqual(
2443 + __DEV__
2444 + ? [
2445 + {
2446 + name: 'Keyed',
2447 + env: 'Server',
2448 + owner: null,
2449 + stack: gate(flag => flag.enableOwnerStacks)
2450 + ? ' in ServerComponent (at **)'
2451 + : undefined,
2452 + },
2453 + ]
2454 + : undefined,
2455 );
2456 // We expect the debug info to be transferred from the inner stream to the outer.
2371 - expect(thirdPartyFragment.props.children._debugInfo).toEqual(
2457 + expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
2458 __DEV__
2459 ? [
2460 {
2461 name: 'ThirdPartyAsyncIterableComponent',
2462 env: 'third-party',
2463 owner: null,
2464 + stack: gate(flag => flag.enableOwnerStacks)
2465 + ? ' in Object.<anonymous> (at **)'
2466 + : undefined,
2467 },
2468 ]
2469 : undefined,
@@ -2467,10 +2556,24 @@ describe('ReactFlight', () => {
2556 // We've rendered down to the span.
2557 expect(greeting.type).toBe('span');
2558 if (__DEV__) {
2470 - const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
2471 - expect(greeting._debugInfo).toEqual([
2559 + const greetInfo = {
2560 + name: 'Greeting',
2561 + env: 'Server',
2562 + owner: null,
2563 + stack: gate(flag => flag.enableOwnerStacks)
2564 + ? ' in Object.<anonymous> (at **)'
2565 + : undefined,
2566 + };
2567 + expect(getDebugInfo(greeting)).toEqual([
2568 greetInfo,
2473 - {name: 'Container', env: 'Server', owner: greetInfo},
2569 + {
2570 + name: 'Container',
2571 + env: 'Server',
2572 + owner: greetInfo,
2573 + stack: gate(flag => flag.enableOwnerStacks)
2574 + ? ' in Greeting (at **)'
2575 + : undefined,
2576 + },
2577 ]);
2578 // The owner that created the span was the outer server component.
2579 // We expect the debug info to be referentially equal to the owner.
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+13 -5
@@ -263,7 +263,7 @@ describe('ReactFlightDOMEdge', () => {
263
264 const serializedContent = await readResult(stream1);
265
266 - expect(serializedContent.length).toBeLessThan(400);
266 + expect(serializedContent.length).toBeLessThan(410);
267 expect(timesRendered).toBeLessThan(5);
268
269 const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
@@ -296,7 +296,7 @@ describe('ReactFlightDOMEdge', () => {
296 const [stream1, stream2] = passThrough(stream).tee();
297
298 const serializedContent = await readResult(stream1);
299 - expect(serializedContent.length).toBeLessThan(400);
299 + expect(serializedContent.length).toBeLessThan(__DEV__ ? 590 : 400);
300 expect(timesRendered).toBeLessThan(5);
301
302 const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
@@ -324,7 +324,7 @@ describe('ReactFlightDOMEdge', () => {
324 <ServerComponent recurse={20} />,
325 );
326 const serializedContent = await readResult(stream);
327 - const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0;
327 + const expectedDebugInfoSize = __DEV__ ? 300 * 20 : 0;
328 expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
329 });
330
@@ -742,10 +742,18 @@ describe('ReactFlightDOMEdge', () => {
742 // We've rendered down to the span.
743 expect(greeting.type).toBe('span');
744 if (__DEV__) {
745 - const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
745 + const greetInfo = expect.objectContaining({
746 + name: 'Greeting',
747 + env: 'Server',
748 + owner: null,
749 + });
750 expect(lazyWrapper._debugInfo).toEqual([
751 greetInfo,
748 - {name: 'Container', env: 'Server', owner: greetInfo},
752 + expect.objectContaining({
753 + name: 'Container',
754 + env: 'Server',
755 + owner: greetInfo,
756 + }),
757 ]);
758 // The owner that created the span was the outer server component.
759 // We expect the debug info to be referentially equal to the owner.
packages/react-server/src/ReactFlightServer.js
+210 -38
@@ -17,6 +17,7 @@ import {
17 enableTaint,
18 enableRefAsProp,
19 enableServerComponentLogs,
20 + enableOwnerStacks,
21 } from 'shared/ReactFeatureFlags';
22
23 import {enableFlightReadableStream} from 'shared/ReactFeatureFlags';
@@ -123,6 +124,98 @@ import binaryToComparableString from 'shared/binaryToComparableString';
124
125 import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
126
127 +// TODO: Make this configurable on the Request.
128 +const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>\)/;
129 +
130 +let callComponentFrame: null | string = null;
131 +let callIteratorFrame: null | string = null;
132 +let callLazyInitFrame: null | string = null;
133 +
134 +function isNotExternal(stackFrame: string): boolean {
135 + return !externalRegExp.test(stackFrame);
136 +}
137 +
138 +function initCallComponentFrame(): string {
139 + // Extract the stack frame of the callComponentInDEV function.
140 + const error = callComponentInDEV(Error, 'react-stack-top-frame', {});
141 + const stack = error.stack;
142 + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
143 + const endIdx = stack.indexOf('\n', startIdx);
144 + if (endIdx === -1) {
145 + return stack.slice(startIdx);
146 + }
147 + return stack.slice(startIdx, endIdx);
148 +}
149 +
150 +function initCallIteratorFrame(): string {
151 + // Extract the stack frame of the callIteratorInDEV function.
152 + try {
153 + (callIteratorInDEV: any)({next: null});
154 + return '';
155 + } catch (error) {
156 + const stack = error.stack;
157 + const startIdx = stack.startsWith('TypeError: ')
158 + ? stack.indexOf('\n') + 1
159 + : 0;
160 + const endIdx = stack.indexOf('\n', startIdx);
161 + if (endIdx === -1) {
162 + return stack.slice(startIdx);
163 + }
164 + return stack.slice(startIdx, endIdx);
165 + }
166 +}
167 +
168 +function initCallLazyInitFrame(): string {
169 + // Extract the stack frame of the callLazyInitInDEV function.
170 + const error = callLazyInitInDEV({
171 + $$typeof: REACT_LAZY_TYPE,
172 + _init: Error,
173 + _payload: 'react-stack-top-frame',
174 + });
175 + const stack = error.stack;
176 + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
177 + const endIdx = stack.indexOf('\n', startIdx);
178 + if (endIdx === -1) {
179 + return stack.slice(startIdx);
180 + }
181 + return stack.slice(startIdx, endIdx);
182 +}
183 +
184 +function filterDebugStack(error: Error): string {
185 + // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
186 + // to save bandwidth even in DEV. We'll also replay these stacks on the client so by
187 + // stripping them early we avoid that overhead. Otherwise we'd normally just rely on
188 + // the DevTools or framework's ignore lists to filter them out.
189 + let stack = error.stack;
190 + if (stack.startsWith('Error: react-stack-top-frame\n')) {
191 + // V8's default formatting prefixes with the error message which we
192 + // don't want/need.
193 + stack = stack.slice(29);
194 + }
195 + const frames = stack.split('\n').slice(1);
196 + if (callComponentFrame === null) {
197 + callComponentFrame = initCallComponentFrame();
198 + }
199 + let lastFrameIdx = frames.indexOf(callComponentFrame);
200 + if (lastFrameIdx === -1) {
201 + if (callLazyInitFrame === null) {
202 + callLazyInitFrame = initCallLazyInitFrame();
203 + }
204 + lastFrameIdx = frames.indexOf(callLazyInitFrame);
205 + if (lastFrameIdx === -1) {
206 + if (callIteratorFrame === null) {
207 + callIteratorFrame = initCallIteratorFrame();
208 + }
209 + lastFrameIdx = frames.indexOf(callIteratorFrame);
210 + }
211 + }
212 + if (lastFrameIdx !== -1) {
213 + // Cut off everything after our "callComponent" slot since it'll be Flight internals.
214 + frames.length = lastFrameIdx;
215 + }
216 + return frames.filter(isNotExternal).join('\n');
217 +}
218 +
219 initAsyncDebugInfo();
220
221 function patchConsole(consoleInst: typeof console, methodName: string) {
@@ -146,10 +239,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
239 // Extract the stack. Not all console logs print the full stack but they have at
240 // least the line it was called from. We could optimize transfer by keeping just
241 // one stack frame but keeping it simple for now and include all frames.
149 - let stack = new Error().stack;
150 - if (stack.startsWith('Error: \n')) {
151 - stack = stack.slice(8);
152 - }
242 + let stack = filterDebugStack(new Error('react-stack-top-frame'));
243 const firstLine = stack.indexOf('\n');
244 if (firstLine === -1) {
245 stack = '';
@@ -621,6 +711,20 @@ function serializeReadableStream(
711 return serializeByValueID(streamTask.id);
712 }
713
714 +// This indirect exists so we can exclude its stack frame in DEV (and anything below it).
715 +/** @noinline */
716 +function callIteratorInDEV(
717 + iterator: $AsyncIterator<ReactClientValue, ReactClientValue, void>,
718 + progress: (
719 + entry:
720 + | {done: false, +value: ReactClientValue, ...}
721 + | {done: true, +value: ReactClientValue, ...},
722 + ) => void,
723 + error: (reason: mixed) => void,
724 +) {
725 + iterator.next().then(progress, error);
726 +}
727 +
728 function serializeAsyncIterable(
729 request: Request,
730 task: Task,
@@ -697,7 +801,11 @@ function serializeAsyncIterable(
801 request.pendingChunks++;
802 tryStreamTask(request, streamTask);
803 enqueueFlush(request);
700 - iterator.next().then(progress, error);
804 + if (__DEV__) {
805 + callIteratorInDEV(iterator, progress, error);
806 + } else {
807 + iterator.next().then(progress, error);
808 + }
809 } catch (x) {
810 error(x);
811 return;
@@ -731,7 +839,11 @@ function serializeAsyncIterable(
839 }
840 }
841 request.abortListeners.add(error);
734 - iterator.next().then(progress, error);
842 + if (__DEV__) {
843 + callIteratorInDEV(iterator, progress, error);
844 + } else {
845 + iterator.next().then(progress, error);
846 + }
847 return serializeByValueID(streamTask.id);
848 }
849
@@ -809,13 +921,49 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
921 return lazyType;
922 }
923
924 +// This indirect exists so we can exclude its stack frame in DEV (and anything below it).
925 +/** @noinline */
926 +function callComponentInDEV<Props, R>(
927 + Component: (p: Props, arg: void) => R,
928 + props: Props,
929 + componentDebugInfo: ReactComponentInfo,
930 +): R {
931 + // The secondArg is always undefined in Server Components since refs error early.
932 + const secondArg = undefined;
933 + setCurrentOwner(componentDebugInfo);
934 + try {
935 + if (supportsComponentStorage) {
936 + // Run the component in an Async Context that tracks the current owner.
937 + return componentStorage.run(
938 + componentDebugInfo,
939 + Component,
940 + props,
941 + secondArg,
942 + );
943 + } else {
944 + return Component(props, secondArg);
945 + }
946 + } finally {
947 + setCurrentOwner(null);
948 + }
949 +}
950 +
951 +// This indirect exists so we can exclude its stack frame in DEV (and anything below it).
952 +/** @noinline */
953 +function callLazyInitInDEV(lazy: LazyComponent<any, any>): any {
954 + const payload = lazy._payload;
955 + const init = lazy._init;
956 + return init(payload);
957 +}
958 +
959 function renderFunctionComponent<Props>(
960 request: Request,
961 task: Task,
962 key: null | string,
963 Component: (p: Props, arg: void) => any,
964 props: Props,
818 - owner: null | ReactComponentInfo,
965 + owner: null | ReactComponentInfo, // DEV-only
966 + stack: null | string, // DEV-only
967 ): ReactJSONValue {
968 // Reset the task's thenable state before continuing, so that if a later
969 // component suspends we can reuse the same task object. If the same
@@ -823,8 +971,6 @@ function renderFunctionComponent<Props>(
971 const prevThenableState = task.thenableState;
972 task.thenableState = null;
973
826 - // The secondArg is always undefined in Server Components since refs error early.
827 - const secondArg = undefined;
974 let result;
975
976 let componentDebugInfo: ReactComponentInfo;
@@ -850,6 +996,9 @@ function renderFunctionComponent<Props>(
996 env: request.environmentName,
997 owner: owner,
998 };
999 + if (enableOwnerStacks) {
1000 + (componentDebugInfo: any).stack = stack;
1001 + }
1002 // We outline this model eagerly so that we can refer to by reference as an owner.
1003 // If we had a smarter way to dedupe we might not have to do this if there ends up
1004 // being no references to this as an owner.
@@ -857,24 +1006,11 @@ function renderFunctionComponent<Props>(
1006 emitDebugChunk(request, componentDebugID, componentDebugInfo);
1007 }
1008 prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
860 - setCurrentOwner(componentDebugInfo);
861 - try {
862 - if (supportsComponentStorage) {
863 - // Run the component in an Async Context that tracks the current owner.
864 - result = componentStorage.run(
865 - componentDebugInfo,
866 - Component,
867 - props,
868 - secondArg,
869 - );
870 - } else {
871 - result = Component(props, secondArg);
872 - }
873 - } finally {
874 - setCurrentOwner(null);
875 - }
1009 + result = callComponentInDEV(Component, props, componentDebugInfo);
1010 } else {
1011 prepareToUseHooksForComponent(prevThenableState, null);
1012 + // The secondArg is always undefined in Server Components since refs error early.
1013 + const secondArg = undefined;
1014 result = Component(props, secondArg);
1015 }
1016 if (typeof result === 'object' && result !== null) {
@@ -1093,6 +1229,7 @@ function renderClientElement(
1229 key: null | string,
1230 props: any,
1231 owner: null | ReactComponentInfo, // DEV-only
1232 + stack: null | string, // DEV-only
1233 ): ReactJSONValue {
1234 // We prepend the terminal client element that actually gets serialized with
1235 // the keys of any Server Components which are not serialized.
@@ -1103,7 +1240,9 @@ function renderClientElement(
1240 key = keyPath + ',' + key;
1241 }
1242 const element = __DEV__
1106 - ? [REACT_ELEMENT_TYPE, type, key, props, owner]
1243 + ? enableOwnerStacks
1244 + ? [REACT_ELEMENT_TYPE, type, key, props, owner, stack]
1245 + : [REACT_ELEMENT_TYPE, type, key, props, owner]
1246 : [REACT_ELEMENT_TYPE, type, key, props];
1247 if (task.implicitSlot && key !== null) {
1248 // The root Server Component had no key so it was in an implicit slot.
@@ -1151,6 +1290,7 @@ function renderElement(
1290 ref: mixed,
1291 props: any,
1292 owner: null | ReactComponentInfo, // DEV only
1293 + stack: null | string, // DEV only
1294 ): ReactJSONValue {
1295 if (ref !== null && ref !== undefined) {
1296 // When the ref moves to the regular props object this will implicitly
@@ -1171,13 +1311,21 @@ function renderElement(
1311 if (typeof type === 'function') {
1312 if (isClientReference(type) || isTemporaryReference(type)) {
1313 // This is a reference to a Client Component.
1174 - return renderClientElement(task, type, key, props, owner);
1314 + return renderClientElement(task, type, key, props, owner, stack);
1315 }
1316 // This is a Server Component.
1177 - return renderFunctionComponent(request, task, key, type, props, owner);
1317 + return renderFunctionComponent(
1318 + request,
1319 + task,
1320 + key,
1321 + type,
1322 + props,
1323 + owner,
1324 + stack,
1325 + );
1326 } else if (typeof type === 'string') {
1327 // This is a host element. E.g. HTML.
1180 - return renderClientElement(task, type, key, props, owner);
1328 + return renderClientElement(task, type, key, props, owner, stack);
1329 } else if (typeof type === 'symbol') {
1330 if (type === REACT_FRAGMENT_TYPE && key === null) {
1331 // For key-less fragments, we add a small optimization to avoid serializing
@@ -1198,17 +1346,22 @@ function renderElement(
1346 }
1347 // This might be a built-in React component. We'll let the client decide.
1348 // Any built-in works as long as its props are serializable.
1201 - return renderClientElement(task, type, key, props, owner);
1349 + return renderClientElement(task, type, key, props, owner, stack);
1350 } else if (type != null && typeof type === 'object') {
1351 if (isClientReference(type)) {
1352 // This is a reference to a Client Component.
1205 - return renderClientElement(task, type, key, props, owner);
1353 + return renderClientElement(task, type, key, props, owner, stack);
1354 }
1355 switch (type.$$typeof) {
1356 case REACT_LAZY_TYPE: {
1209 - const payload = type._payload;
1210 - const init = type._init;
1211 - const wrappedType = init(payload);
1357 + let wrappedType;
1358 + if (__DEV__) {
1359 + wrappedType = callLazyInitInDEV(type);
1360 + } else {
1361 + const payload = type._payload;
1362 + const init = type._init;
1363 + wrappedType = init(payload);
1364 + }
1365 return renderElement(
1366 request,
1367 task,
@@ -1217,6 +1370,7 @@ function renderElement(
1370 ref,
1371 props,
1372 owner,
1373 + stack,
1374 );
1375 }
1376 case REACT_FORWARD_REF_TYPE: {
@@ -1227,10 +1381,20 @@ function renderElement(
1381 type.render,
1382 props,
1383 owner,
1384 + stack,
1385 );
1386 }
1387 case REACT_MEMO_TYPE: {
1233 - return renderElement(request, task, type.type, key, ref, props, owner);
1388 + return renderElement(
1389 + request,
1390 + task,
1391 + type.type,
1392 + key,
1393 + ref,
1394 + props,
1395 + owner,
1396 + stack,
1397 + );
1398 }
1399 }
1400 }
@@ -1822,6 +1986,9 @@ function renderModelDestructive(
1986 ref,
1987 props,
1988 __DEV__ ? element._owner : null,
1989 + __DEV__ && enableOwnerStacks
1990 + ? filterDebugStack(element._debugStack)
1991 + : null,
1992 );
1993 }
1994 case REACT_LAZY_TYPE: {
@@ -1830,9 +1997,14 @@ function renderModelDestructive(
1997 task.thenableState = null;
1998
1999 const lazy: LazyComponent<any, any> = (value: any);
1833 - const payload = lazy._payload;
1834 - const init = lazy._init;
1835 - const resolvedModel = init(payload);
2000 + let resolvedModel;
2001 + if (__DEV__) {
2002 + resolvedModel = callLazyInitInDEV(lazy);
2003 + } else {
2004 + const payload = lazy._payload;
2005 + const init = lazy._init;
2006 + resolvedModel = init(payload);
2007 + }
2008 if (__DEV__) {
2009 const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
2010 if (debugInfo) {
packages/react/src/jsx/ReactJSXElement.js
+74 -2
@@ -13,6 +13,7 @@ import {
13 getIteratorFn,
14 REACT_ELEMENT_TYPE,
15 REACT_FRAGMENT_TYPE,
16 + REACT_LAZY_TYPE,
17 } from 'shared/ReactSymbols';
18 import {checkKeyStringCoercion} from 'shared/CheckStringCoercion';
19 import isValidElementType from 'shared/isValidElementType';
@@ -23,6 +24,7 @@ import {
24 disableStringRefs,
25 disableDefaultPropsExceptForClasses,
26 enableFastJSX,
27 + enableOwnerStacks,
28 } from 'shared/ReactFeatureFlags';
29 import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
30 import {ClassComponent} from 'react-reconciler/src/ReactWorkTags';
@@ -30,6 +32,34 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom
32
33 const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference');
34
35 +const createTask =
36 + // eslint-disable-next-line react-internal/no-production-logging
37 + __DEV__ && enableOwnerStacks && console.createTask
38 + ? // eslint-disable-next-line react-internal/no-production-logging
39 + console.createTask
40 + : () => null;
41 +
42 +function getTaskName(type) {
43 + if (type === REACT_FRAGMENT_TYPE) {
44 + return '<>';
45 + }
46 + if (
47 + typeof type === 'object' &&
48 + type !== null &&
49 + type.$$typeof === REACT_LAZY_TYPE
50 + ) {
51 + // We don't want to eagerly initialize the initializer in DEV mode so we can't
52 + // call it to extract the type so we don't know the type of this component.
53 + return '<...>';
54 + }
55 + try {
56 + const name = getComponentNameFromType(type);
57 + return name ? '<' + name + '>' : '<...>';
58 + } catch (x) {
59 + return '<...>';
60 + }
61 +}
62 +
63 function getOwner() {
64 if (__DEV__ || !disableStringRefs) {
65 const dispatcher = ReactSharedInternals.A;
@@ -194,7 +224,17 @@ function elementRefGetterWithDeprecationWarning() {
224 * indicating filename, line number, and/or other information.
225 * @internal
226 */
197 -function ReactElement(type, key, _ref, self, source, owner, props) {
227 +function ReactElement(
228 + type,
229 + key,
230 + _ref,
231 + self,
232 + source,
233 + owner,
234 + props,
235 + debugStack,
236 + debugTask,
237 +) {
238 let ref;
239 if (enableRefAsProp) {
240 // When enableRefAsProp is on, ignore whatever was passed as the ref
@@ -311,6 +351,20 @@ function ReactElement(type, key, _ref, self, source, owner, props) {
351 writable: true,
352 value: null,
353 });
354 + if (enableOwnerStacks) {
355 + Object.defineProperty(element, '_debugStack', {
356 + configurable: false,
357 + enumerable: false,
358 + writable: true,
359 + value: debugStack,
360 + });
361 + Object.defineProperty(element, '_debugTask', {
362 + configurable: false,
363 + enumerable: false,
364 + writable: true,
365 + value: debugTask,
366 + });
367 + }
368 if (Object.freeze) {
369 Object.freeze(element.props);
370 Object.freeze(element);
@@ -404,7 +458,17 @@ export function jsxProd(type, config, maybeKey) {
458 }
459 }
460
407 - return ReactElement(type, key, ref, undefined, undefined, getOwner(), props);
461 + return ReactElement(
462 + type,
463 + key,
464 + ref,
465 + undefined,
466 + undefined,
467 + getOwner(),
468 + props,
469 + undefined,
470 + undefined,
471 + );
472 }
473
474 // While `jsxDEV` should never be called when running in production, we do
@@ -652,6 +716,8 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
716 source,
717 getOwner(),
718 props,
719 + __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
720 + __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
721 );
722
723 if (type === REACT_FRAGMENT_TYPE) {
@@ -842,6 +908,8 @@ export function createElement(type, config, children) {
908 undefined,
909 getOwner(),
910 props,
911 + __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
912 + __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
913 );
914
915 if (type === REACT_FRAGMENT_TYPE) {
@@ -862,6 +930,8 @@ export function cloneAndReplaceKey(oldElement, newKey) {
930 undefined,
931 !__DEV__ && disableStringRefs ? undefined : oldElement._owner,
932 oldElement.props,
933 + __DEV__ && enableOwnerStacks ? oldElement._debugStack : undefined,
934 + __DEV__ && enableOwnerStacks ? oldElement._debugTask : undefined,
935 );
936 }
937
@@ -973,6 +1043,8 @@ export function cloneElement(element, config, children) {
1043 undefined,
1044 owner,
1045 props,
1046 + __DEV__ && enableOwnerStacks ? element._debugStack : undefined,
1047 + __DEV__ && enableOwnerStacks ? element._debugTask : undefined,
1048 );
1049
1050 for (let i = 2; i < arguments.length; i++) {
packages/shared/ReactElementType.js
+9
@@ -7,6 +7,12 @@
7 * @flow
8 */
9
10 +import type {ReactDebugInfo} from './ReactTypes';
11 +
12 +interface ConsoleTask {
13 + run<T>(f: () => T): T;
14 +}
15 +
16 export type ReactElement = {
17 $$typeof: any,
18 type: any,
@@ -18,4 +24,7 @@ export type ReactElement = {
24
25 // __DEV__
26 _store: {validated: boolean, ...},
27 + _debugInfo: null | ReactDebugInfo,
28 + _debugStack: Error,
29 + _debugTask: null | ConsoleTask,
30 };
packages/shared/ReactFeatureFlags.js
+2
@@ -125,6 +125,8 @@ export const enableEarlyReturnForPropDiffing = false;
125
126 export const enableAddPropertiesFastPath = false;
127
128 +export const enableOwnerStacks = __EXPERIMENTAL__;
129 +
130 /**
131 * Enables an expiration time for retry lanes to avoid starvation.
132 */
packages/shared/ReactTypes.js
+1
@@ -182,6 +182,7 @@ export type ReactComponentInfo = {
182 +name?: string,
183 +env?: string,
184 +owner?: null | ReactComponentInfo,
185 + +stack?: null | string,
186 };
187
188 export type ReactAsyncInfo = {
packages/shared/forks/ReactFeatureFlags.native-fb.js
+2
@@ -100,5 +100,7 @@ export const enableReactTestRendererWarning = false;
100 export const disableLegacyMode = false;
101 export const disableDOMTestUtils = false;
102
103 +export const enableOwnerStacks = false;
104 +
105 // Flow magic to verify the exports of this file match the original version.
106 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -107,6 +107,8 @@ export const enableAddPropertiesFastPath = false;
107
108 export const renameElementSymbol = true;
109
110 +export const enableOwnerStacks = __EXPERIMENTAL__;
111 +
112 // Profiling Only
113 export const enableProfilerTimer = __PROFILE__;
114 export const enableProfilerCommitHooks = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+2
@@ -98,5 +98,7 @@ export const enableRenderableContext = true;
98 export const enableReactTestRendererWarning = true;
99 export const disableDefaultPropsExceptForClasses = true;
100
101 +export const enableOwnerStacks = false;
102 +
103 // Flow magic to verify the exports of this file match the original version.
104 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+2
@@ -93,5 +93,7 @@ export const enableAddPropertiesFastPath = false;
93
94 export const renameElementSymbol = false;
95
96 +export const enableOwnerStacks = false;
97 +
98 // Flow magic to verify the exports of this file match the original version.
99 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -93,5 +93,7 @@ export const enableAddPropertiesFastPath = false;
93
94 export const renameElementSymbol = false;
95
96 +export const enableOwnerStacks = false;
97 +
98 // Flow magic to verify the exports of this file match the original version.
99 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -123,5 +123,7 @@ export const disableLegacyMode = __EXPERIMENTAL__;
123 export const disableDOMTestUtils = false;
124 export const enableEarlyReturnForPropDiffing = false;
125
126 +export const enableOwnerStacks = false;
127 +
128 // Flow magic to verify the exports of this file match the original version.
129 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
scripts/error-codes/transform-error-messages.js
+5
@@ -49,6 +49,11 @@ module.exports = function (babel) {
49 errorMsgExpressions
50 );
51
52 + if (errorMsgLiteral === 'react-stack-top-frame') {
53 + // This is a special case for generating stack traces.
54 + return;
55 + }
56 +
57 let prodErrorId = errorMap[errorMsgLiteral];
58 if (prodErrorId === undefined) {
59 // There is no error code for this message. Add an inline comment
scripts/eslint-rules/prod-error-codes.js
+4
@@ -50,6 +50,10 @@ module.exports = {
50 return;
51 }
52 const errorMessage = nodeToErrorTemplate(errorMessageNode);
53 + if (errorMessage === 'react-stack-top-frame') {
54 + // This is a special case for generating stack traces.
55 + return;
56 + }
57 if (errorMessages.has(errorMessage)) {
58 return;
59 }
scripts/jest/setupTests.js
+6 -1
@@ -292,9 +292,14 @@ function lazyRequireFunctionExports(moduleName) {
292 // If this export is a function, return a wrapper function that lazily
293 // requires the implementation from the current module cache.
294 if (typeof originalModule[prop] === 'function') {
295 - return function () {
295 + const wrapper = function () {
296 return jest.requireActual(moduleName)[prop].apply(this, arguments);
297 };
298 + // We use this to trick the filtering of Flight to exclude this frame.
299 + Object.defineProperty(wrapper, 'name', {
300 + value: '(<anonymous>)',
301 + });
302 + return wrapper;
303 } else {
304 return originalModule[prop];
305 }