Skip to content

Commit 98d410f

Browse files
authored
Build Component Stacks from Native Stack Frames (#18561)
* Implement component stack extraction hack * Normalize errors in tests This drops the requirement to include owner to pass the test. * Special case tests * Add destructuring to force toObject which throws before the side-effects This ensures that we don't double call yieldValue or advanceTime in tests. Ideally we could use empty destructuring but ES lint doesn't like it. * Cache the result in DEV In DEV it's somewhat likely that we'll see many logs that add component stacks. This could be slow so we cache the results of previous components. * Fixture * Add Reflect to lint * Log if out of range. * Fix special case when the function call throws in V8 In V8 we need to ignore the first line. Normally we would never get there because the stacks would differ before that, but the stacks are the same if we end up throwing at the same place as the control.
1 parent 8e13f09 commit 98d410f

35 files changed

+594
-179
lines changed

fixtures/stacks/BabelClass-compiled.js

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fixtures/stacks/BabelClass-compiled.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fixtures/stacks/BabelClass.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Compile this with Babel.
2+
// babel --config-file ./babel.config.json BabelClass.js --out-file BabelClass-compiled.js --source-maps
3+
4+
class BabelClass extends React.Component {
5+
render() {
6+
return this.props.children;
7+
}
8+
}

fixtures/stacks/Component.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Example
2+
3+
const Throw = React.lazy(() => {
4+
throw new Error('Example');
5+
});
6+
7+
const Component = React.memo(function Component({children}) {
8+
return children;
9+
});
10+
11+
function DisplayName({children}) {
12+
return children;
13+
}
14+
DisplayName.displayName = 'Custom Name';
15+
16+
class NativeClass extends React.Component {
17+
render() {
18+
return this.props.children;
19+
}
20+
}

fixtures/stacks/Example.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Example
2+
3+
const x = React.createElement;
4+
5+
class ErrorBoundary extends React.Component {
6+
static getDerivedStateFromError(error) {
7+
return {
8+
error: error,
9+
};
10+
}
11+
12+
componentDidCatch(error, errorInfo) {
13+
console.log(error.message, errorInfo.componentStack);
14+
this.setState({
15+
componentStack: errorInfo.componentStack,
16+
});
17+
}
18+
19+
render() {
20+
if (this.state && this.state.error) {
21+
return x(
22+
'div',
23+
null,
24+
x('h3', null, this.state.error.message),
25+
x('pre', null, this.state.componentStack)
26+
);
27+
}
28+
return this.props.children;
29+
}
30+
}
31+
32+
function Example() {
33+
let state = React.useState(false);
34+
return x(
35+
ErrorBoundary,
36+
null,
37+
x(
38+
DisplayName,
39+
null,
40+
x(
41+
React.SuspenseList,
42+
null,
43+
x(
44+
NativeClass,
45+
null,
46+
x(
47+
BabelClass,
48+
null,
49+
x(
50+
React.Suspense,
51+
null,
52+
x('div', null, x(Component, null, x(Throw)))
53+
)
54+
)
55+
)
56+
)
57+
)
58+
);
59+
}

fixtures/stacks/babel.config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"plugins": [
3+
["@babel/plugin-transform-classes", {"loose": true}]
4+
]
5+
}

fixtures/stacks/index.html

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Component Stacks</title>
6+
<style>
7+
html, body {
8+
margin: 20px;
9+
}
10+
pre {
11+
background: #eee;
12+
border: 1px solid #ccc;
13+
padding: 2px;
14+
}
15+
</style>
16+
</head>
17+
<body>
18+
<div id="container">
19+
<p>
20+
To install React, follow the instructions on
21+
<a href="https://github.com/facebook/react/">GitHub</a>.
22+
</p>
23+
<p>
24+
If you can see this, React is <strong>not</strong> working right.
25+
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
26+
</p>
27+
</div>
28+
<script src="../../build/node_modules/react/umd/react.production.min.js"></script>
29+
<script src="../../build/node_modules/react-dom/umd/react-dom.production.min.js"></script>
30+
<script src="./Component.js"></script>
31+
<script src="./BabelClass-compiled.js"></script>
32+
<script src="./Example.js"></script>
33+
<script>
34+
const container = document.getElementById("container");
35+
ReactDOM.render(React.createElement(Example), container);
36+
</script>
37+
<h3>The above stack should look something like this:</h3>
38+
<pre>
39+
40+
at Lazy
41+
at Component (/stacks/Component.js:7:50)
42+
at div
43+
at Suspense
44+
at BabelClass (/stacks/BabelClass-compiled.js:13:29)
45+
at NativeClass (/stacks/Component.js:16:1)
46+
at SuspenseList
47+
at Custom Name (/stacks/Component.js:11:23)
48+
at ErrorBoundary (/stacks/Example.js:5:1)
49+
at Example (/stacks/Example.js:33:21)</pre>
50+
</body>
51+
</html>

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ describe('console', () => {
6161
});
6262

6363
function normalizeCodeLocInfo(str) {
64-
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
64+
return (
65+
str &&
66+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
67+
return '\n in ' + name + ' (at **)';
68+
})
69+
);
6570
}
6671

6772
it('should not patch console methods that do not receive component stacks', () => {

packages/react-dom/src/__tests__/ReactComponent-test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ let ReactTestUtils;
1616

1717
describe('ReactComponent', () => {
1818
function normalizeCodeLocInfo(str) {
19-
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
19+
return (
20+
str &&
21+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
22+
return '\n in ' + name + ' (at **)';
23+
})
24+
);
2025
}
2126

2227
beforeEach(() => {

packages/react-dom/src/__tests__/ReactDOMComponent-test.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ describe('ReactDOMComponent', () => {
1717
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
1818

1919
function normalizeCodeLocInfo(str) {
20-
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
20+
return (
21+
str &&
22+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
23+
return '\n in ' + name + ' (at **)';
24+
})
25+
);
2126
}
2227

2328
beforeEach(() => {
@@ -1719,16 +1724,26 @@ describe('ReactDOMComponent', () => {
17191724
<tr />
17201725
</div>,
17211726
);
1722-
}).toErrorDev([
1723-
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1724-
'<div>.' +
1725-
'\n in tr (at **)' +
1726-
'\n in div (at **)',
1727-
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1728-
'<div>.' +
1729-
'\n in tr (at **)' +
1730-
'\n in div (at **)',
1731-
]);
1727+
}).toErrorDev(
1728+
ReactFeatureFlags.enableComponentStackLocations
1729+
? [
1730+
// This warning dedupes since they're in the same component.
1731+
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1732+
'<div>.' +
1733+
'\n in tr (at **)' +
1734+
'\n in div (at **)',
1735+
]
1736+
: [
1737+
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1738+
'<div>.' +
1739+
'\n in tr (at **)' +
1740+
'\n in div (at **)',
1741+
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1742+
'<div>.' +
1743+
'\n in tr (at **)' +
1744+
'\n in div (at **)',
1745+
],
1746+
);
17321747
});
17331748

17341749
it('warns on invalid nesting at root', () => {

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ describe('ReactDOMServerHooks', () => {
10971097

10981098
it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => {
10991099
let _setShow;
1100-
function App() {
1100+
function App({unused}) {
11011101
Scheduler.unstable_yieldValue('App');
11021102
const id = useOpaqueIdentifier();
11031103
const [show, setShow] = useState(false);
@@ -1129,7 +1129,7 @@ describe('ReactDOMServerHooks', () => {
11291129

11301130
it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => {
11311131
let _setShow;
1132-
function App() {
1132+
function App({unused}) {
11331133
Scheduler.unstable_yieldValue('App');
11341134
const id = useOpaqueIdentifier();
11351135
const [show, setShow] = useState(false);

packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -497,18 +497,18 @@ describe('ReactErrorBoundaries', () => {
497497
}
498498
};
499499

500-
BrokenUseEffect = props => {
500+
BrokenUseEffect = ({children}) => {
501501
Scheduler.unstable_yieldValue('BrokenUseEffect render');
502502

503503
React.useEffect(() => {
504504
Scheduler.unstable_yieldValue('BrokenUseEffect useEffect [!]');
505505
throw new Error('Hello');
506506
});
507507

508-
return props.children;
508+
return children;
509509
};
510510

511-
BrokenUseLayoutEffect = props => {
511+
BrokenUseLayoutEffect = ({children}) => {
512512
Scheduler.unstable_yieldValue('BrokenUseLayoutEffect render');
513513

514514
React.useLayoutEffect(() => {
@@ -518,7 +518,7 @@ describe('ReactErrorBoundaries', () => {
518518
throw new Error('Hello');
519519
});
520520

521-
return props.children;
521+
return children;
522522
};
523523

524524
NoopErrorBoundary = class extends React.Component {

packages/react-dom/src/__tests__/ReactServerRendering-test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ const enableSuspenseServerRenderer = require('shared/ReactFeatureFlags')
1818
.enableSuspenseServerRenderer;
1919

2020
function normalizeCodeLocInfo(str) {
21-
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
21+
return (
22+
str &&
23+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
24+
return '\n in ' + name + ' (at **)';
25+
})
26+
);
2227
}
2328

2429
describe('ReactDOMServer', () => {

packages/react-dom/src/__tests__/ReactUpdates-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1615,7 +1615,7 @@ describe('ReactUpdates', () => {
16151615
Scheduler.unstable_clearYields();
16161616
}
16171617
expect(error).toContain('Warning: Maximum update depth exceeded.');
1618-
expect(stack).toContain('in NonTerminating');
1618+
expect(stack).toContain(' NonTerminating');
16191619
// rethrow error to prevent going into an infinite loop when act() exits
16201620
throw error;
16211621
});

packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ let createReactNativeComponentClass;
1616
let computeComponentStackForErrorReporting;
1717

1818
function normalizeCodeLocInfo(str) {
19-
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
19+
return (
20+
str &&
21+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
22+
return '\n in ' + name + ' (at **)';
23+
})
24+
);
2025
}
2126

2227
describe('ReactNativeError', () => {
@@ -74,20 +79,11 @@ describe('ReactNativeError', () => {
7479
computeComponentStackForErrorReporting(reactTag),
7580
);
7681

77-
if (__DEV__) {
78-
expect(componentStack).toBe(
79-
'\n' +
80-
' in View (at **)\n' +
81-
' in FunctionComponent (at **)\n' +
82-
' in ClassComponent (at **)',
83-
);
84-
} else {
85-
expect(componentStack).toBe(
86-
'\n' +
87-
' in View\n' +
88-
' in FunctionComponent\n' +
89-
' in ClassComponent',
90-
);
91-
}
82+
expect(componentStack).toBe(
83+
'\n' +
84+
' in View (at **)\n' +
85+
' in FunctionComponent (at **)\n' +
86+
' in ClassComponent (at **)',
87+
);
9288
});
9389
});

0 commit comments

Comments
 (0)