[DevTools] Ignore List Stack Traces (#34210)
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
Sebastian Markbåge committed
Aug 21, 2025 at 18:03 UTC
a85ec041d63428d02b952f93822e429322cfe821
15 files changed
+89
-61
fixtures/flight/config/webpack.config.js
+10
@@ -15,6 +15,7 @@ const TerserPlugin = require('terser-webpack-plugin');
15
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
16
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
17
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
18
+const DevToolsIgnorePlugin = require('devtools-ignore-webpack-plugin');
19
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
20
const paths = require('./paths');
21
const modules = require('./modules');
@@ -685,6 +686,15 @@ module.exports = function (webpackEnv) {
686
},
687
}),
688
// Fork Start
689
+ new DevToolsIgnorePlugin({
690
+ shouldIgnorePath: function (path) {
691
+ return (
692
+ path.includes('/node_modules/') ||
693
+ path.includes('/webpack/') ||
694
+ path.endsWith('/src/index.js')
695
+ );
696
+ },
697
+ }),
698
new ReactFlightWebpackPlugin({
699
isServer: false,
700
clientReferences: {
fixtures/flight/package.json
+1
@@ -29,6 +29,7 @@
29
"concurrently": "^7.3.0",
30
"css-loader": "^6.5.1",
31
"css-minimizer-webpack-plugin": "^3.2.0",
32
+ "devtools-ignore-webpack-plugin": "^0.2.0",
33
"dotenv": "^10.0.0",
34
"dotenv-expand": "^5.1.0",
35
"file-loader": "^6.2.0",
fixtures/flight/server/region.js
+2
-1
@@ -58,7 +58,8 @@ function filterStackFrame(sourceURL, functionName) {
58
sourceURL !== '' &&
59
!sourceURL.startsWith('node:') &&
60
!sourceURL.includes('node_modules') &&
61
- !sourceURL.endsWith('library.js')
61
+ !sourceURL.endsWith('library.js') &&
62
+ !sourceURL.includes('/server/region.js')
63
);
64
}
65
fixtures/flight/yarn.lock
+8
-28
@@ -4614,6 +4614,11 @@ detect-port-alt@^1.1.6:
4614
address "^1.0.1"
4615
debug "^2.6.0"
4616
4617
+devtools-ignore-webpack-plugin@^0.2.0:
4618
+ version "0.2.0"
4619
+ resolved "https://registry.yarnpkg.com/devtools-ignore-webpack-plugin/-/devtools-ignore-webpack-plugin-0.2.0.tgz#a7b3d1bd0f593c7fee5cbb7534b07860e5e2447c"
4620
+ integrity sha512-4P+1Y1VhSt1MRBRF6my8N1bs9nNMOFfIFSBHI6u18W73iCHWXNHTSWYeMoQQ72PIIHrP1q4koKpYg1Em3jb9Rw==
4621
+
4622
didyoumean@^1.2.2:
4623
version "1.2.2"
4624
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -8650,16 +8655,7 @@ string-length@^5.0.1:
8655
char-regex "^2.0.0"
8656
strip-ansi "^7.0.1"
8657
8653
-"string-width-cjs@npm:string-width@^4.2.0":
8654
- version "4.2.3"
8655
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
8656
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
8657
- dependencies:
8658
- emoji-regex "^8.0.0"
8659
- is-fullwidth-code-point "^3.0.0"
8660
- strip-ansi "^6.0.1"
8661
-
8662
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
8658
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
8659
version "4.2.3"
8660
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
8661
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8730,14 +8726,7 @@ string_decoder@^1.1.1:
8726
dependencies:
8727
safe-buffer "~5.2.0"
8728
8733
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
8734
- version "6.0.1"
8735
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
8736
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
8737
- dependencies:
8738
- ansi-regex "^5.0.1"
8739
-
8740
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
8729
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
8730
version "6.0.1"
8731
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
8732
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9452,16 +9441,7 @@ wordwrap@~1.0.0:
9441
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
9442
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
9443
9455
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
9456
- version "7.0.0"
9457
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
9458
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
9459
- dependencies:
9460
- ansi-styles "^4.0.0"
9461
- string-width "^4.1.0"
9462
- strip-ansi "^6.0.0"
9463
-
9464
-wrap-ansi@^7.0.0:
9444
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
9445
version "7.0.0"
9446
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
9447
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
packages/react-devtools-shared/src/__tests__/utils-test.js
+4
-1
@@ -401,7 +401,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
401
exports.f = f;
402
function f() { }
403
//# sourceMappingURL=`;
404
- const result = ['', 'http://test/a.mts', 1, 17];
404
+ const result = {
405
+ location: ['', 'http://test/a.mts', 1, 17],
406
+ ignored: false,
407
+ };
408
const fs = {
409
'http://test/a.mts': `export function f() {}`,
410
'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`,
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+3
-3
@@ -7,6 +7,8 @@
7
* @flow
8
*/
9
10
+import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
11
+
12
import * as React from 'react';
13
import {useCallback, useContext, useSyncExternalStore} from 'react';
14
import {TreeStateContext} from './TreeContext';
@@ -28,8 +30,6 @@ import useEditorURL from '../useEditorURL';
30
31
import styles from './InspectedElement.css';
32
31
-import type {ReactFunctionLocation} from 'shared/ReactTypes';
32
-
33
export type Props = {};
34
35
// TODO Make edits and deletes also use transition API!
@@ -61,7 +61,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
61
? inspectedElement.stack[0]
62
: null;
63
64
- const symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> =
64
+ const symbolicatedSourcePromise: Promise<SourceMappedLocation | null> =
65
React.useMemo(() => {
66
if (fetchFileWithCaching == null) return noSourcePromise;
67
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js
+5
-4
@@ -17,6 +17,7 @@ import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/wit
17
18
import useOpenResource from '../useOpenResource';
19
20
+import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
21
import type {ReactFunctionLocation} from 'shared/ReactTypes';
22
import styles from './InspectedElementSourcePanel.css';
23
@@ -24,7 +25,7 @@ import formatLocationForDisplay from './formatLocationForDisplay';
25
26
type Props = {
27
source: ReactFunctionLocation,
27
- symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
28
+ symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
29
};
30
31
function InspectedElementSourcePanel({
@@ -80,7 +81,7 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
81
);
82
}
83
83
- const [, sourceURL, line, column] = symbolicatedSource;
84
+ const [, sourceURL, line, column] = symbolicatedSource.location;
85
const handleCopy = withPermissionsCheck(
86
{permissions: ['clipboardWrite']},
87
() => copy(`${sourceURL}:${line}:${column}`),
@@ -98,11 +99,11 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
99
100
const [linkIsEnabled, viewSource] = useOpenResource(
101
source,
101
- symbolicatedSource,
102
+ symbolicatedSource == null ? null : symbolicatedSource.location,
103
);
104
105
const [, sourceURL, line, column] =
105
- symbolicatedSource == null ? source : symbolicatedSource;
106
+ symbolicatedSource == null ? source : symbolicatedSource.location;
107
108
return (
109
<div
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+2
-2
@@ -34,7 +34,7 @@ import type {
34
} from 'react-devtools-shared/src/frontend/types';
35
import type {HookNames} from 'react-devtools-shared/src/frontend/types';
36
import type {ToggleParseHookNames} from './InspectedElementContext';
37
-import type {ReactFunctionLocation} from 'shared/ReactTypes';
37
+import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
38
39
type Props = {
40
element: Element,
@@ -42,7 +42,7 @@ type Props = {
42
inspectedElement: InspectedElement,
43
parseHookNames: boolean,
44
toggleParseHookNames: ToggleParseHookNames,
45
- symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
45
+ symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
46
};
47
48
export default function InspectedElementView({
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js
+4
-3
@@ -13,12 +13,13 @@ import ButtonIcon from '../ButtonIcon';
13
import Button from '../Button';
14
15
import type {ReactFunctionLocation} from 'shared/ReactTypes';
16
+import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
17
18
import useOpenResource from '../useOpenResource';
19
20
type Props = {
21
source: null | ReactFunctionLocation,
21
- symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
22
+ symbolicatedSourcePromise: Promise<SourceMappedLocation | null> | null,
23
};
24
25
function InspectedElementViewSourceButton({
@@ -42,7 +43,7 @@ function InspectedElementViewSourceButton({
43
44
type ActualSourceButtonProps = {
45
source: null | ReactFunctionLocation,
45
- symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
46
+ symbolicatedSourcePromise: Promise<SourceMappedLocation | null> | null,
47
};
48
function ActualSourceButton({
49
source,
@@ -55,7 +56,7 @@ function ActualSourceButton({
56
57
const [buttonIsEnabled, viewSource] = useOpenResource(
58
source,
58
- symbolicatedSource,
59
+ symbolicatedSource == null ? null : symbolicatedSource.location,
60
);
61
return (
62
<Button
packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js
+3
-2
@@ -13,13 +13,14 @@ import Button from 'react-devtools-shared/src/devtools/views/Button';
13
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
14
15
import type {ReactFunctionLocation} from 'shared/ReactTypes';
16
+import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
17
18
import {checkConditions} from '../Editor/utils';
19
20
type Props = {
21
editorURL: string,
22
source: ReactFunctionLocation,
22
- symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
23
+ symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
24
};
25
26
function OpenInEditorButton({
@@ -31,7 +32,7 @@ function OpenInEditorButton({
32
33
const {url, shouldDisableButton} = checkConditions(
34
editorURL,
34
- symbolicatedSource ? symbolicatedSource : source,
35
+ symbolicatedSource ? symbolicatedSource.location : source,
36
);
37
38
return (
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.css
+5
-1
@@ -2,11 +2,15 @@
2
padding: 0.25rem;
3
}
4
5
-.CallSite {
5
+.CallSite, .IgnoredCallSite {
6
display: block;
7
padding-left: 1rem;
8
}
9
10
+.IgnoredCallSite {
11
+ opacity: 0.5;
12
+}
13
+
14
.Link {
15
color: var(--color-link);
16
white-space: pre;
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js
+15
-9
@@ -16,11 +16,9 @@ import ElementBadges from './ElementBadges';
16
17
import styles from './StackTraceView.css';
18
19
-import type {
20
- ReactStackTrace,
21
- ReactCallSite,
22
- ReactFunctionLocation,
23
-} from 'shared/ReactTypes';
19
+import type {ReactStackTrace, ReactCallSite} from 'shared/ReactTypes';
20
+
21
+import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
22
23
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
24
@@ -42,7 +40,7 @@ export function CallSiteView({
40
const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
41
callSite;
42
45
- const symbolicatedCallSite: null | ReactFunctionLocation =
43
+ const symbolicatedCallSite: null | SourceMappedLocation =
44
fetchFileWithCaching !== null
45
? use(
46
symbolicateSourceWithCache(
@@ -56,12 +54,20 @@ export function CallSiteView({
54
55
const [linkIsEnabled, viewSource] = useOpenResource(
56
callSite,
59
- symbolicatedCallSite,
57
+ symbolicatedCallSite == null ? null : symbolicatedCallSite.location,
58
);
59
const [functionName, url, line, column] =
62
- symbolicatedCallSite !== null ? symbolicatedCallSite : callSite;
60
+ symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
61
+ const ignored =
62
+ symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
63
+ if (ignored) {
64
+ // TODO: Make an option to be able to toggle the display of ignore listed rows.
65
+ // Ideally this UI should be higher than a single Stack Trace so that there's not
66
+ // multiple buttons in a single inspection taking up space.
67
+ return null;
68
+ }
69
return (
64
- <div className={styles.CallSite}>
70
+ <div className={ignored ? styles.IgnoredCallSite : styles.CallSite}>
71
{functionName || virtualFunctionName}
72
{url !== '' && (
73
<>
packages/react-devtools-shared/src/hooks/SourceMapConsumer.js
+5
-1
@@ -26,6 +26,7 @@ type ResultPosition = {
26
line: number,
27
sourceContent: string | null,
28
sourceURL: string | null,
29
+ ignored: boolean,
30
};
31
32
export type SourceMapConsumerType = {
@@ -117,12 +118,15 @@ function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
118
const sourceURL = sourceMapJSON.sources[sourceIndex] ?? null;
119
const line = nearestEntry[2] + 1;
120
const column = nearestEntry[3];
120
-
121
+ const ignored =
122
+ sourceMapJSON.ignoreList != null &&
123
+ sourceMapJSON.ignoreList.includes(sourceIndex);
124
return {
125
column,
126
line,
127
sourceContent: ((sourceContent: any): string | null),
128
sourceURL: ((sourceURL: any): string | null),
129
+ ignored,
130
};
131
}
132
packages/react-devtools-shared/src/hooks/SourceMapTypes.js
+1
@@ -25,6 +25,7 @@ export type BasicSourceMap = {
25
+version: number,
26
+x_facebook_sources?: FBSourcesArray,
27
+x_react_sources?: ReactSourcesArray,
28
+ +ignoreList?: Array<number>,
29
};
30
31
export type IndexSourceMapSection = {
packages/react-devtools-shared/src/symbolicateSource.js
+21
-6
@@ -14,15 +14,20 @@ import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/view
14
15
const symbolicationCache: Map<
16
string,
17
- Promise<ReactFunctionLocation | null>,
17
+ Promise<SourceMappedLocation | null>,
18
> = new Map();
19
20
+export type SourceMappedLocation = {
21
+ location: ReactFunctionLocation,
22
+ ignored: boolean, // Whether the file for this location was ignore listed
23
+};
24
+
25
export function symbolicateSourceWithCache(
26
fetchFileWithCaching: FetchFileWithCaching,
27
sourceURL: string,
28
line: number, // 1-based
29
column: number, // 1-based
25
-): Promise<ReactFunctionLocation | null> {
30
+): Promise<SourceMappedLocation | null> {
31
const key = `${sourceURL}:${line}:${column}`;
32
const cachedPromise = symbolicationCache.get(key);
33
if (cachedPromise != null) {
@@ -46,7 +51,7 @@ export async function symbolicateSource(
51
sourceURL: string,
52
lineNumber: number, // 1-based
53
columnNumber: number, // 1-based
49
-): Promise<ReactFunctionLocation | null> {
54
+): Promise<SourceMappedLocation | null> {
55
const resource = await fetchFileWithCaching(sourceURL).catch(() => null);
56
if (resource == null) {
57
return null;
@@ -83,6 +88,7 @@ export async function symbolicateSource(
88
sourceURL: possiblyURL,
89
line,
90
column: columnZeroBased,
91
+ ignored,
92
} = consumer.originalPositionFor({
93
lineNumber, // 1-based
94
columnNumber, // 1-based
@@ -97,7 +103,10 @@ export async function symbolicateSource(
103
// sourceMapURL = https://react.dev/script.js.map
104
void new URL(possiblyURL); // test if it is a valid URL
105
100
- return [functionName, possiblyURL, line, column];
106
+ return {
107
+ location: [functionName, possiblyURL, line, column],
108
+ ignored,
109
+ };
110
} catch (e) {
111
// This is not valid URL
112
if (
@@ -107,7 +116,10 @@ export async function symbolicateSource(
116
possiblyURL.slice(1).startsWith(':\\\\')
117
) {
118
// This is an absolute path
110
- return [functionName, possiblyURL, line, column];
119
+ return {
120
+ location: [functionName, possiblyURL, line, column],
121
+ ignored,
122
+ };
123
}
124
125
// This is a relative path
@@ -116,7 +128,10 @@ export async function symbolicateSource(
128
possiblyURL,
129
sourceMapURL,
130
).toString();
119
- return [functionName, absoluteSourcePath, line, column];
131
+ return {
132
+ location: [functionName, absoluteSourcePath, line, column],
133
+ ignored,
134
+ };
135
}
136
} catch (e) {
137
return null;