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', () => {

0 commit comments

Comments
 (0)