@samitouri / QOS-React / commits / a85ec041d6

[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;