Skip to content

Commit 43dac1e

Browse files
authored
[DevTools] Implement Owner Stacks (facebook#30417)
Stacked on facebook#30410. Use "owner stacks" as the appended component stack if it is available on the Fiber. This will only be available if the enableOwnerStacks flag is on. Otherwise it fallback to parent stacks. In prod, there's no owner so it's never added there. I was going back and forth on whether to inject essentially `captureOwnerStack` as part of the DevTools hooks or replicate the implementation but decided to replicate the implementation. The DevTools needs all the same information from internals to implement owner views elsewhere in the UI anyway so we're not saving anything in terms of the scope of internals. Additionally, we really need this information for non-current components as well like "rendered by" views of the currently selected component. It can also be useful if we need to change the format after the fact like we did for parent stacks in: facebook#30289 Injecting the implementation would lock us into specifics both in terms of what the core needs to provide and what the DevTools can use. The implementation depends on the technique used in facebook#30369 which tags frames to strip out with `react-stack-bottom-frame`. That's how the implementation knows how to materialize the error if it hasn't already. Firefox: <img width="487" alt="Screenshot 2024-07-21 at 11 33 37 PM" src="https://github.com/user-attachments/assets/d3539b53-4578-4fdd-af25-25698b2bcc7d"> Follow up: One thing about this view is that it doesn't include the current actual synchronous stack. When I used to append these I would include both the real current stack and the owner stack. That's because the owner stack doesn't include the name of the currently executing component. I'll probably inject the current stack too in addition to the owner stack. This is similar to how native Async Stacks are basically just appended onto the current stack rather than its own.
1 parent b7e7f1a commit 43dac1e

File tree

8 files changed

+1697
-1477
lines changed

8 files changed

+1697
-1477
lines changed

packages/react-devtools-extensions/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,6 @@
6767
"workerize-loader": "^2.0.2"
6868
},
6969
"dependencies": {
70-
"web-ext": "^4"
70+
"web-ext": "^8"
7171
}
7272
}

packages/react-devtools-shared/src/__tests__/componentStacks-test.js

+21-10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('component stack', () => {
1414
let act;
1515
let mockError;
1616
let mockWarn;
17+
let supportsOwnerStacks;
1718

1819
beforeEach(() => {
1920
// Intercept native console methods before DevTools bootstraps.
@@ -31,6 +32,12 @@ describe('component stack', () => {
3132
act = utils.act;
3233

3334
React = require('react');
35+
if (
36+
React.version.startsWith('19') &&
37+
React.version.includes('experimental')
38+
) {
39+
supportsOwnerStacks = true;
40+
}
3441
});
3542

3643
const {render} = getVersionedRenderImplementation();
@@ -49,13 +56,13 @@ describe('component stack', () => {
4956

5057
expect(mockError).toHaveBeenCalledWith(
5158
'Test error.',
52-
'\n in Child (at **)' +
59+
(supportsOwnerStacks ? '' : '\n in Child (at **)') +
5360
'\n in Parent (at **)' +
5461
'\n in Grandparent (at **)',
5562
);
5663
expect(mockWarn).toHaveBeenCalledWith(
5764
'Test warning.',
58-
'\n in Child (at **)' +
65+
(supportsOwnerStacks ? '' : '\n in Child (at **)') +
5966
'\n in Parent (at **)' +
6067
'\n in Grandparent (at **)',
6168
);
@@ -112,17 +119,21 @@ describe('component stack', () => {
112119

113120
expect(mockError).toHaveBeenCalledWith(
114121
'Test error.',
115-
'\n in Child (at **)' +
116-
'\n in ServerComponent (at **)' +
117-
'\n in Parent (at **)' +
118-
'\n in Grandparent (at **)',
122+
supportsOwnerStacks
123+
? '\n in Child (at **)'
124+
: '\n in Child (at **)' +
125+
'\n in ServerComponent (at **)' +
126+
'\n in Parent (at **)' +
127+
'\n in Grandparent (at **)',
119128
);
120129
expect(mockWarn).toHaveBeenCalledWith(
121130
'Test warning.',
122-
'\n in Child (at **)' +
123-
'\n in ServerComponent (at **)' +
124-
'\n in Parent (at **)' +
125-
'\n in Grandparent (at **)',
131+
supportsOwnerStacks
132+
? '\n in Child (at **)'
133+
: '\n in Child (at **)' +
134+
'\n in ServerComponent (at **)' +
135+
'\n in Parent (at **)' +
136+
'\n in Grandparent (at **)',
126137
);
127138
});
128139
});

packages/react-devtools-shared/src/__tests__/console-test.js

+61-18
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let mockWarn;
2222
let patchConsole;
2323
let unpatchConsole;
2424
let rendererID;
25+
let supportsOwnerStacks = false;
2526

2627
describe('console', () => {
2728
beforeEach(() => {
@@ -62,6 +63,12 @@ describe('console', () => {
6263
};
6364

6465
React = require('react');
66+
if (
67+
React.version.startsWith('19') &&
68+
React.version.includes('experimental')
69+
) {
70+
supportsOwnerStacks = true;
71+
}
6572
ReactDOMClient = require('react-dom/client');
6673

6774
const utils = require('./utils');
@@ -224,13 +231,17 @@ describe('console', () => {
224231
expect(mockWarn.mock.calls[0]).toHaveLength(2);
225232
expect(mockWarn.mock.calls[0][0]).toBe('warn');
226233
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
227-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
234+
supportsOwnerStacks
235+
? '\n in Parent (at **)'
236+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
228237
);
229238
expect(mockError).toHaveBeenCalledTimes(1);
230239
expect(mockError.mock.calls[0]).toHaveLength(2);
231240
expect(mockError.mock.calls[0][0]).toBe('error');
232241
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
233-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
242+
supportsOwnerStacks
243+
? '\n in Parent (at **)'
244+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
234245
);
235246
});
236247

@@ -267,23 +278,31 @@ describe('console', () => {
267278
expect(mockWarn.mock.calls[0]).toHaveLength(2);
268279
expect(mockWarn.mock.calls[0][0]).toBe('active warn');
269280
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
270-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
281+
supportsOwnerStacks
282+
? '\n in Parent (at **)'
283+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
271284
);
272285
expect(mockWarn.mock.calls[1]).toHaveLength(2);
273286
expect(mockWarn.mock.calls[1][0]).toBe('passive warn');
274287
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
275-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
288+
supportsOwnerStacks
289+
? '\n in Parent (at **)'
290+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
276291
);
277292
expect(mockError).toHaveBeenCalledTimes(2);
278293
expect(mockError.mock.calls[0]).toHaveLength(2);
279294
expect(mockError.mock.calls[0][0]).toBe('active error');
280295
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
281-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
296+
supportsOwnerStacks
297+
? '\n in Parent (at **)'
298+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
282299
);
283300
expect(mockError.mock.calls[1]).toHaveLength(2);
284301
expect(mockError.mock.calls[1][0]).toBe('passive error');
285302
expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
286-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
303+
supportsOwnerStacks
304+
? '\n in Parent (at **)'
305+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
287306
);
288307
});
289308

@@ -325,23 +344,31 @@ describe('console', () => {
325344
expect(mockWarn.mock.calls[0]).toHaveLength(2);
326345
expect(mockWarn.mock.calls[0][0]).toBe('didMount warn');
327346
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
328-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
347+
supportsOwnerStacks
348+
? '\n in Parent (at **)'
349+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
329350
);
330351
expect(mockWarn.mock.calls[1]).toHaveLength(2);
331352
expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn');
332353
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
333-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
354+
supportsOwnerStacks
355+
? '\n in Parent (at **)'
356+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
334357
);
335358
expect(mockError).toHaveBeenCalledTimes(2);
336359
expect(mockError.mock.calls[0]).toHaveLength(2);
337360
expect(mockError.mock.calls[0][0]).toBe('didMount error');
338361
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
339-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
362+
supportsOwnerStacks
363+
? '\n in Parent (at **)'
364+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
340365
);
341366
expect(mockError.mock.calls[1]).toHaveLength(2);
342367
expect(mockError.mock.calls[1][0]).toBe('didUpdate error');
343368
expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
344-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
369+
supportsOwnerStacks
370+
? '\n in Parent (at **)'
371+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
345372
);
346373
});
347374

@@ -375,13 +402,17 @@ describe('console', () => {
375402
expect(mockWarn.mock.calls[0]).toHaveLength(2);
376403
expect(mockWarn.mock.calls[0][0]).toBe('warn');
377404
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
378-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
405+
supportsOwnerStacks
406+
? '\n in Parent (at **)'
407+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
379408
);
380409
expect(mockError).toHaveBeenCalledTimes(1);
381410
expect(mockError.mock.calls[0]).toHaveLength(2);
382411
expect(mockError.mock.calls[0][0]).toBe('error');
383412
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
384-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
413+
supportsOwnerStacks
414+
? '\n in Parent (at **)'
415+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
385416
);
386417
});
387418

@@ -465,13 +496,17 @@ describe('console', () => {
465496
expect(mockWarn.mock.calls[0]).toHaveLength(2);
466497
expect(mockWarn.mock.calls[0][0]).toBe('warn');
467498
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
468-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
499+
supportsOwnerStacks
500+
? '\n in Parent (at **)'
501+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
469502
);
470503
expect(mockError).toHaveBeenCalledTimes(1);
471504
expect(mockError.mock.calls[0]).toHaveLength(2);
472505
expect(mockError.mock.calls[0][0]).toBe('error');
473506
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
474-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
507+
supportsOwnerStacks
508+
? '\n in Parent (at **)'
509+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
475510
);
476511
});
477512

@@ -996,29 +1031,37 @@ describe('console', () => {
9961031
expect(mockWarn).toHaveBeenCalledTimes(2);
9971032
expect(mockWarn.mock.calls[0]).toHaveLength(2);
9981033
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
999-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1034+
supportsOwnerStacks
1035+
? '\n in Parent (at **)'
1036+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
10001037
);
10011038
expect(mockWarn.mock.calls[1]).toHaveLength(3);
10021039
expect(mockWarn.mock.calls[1][0]).toEqual(
10031040
'\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
10041041
);
10051042
expect(mockWarn.mock.calls[1][1]).toMatch('warn');
10061043
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual(
1007-
'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1044+
supportsOwnerStacks
1045+
? 'in Parent (at **)'
1046+
: 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
10081047
);
10091048

10101049
expect(mockError).toHaveBeenCalledTimes(2);
10111050
expect(mockError.mock.calls[0]).toHaveLength(2);
10121051
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toEqual(
1013-
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1052+
supportsOwnerStacks
1053+
? '\n in Parent (at **)'
1054+
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
10141055
);
10151056
expect(mockError.mock.calls[1]).toHaveLength(3);
10161057
expect(mockError.mock.calls[1][0]).toEqual(
10171058
'\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
10181059
);
10191060
expect(mockError.mock.calls[1][1]).toEqual('error');
10201061
expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual(
1021-
'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1062+
supportsOwnerStacks
1063+
? 'in Parent (at **)'
1064+
: 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
10221065
);
10231066
});
10241067
});

0 commit comments

Comments
 (0)