Skip to content

Commit d64c7fe

Browse files
committed
Track stacks in JSX
We track the debug stack using an error. We don't eagerly access the stack to allow it to be lazily generated from the internals. This will be used for passing RSC stacks to the client as well as for user space display. If available, we also track console.createTask separately. This will be used for native stack traces in the native DevTools.
1 parent a2470c8 commit d64c7fe

File tree

9 files changed

+439
-67
lines changed

9 files changed

+439
-67
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
enablePostpone,
4444
enableRefAsProp,
4545
enableFlightReadableStream,
46+
enableOwnerStacks,
4647
} from 'shared/ReactFeatureFlags';
4748

4849
import {
@@ -563,6 +564,7 @@ function createElement(
563564
key: mixed,
564565
props: mixed,
565566
owner: null | ReactComponentInfo, // DEV-only
567+
stack: null | string, // DEV-only
566568
): React$Element<any> {
567569
let element: any;
568570
if (__DEV__ && enableRefAsProp) {
@@ -623,6 +625,23 @@ function createElement(
623625
writable: true,
624626
value: null,
625627
});
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);
626645
}
627646
return element;
628647
}
@@ -1003,6 +1022,7 @@ function parseModelTuple(
10031022
tuple[2],
10041023
tuple[3],
10051024
__DEV__ ? (tuple: any)[4] : null,
1025+
__DEV__ && enableOwnerStacks ? (tuple: any)[5] : null,
10061026
);
10071027
}
10081028
return value;

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 127 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,24 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') {
2121
function normalizeCodeLocInfo(str) {
2222
return (
2323
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 **)' : '');
2626
})
2727
);
2828
}
2929

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+
3042
const heldValues = [];
3143
let finalizationCallback;
3244
function FinalizationRegistryMock(callback) {
@@ -221,8 +233,19 @@ describe('ReactFlight', () => {
221233
await act(async () => {
222234
const rootModel = await ReactNoopFlightClient.read(transport);
223235
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,
226249
);
227250
ReactNoop.render(greeting);
228251
});
@@ -248,8 +271,19 @@ describe('ReactFlight', () => {
248271

249272
await act(async () => {
250273
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,
253287
);
254288
ReactNoop.render(await promise);
255289
});
@@ -2233,9 +2267,11 @@ describe('ReactFlight', () => {
22332267
return <span>!</span>;
22342268
}
22352269

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+
});
22392275

22402276
function ThirdPartyComponent() {
22412277
return <span>stranger</span>;
@@ -2269,31 +2305,61 @@ describe('ReactFlight', () => {
22692305

22702306
await act(async () => {
22712307
const promise = ReactNoopFlightClient.read(transport);
2272-
expect(promise._debugInfo).toEqual(
2308+
expect(getDebugInfo(promise)).toEqual(
22732309
__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+
]
22752320
: undefined,
22762321
);
22772322
const result = await promise;
22782323
const thirdPartyChildren = await result.props.children[1];
22792324
// 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(
22812326
__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+
]
22832337
: undefined,
22842338
);
2285-
expect(thirdPartyChildren[1]._debugInfo).toEqual(
2339+
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
22862340
__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+
]
22882351
: undefined,
22892352
);
2290-
expect(thirdPartyChildren[2]._debugInfo).toEqual(
2353+
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
22912354
__DEV__
22922355
? [
22932356
{
22942357
name: 'ThirdPartyFragmentComponent',
22952358
env: 'third-party',
22962359
owner: null,
2360+
stack: gate(flag => flag.enableOwnerStacks)
2361+
? ' in Object.<anonymous> (at **)'
2362+
: undefined,
22972363
},
22982364
]
22992365
: undefined,
@@ -2357,24 +2423,47 @@ describe('ReactFlight', () => {
23572423

23582424
await act(async () => {
23592425
const promise = ReactNoopFlightClient.read(transport);
2360-
expect(promise._debugInfo).toEqual(
2426+
expect(getDebugInfo(promise)).toEqual(
23612427
__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+
]
23632438
: undefined,
23642439
);
23652440
const result = await promise;
23662441
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,
23692455
);
23702456
// 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(
23722458
__DEV__
23732459
? [
23742460
{
23752461
name: 'ThirdPartyAsyncIterableComponent',
23762462
env: 'third-party',
23772463
owner: null,
2464+
stack: gate(flag => flag.enableOwnerStacks)
2465+
? ' in Object.<anonymous> (at **)'
2466+
: undefined,
23782467
},
23792468
]
23802469
: undefined,
@@ -2467,10 +2556,24 @@ describe('ReactFlight', () => {
24672556
// We've rendered down to the span.
24682557
expect(greeting.type).toBe('span');
24692558
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([
24722568
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+
},
24742577
]);
24752578
// The owner that created the span was the outer server component.
24762579
// We expect the debug info to be referentially equal to the owner.

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ describe('ReactFlightDOMEdge', () => {
296296
const [stream1, stream2] = passThrough(stream).tee();
297297

298298
const serializedContent = await readResult(stream1);
299-
expect(serializedContent.length).toBeLessThan(400);
299+
expect(serializedContent.length).toBeLessThan(__DEV__ ? 470 : 400);
300300
expect(timesRendered).toBeLessThan(5);
301301

302302
const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
@@ -324,7 +324,7 @@ describe('ReactFlightDOMEdge', () => {
324324
<ServerComponent recurse={20} />,
325325
);
326326
const serializedContent = await readResult(stream);
327-
const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0;
327+
const expectedDebugInfoSize = __DEV__ ? 100 * 20 : 0;
328328
expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
329329
});
330330

0 commit comments

Comments
 (0)