Stop filtering owner stacks (#30438)
We still filter them before passing from server to client in Flight Server but when presenting a native stack, we don't need to filter them. That's left to ignore listing in the presentation. The stacks are pretty clean regardless thanks to the bottom stack frames. We can also unify the owner stack formatters into one shared module since Fizz/Flight/Fiber all do the same thing. DevTools currently does the same thing but is forked so it can support multiple versions.
Sebastian Markbåge committed
Jul 24, 2024 at 13:01 UTC
5b37af7daa896ef21eec2db2fe9bc8ad42a1a328
11 files changed
+128
-212
packages/react-devtools-shared/src/backend/DevToolsOwnerStack.js
+9
-20
@@ -7,20 +7,9 @@
7
* @flow
8
*/
9
10
-// This is a DevTools fork of ReactFiberOwnerStack.
10
+// This is a DevTools fork of shared/ReactOwnerStackFrames.
11
12
-// TODO: Make this configurable?
13
-const externalRegExp = /\/node\_modules\/|\(\<anonymous\>/;
14
-
15
-function isNotExternal(stackFrame: string): boolean {
16
- return !externalRegExp.test(stackFrame);
17
-}
18
-
19
-function filterDebugStack(error: Error): string {
20
- // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
21
- // to save bandwidth even in DEV. We'll also replay these stacks on the client so by
22
- // stripping them early we avoid that overhead. Otherwise we'd normally just rely on
23
- // the DevTools or framework's ignore lists to filter them out.
12
+export function formatOwnerStack(error: Error): string {
13
const prevPrepareStackTrace = Error.prepareStackTrace;
14
// $FlowFixMe[incompatible-type] It does accept undefined.
15
Error.prepareStackTrace = undefined;
@@ -31,7 +20,12 @@ function filterDebugStack(error: Error): string {
20
// don't want/need.
21
stack = stack.slice(29);
22
}
34
- let idx = stack.indexOf('react-stack-bottom-frame');
23
+ let idx = stack.indexOf('\n');
24
+ if (idx !== -1) {
25
+ // Pop the JSX frame.
26
+ stack = stack.slice(idx + 1);
27
+ }
28
+ idx = stack.indexOf('react-stack-bottom-frame');
29
if (idx !== -1) {
30
idx = stack.lastIndexOf('\n', idx);
31
}
@@ -44,10 +38,5 @@ function filterDebugStack(error: Error): string {
38
// To keep things light we exclude the entire trace in this case.
39
return '';
40
}
47
- const frames = stack.split('\n').slice(1); // Pop the JSX frame.
48
- return frames.filter(isNotExternal).join('\n');
49
-}
50
-
51
-export function formatOwnerStack(ownerStackTrace: Error): string {
52
- return filterDebugStack(ownerStackTrace);
41
+ return stack;
42
}
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+1
@@ -1848,6 +1848,7 @@ describe('ReactDOMFizzServer', () => {
1848
(gate(flags => flags.enableOwnerStacks)
1849
? ' in span (at **)\n' +
1850
' in mapper (at **)\n' +
1851
+ ' in Array.map (at **)\n' +
1852
' in B (at **)\n' +
1853
' in A (at **)'
1854
: ' in span (at **)\n' +
packages/react-reconciler/src/ReactFiberComponentStack.js
+1
-1
@@ -30,7 +30,7 @@ import {
30
describeClassComponentFrame,
31
describeDebugInfoFrame,
32
} from 'shared/ReactComponentStackFrame';
33
-import {formatOwnerStack} from './ReactFiberOwnerStack';
33
+import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
34
35
function describeFiber(fiber: Fiber): string {
36
switch (fiber.tag) {
packages/react-reconciler/src/ReactFiberOwnerStack.js
deleted
-47
@@ -1,47 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- * @flow
8
- */
9
-
10
-// TODO: Make this configurable on the root.
11
-const externalRegExp = /\/node\_modules\/|\(\<anonymous\>/;
12
-
13
-function isNotExternal(stackFrame: string): boolean {
14
- return !externalRegExp.test(stackFrame);
15
-}
16
-
17
-function filterDebugStack(error: Error): string {
18
- // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
19
- // to save bandwidth even in DEV. We'll also replay these stacks on the client so by
20
- // stripping them early we avoid that overhead. Otherwise we'd normally just rely on
21
- // the DevTools or framework's ignore lists to filter them out.
22
- let stack = error.stack;
23
- if (stack.startsWith('Error: react-stack-top-frame\n')) {
24
- // V8's default formatting prefixes with the error message which we
25
- // don't want/need.
26
- stack = stack.slice(29);
27
- }
28
- let idx = stack.indexOf('react-stack-bottom-frame');
29
- if (idx !== -1) {
30
- idx = stack.lastIndexOf('\n', idx);
31
- }
32
- if (idx !== -1) {
33
- // Cut off everything after the bottom frame since it'll be internals.
34
- stack = stack.slice(0, idx);
35
- } else {
36
- // We didn't find any internal callsite out to user space.
37
- // This means that this was called outside an owner or the owner is fully internal.
38
- // To keep things light we exclude the entire trace in this case.
39
- return '';
40
- }
41
- const frames = stack.split('\n').slice(1); // Pop the JSX frame.
42
- return frames.filter(isNotExternal).join('\n');
43
-}
44
-
45
-export function formatOwnerStack(ownerStackTrace: Error): string {
46
- return filterDebugStack(ownerStackTrace);
47
-}
packages/react-server/src/ReactFizzComponentStack.js
+1
-1
@@ -27,7 +27,7 @@ import {
27
28
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
29
30
-import {formatOwnerStack} from './ReactFizzOwnerStack';
30
+import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
31
32
export type ComponentStackNode = {
33
parent: null | ComponentStackNode,
packages/react-server/src/ReactFizzOwnerStack.js
deleted
-47
@@ -1,47 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- * @flow
8
- */
9
-
10
-// TODO: Make this configurable on the root.
11
-const externalRegExp = /\/node\_modules\/|\(\<anonymous\>/;
12
-
13
-function isNotExternal(stackFrame: string): boolean {
14
- return !externalRegExp.test(stackFrame);
15
-}
16
-
17
-function filterDebugStack(error: Error): string {
18
- // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
19
- // to save bandwidth even in DEV. We'll also replay these stacks on the client so by
20
- // stripping them early we avoid that overhead. Otherwise we'd normally just rely on
21
- // the DevTools or framework's ignore lists to filter them out.
22
- let stack = error.stack;
23
- if (stack.startsWith('Error: react-stack-top-frame\n')) {
24
- // V8's default formatting prefixes with the error message which we
25
- // don't want/need.
26
- stack = stack.slice(29);
27
- }
28
- let idx = stack.indexOf('react-stack-bottom-frame');
29
- if (idx !== -1) {
30
- idx = stack.lastIndexOf('\n', idx);
31
- }
32
- if (idx !== -1) {
33
- // Cut off everything after the bottom frame since it'll be internals.
34
- stack = stack.slice(0, idx);
35
- } else {
36
- // We didn't find any internal callsite out to user space.
37
- // This means that this was called outside an owner or the owner is fully internal.
38
- // To keep things light we exclude the entire trace in this case.
39
- return '';
40
- }
41
- const frames = stack.split('\n').slice(1); // Pop the JSX frame.
42
- return frames.filter(isNotExternal).join('\n');
43
-}
44
-
45
-export function formatOwnerStack(ownerStackTrace: Error): string {
46
- return filterDebugStack(ownerStackTrace);
47
-}
packages/react-server/src/ReactFlightCallUserSpace.js
+1
-35
@@ -15,13 +15,6 @@ import type {ReactClientValue} from './ReactFlightServer';
15
16
import {setCurrentOwner} from './flight/ReactFlightCurrentOwner';
17
18
-import {
19
- supportsComponentStorage,
20
- componentStorage,
21
-} from './ReactFlightServerConfig';
22
-
23
-import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
24
-
18
// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
19
// TODO: Consider marking the whole bundle instead of these boundaries.
20
@@ -30,38 +23,12 @@ const callComponent = {
23
Component: (p: Props, arg: void) => R,
24
props: Props,
25
componentDebugInfo: ReactComponentInfo,
33
- debugTask: null | ConsoleTask,
26
): R {
27
// The secondArg is always undefined in Server Components since refs error early.
28
const secondArg = undefined;
29
setCurrentOwner(componentDebugInfo);
30
try {
39
- if (supportsComponentStorage) {
40
- // Run the component in an Async Context that tracks the current owner.
41
- if (enableOwnerStacks && debugTask) {
42
- return debugTask.run(
43
- // $FlowFixMe[method-unbinding]
44
- componentStorage.run.bind(
45
- componentStorage,
46
- componentDebugInfo,
47
- Component,
48
- props,
49
- secondArg,
50
- ),
51
- );
52
- }
53
- return componentStorage.run(
54
- componentDebugInfo,
55
- Component,
56
- props,
57
- secondArg,
58
- );
59
- } else {
60
- if (enableOwnerStacks && debugTask) {
61
- return debugTask.run(Component.bind(null, props, secondArg));
62
- }
63
- return Component(props, secondArg);
64
- }
31
+ return Component(props, secondArg);
32
} finally {
33
setCurrentOwner(null);
34
}
@@ -72,7 +39,6 @@ export const callComponentInDEV: <Props, R>(
39
Component: (p: Props, arg: void) => R,
40
props: Props,
41
componentDebugInfo: ReactComponentInfo,
75
- debugTask: null | ConsoleTask,
42
) => R = __DEV__
43
? // We use this technique to trick minifiers to preserve the function name.
44
(callComponent['react-stack-bottom-frame'].bind(callComponent): any)
packages/react-server/src/ReactFlightOwnerStack.js
deleted
-42
@@ -1,42 +0,0 @@
1
-/**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- * @flow
8
- */
9
-
10
-// TODO: Make this configurable on the Request.
11
-const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>/;
12
-
13
-function isNotExternal(stackFrame: string): boolean {
14
- return !externalRegExp.test(stackFrame);
15
-}
16
-
17
-function filterDebugStack(error: Error): string {
18
- // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
19
- // to save bandwidth even in DEV. We'll also replay these stacks on the client so by
20
- // stripping them early we avoid that overhead. Otherwise we'd normally just rely on
21
- // the DevTools or framework's ignore lists to filter them out.
22
- let stack = error.stack;
23
- if (stack.startsWith('Error: react-stack-top-frame\n')) {
24
- // V8's default formatting prefixes with the error message which we
25
- // don't want/need.
26
- stack = stack.slice(29);
27
- }
28
- let idx = stack.indexOf('react-stack-bottom-frame');
29
- if (idx !== -1) {
30
- idx = stack.lastIndexOf('\n', idx);
31
- }
32
- if (idx !== -1) {
33
- // Cut off everything after the bottom frame since it'll be internals.
34
- stack = stack.slice(0, idx);
35
- }
36
- const frames = stack.split('\n').slice(1); // Pop the JSX frame.
37
- return frames.filter(isNotExternal).join('\n');
38
-}
39
-
40
-export function formatOwnerStack(ownerStackTrace: Error): string {
41
- return filterDebugStack(ownerStackTrace);
42
-}
packages/react-server/src/ReactFlightServer.js
+74
-18
@@ -80,6 +80,8 @@ import {
80
createHints,
81
initAsyncDebugInfo,
82
parseStackTrace,
83
+ supportsComponentStorage,
84
+ componentStorage,
85
} from './ReactFlightServerConfig';
86
87
import {
@@ -1035,12 +1037,38 @@ function renderFunctionComponent<Props>(
1037
}
1038
}
1039
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
1038
- result = callComponentInDEV(
1039
- Component,
1040
- props,
1041
- componentDebugInfo,
1042
- task.debugTask,
1043
- );
1040
+ if (supportsComponentStorage) {
1041
+ // Run the component in an Async Context that tracks the current owner.
1042
+ if (enableOwnerStacks && task.debugTask) {
1043
+ result = task.debugTask.run(
1044
+ // $FlowFixMe[method-unbinding]
1045
+ componentStorage.run.bind(
1046
+ componentStorage,
1047
+ componentDebugInfo,
1048
+ callComponentInDEV,
1049
+ Component,
1050
+ props,
1051
+ componentDebugInfo,
1052
+ ),
1053
+ );
1054
+ } else {
1055
+ result = componentStorage.run(
1056
+ componentDebugInfo,
1057
+ callComponentInDEV,
1058
+ Component,
1059
+ props,
1060
+ componentDebugInfo,
1061
+ );
1062
+ }
1063
+ } else {
1064
+ if (enableOwnerStacks && task.debugTask) {
1065
+ result = task.debugTask.run(
1066
+ callComponentInDEV.bind(null, Component, props, componentDebugInfo),
1067
+ );
1068
+ } else {
1069
+ result = callComponentInDEV(Component, props, componentDebugInfo);
1070
+ }
1071
+ }
1072
} else {
1073
prepareToUseHooksForComponent(prevThenableState, null);
1074
// The secondArg is always undefined in Server Components since refs error early.
@@ -1222,19 +1250,47 @@ function warnForMissingKey(
1250
1251
// Call with the server component as the currently rendering component
1252
// for context.
1225
- callComponentInDEV(
1226
- () => {
1227
- console.error(
1228
- 'Each child in a list should have a unique "key" prop.' +
1229
- '%s%s See https://react.dev/link/warning-keys for more information.',
1230
- '',
1231
- '',
1253
+ const logKeyError = () => {
1254
+ console.error(
1255
+ 'Each child in a list should have a unique "key" prop.' +
1256
+ '%s%s See https://react.dev/link/warning-keys for more information.',
1257
+ '',
1258
+ '',
1259
+ );
1260
+ };
1261
+
1262
+ if (supportsComponentStorage) {
1263
+ // Run the component in an Async Context that tracks the current owner.
1264
+ if (enableOwnerStacks && debugTask) {
1265
+ debugTask.run(
1266
+ // $FlowFixMe[method-unbinding]
1267
+ componentStorage.run.bind(
1268
+ componentStorage,
1269
+ componentDebugInfo,
1270
+ callComponentInDEV,
1271
+ logKeyError,
1272
+ null,
1273
+ componentDebugInfo,
1274
+ ),
1275
);
1233
- },
1234
- null,
1235
- componentDebugInfo,
1236
- debugTask,
1237
- );
1276
+ } else {
1277
+ componentStorage.run(
1278
+ componentDebugInfo,
1279
+ callComponentInDEV,
1280
+ logKeyError,
1281
+ null,
1282
+ componentDebugInfo,
1283
+ );
1284
+ }
1285
+ } else {
1286
+ if (enableOwnerStacks && debugTask) {
1287
+ debugTask.run(
1288
+ callComponentInDEV.bind(null, logKeyError, null, componentDebugInfo),
1289
+ );
1290
+ } else {
1291
+ callComponentInDEV(logKeyError, null, componentDebugInfo);
1292
+ }
1293
+ }
1294
}
1295
}
1296
packages/react-server/src/flight/ReactFlightComponentStack.js
+1
-1
@@ -13,7 +13,7 @@ import {describeBuiltInComponentFrame} from 'shared/ReactComponentStackFrame';
13
14
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
15
16
-import {formatOwnerStack} from '../ReactFlightOwnerStack';
16
+import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
17
18
export function getOwnerStackByComponentInfoInDev(
19
componentInfo: ReactComponentInfo,
packages/shared/ReactOwnerStackFrames.js
new
+40
@@ -0,0 +1,40 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export function formatOwnerStack(error: Error): string {
11
+ const prevPrepareStackTrace = Error.prepareStackTrace;
12
+ // $FlowFixMe[incompatible-type] It does accept undefined.
13
+ Error.prepareStackTrace = undefined;
14
+ let stack = error.stack;
15
+ Error.prepareStackTrace = prevPrepareStackTrace;
16
+ if (stack.startsWith('Error: react-stack-top-frame\n')) {
17
+ // V8's default formatting prefixes with the error message which we
18
+ // don't want/need.
19
+ stack = stack.slice(29);
20
+ }
21
+ let idx = stack.indexOf('\n');
22
+ if (idx !== -1) {
23
+ // Pop the JSX frame.
24
+ stack = stack.slice(idx + 1);
25
+ }
26
+ idx = stack.indexOf('react-stack-bottom-frame');
27
+ if (idx !== -1) {
28
+ idx = stack.lastIndexOf('\n', idx);
29
+ }
30
+ if (idx !== -1) {
31
+ // Cut off everything after the bottom frame since it'll be internals.
32
+ stack = stack.slice(0, idx);
33
+ } else {
34
+ // We didn't find any internal callsite out to user space.
35
+ // This means that this was called outside an owner or the owner is fully internal.
36
+ // To keep things light we exclude the entire trace in this case.
37
+ return '';
38
+ }
39
+ return stack;
40
+}