[DevTools] Ignore List Stack Traces (#34210)

Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
This commit is contained in:
Sebastian Markbåge
2025-08-21 18:03:05 -04:00
committed by GitHub
parent 7d29ecbeb2
commit a85ec041d6
15 changed files with 89 additions and 61 deletions

View File

@@ -15,6 +15,7 @@ const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const DevToolsIgnorePlugin = require('devtools-ignore-webpack-plugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const paths = require('./paths');
const modules = require('./modules');
@@ -685,6 +686,15 @@ module.exports = function (webpackEnv) {
},
}),
// Fork Start
new DevToolsIgnorePlugin({
shouldIgnorePath: function (path) {
return (
path.includes('/node_modules/') ||
path.includes('/webpack/') ||
path.endsWith('/src/index.js')
);
},
}),
new ReactFlightWebpackPlugin({
isServer: false,
clientReferences: {

View File

@@ -29,6 +29,7 @@
"concurrently": "^7.3.0",
"css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^3.2.0",
"devtools-ignore-webpack-plugin": "^0.2.0",
"dotenv": "^10.0.0",
"dotenv-expand": "^5.1.0",
"file-loader": "^6.2.0",

View File

@@ -58,7 +58,8 @@ function filterStackFrame(sourceURL, functionName) {
sourceURL !== '' &&
!sourceURL.startsWith('node:') &&
!sourceURL.includes('node_modules') &&
!sourceURL.endsWith('library.js')
!sourceURL.endsWith('library.js') &&
!sourceURL.includes('/server/region.js')
);
}

View File

@@ -4614,6 +4614,11 @@ detect-port-alt@^1.1.6:
address "^1.0.1"
debug "^2.6.0"
devtools-ignore-webpack-plugin@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/devtools-ignore-webpack-plugin/-/devtools-ignore-webpack-plugin-0.2.0.tgz#a7b3d1bd0f593c7fee5cbb7534b07860e5e2447c"
integrity sha512-4P+1Y1VhSt1MRBRF6my8N1bs9nNMOFfIFSBHI6u18W73iCHWXNHTSWYeMoQQ72PIIHrP1q4koKpYg1Em3jb9Rw==
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -8650,16 +8655,7 @@ string-length@^5.0.1:
char-regex "^2.0.0"
strip-ansi "^7.0.1"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8730,14 +8726,7 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9452,16 +9441,7 @@ wordwrap@~1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

View File

@@ -401,7 +401,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.f = f;
function f() { }
//# sourceMappingURL=`;
const result = ['', 'http://test/a.mts', 1, 17];
const result = {
location: ['', 'http://test/a.mts', 1, 17],
ignored: false,
};
const fs = {
'http://test/a.mts': `export function f() {}`,
'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`,

View File

@@ -7,6 +7,8 @@
* @flow
*/
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
import * as React from 'react';
import {useCallback, useContext, useSyncExternalStore} from 'react';
import {TreeStateContext} from './TreeContext';
@@ -28,8 +30,6 @@ import useEditorURL from '../useEditorURL';
import styles from './InspectedElement.css';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
export type Props = {};
// TODO Make edits and deletes also use transition API!
@@ -61,7 +61,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
? inspectedElement.stack[0]
: null;
const symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> =
const symbolicatedSourcePromise: Promise<SourceMappedLocation | null> =
React.useMemo(() => {
if (fetchFileWithCaching == null) return noSourcePromise;

View File

@@ -17,6 +17,7 @@ import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/wit
import useOpenResource from '../useOpenResource';
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import styles from './InspectedElementSourcePanel.css';
@@ -24,7 +25,7 @@ import formatLocationForDisplay from './formatLocationForDisplay';
type Props = {
source: ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
};
function InspectedElementSourcePanel({
@@ -80,7 +81,7 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
);
}
const [, sourceURL, line, column] = symbolicatedSource;
const [, sourceURL, line, column] = symbolicatedSource.location;
const handleCopy = withPermissionsCheck(
{permissions: ['clipboardWrite']},
() => copy(`${sourceURL}:${line}:${column}`),
@@ -98,11 +99,11 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
const [linkIsEnabled, viewSource] = useOpenResource(
source,
symbolicatedSource,
symbolicatedSource == null ? null : symbolicatedSource.location,
);
const [, sourceURL, line, column] =
symbolicatedSource == null ? source : symbolicatedSource;
symbolicatedSource == null ? source : symbolicatedSource.location;
return (
<div

View File

@@ -34,7 +34,7 @@ import type {
} from 'react-devtools-shared/src/frontend/types';
import type {HookNames} from 'react-devtools-shared/src/frontend/types';
import type {ToggleParseHookNames} from './InspectedElementContext';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
type Props = {
element: Element,
@@ -42,7 +42,7 @@ type Props = {
inspectedElement: InspectedElement,
parseHookNames: boolean,
toggleParseHookNames: ToggleParseHookNames,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
};
export default function InspectedElementView({

View File

@@ -13,12 +13,13 @@ import ButtonIcon from '../ButtonIcon';
import Button from '../Button';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
import useOpenResource from '../useOpenResource';
type Props = {
source: null | ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
symbolicatedSourcePromise: Promise<SourceMappedLocation | null> | null,
};
function InspectedElementViewSourceButton({
@@ -42,7 +43,7 @@ function InspectedElementViewSourceButton({
type ActualSourceButtonProps = {
source: null | ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
symbolicatedSourcePromise: Promise<SourceMappedLocation | null> | null,
};
function ActualSourceButton({
source,
@@ -55,7 +56,7 @@ function ActualSourceButton({
const [buttonIsEnabled, viewSource] = useOpenResource(
source,
symbolicatedSource,
symbolicatedSource == null ? null : symbolicatedSource.location,
);
return (
<Button

View File

@@ -13,13 +13,14 @@ import Button from 'react-devtools-shared/src/devtools/views/Button';
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
import {checkConditions} from '../Editor/utils';
type Props = {
editorURL: string,
source: ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
symbolicatedSourcePromise: Promise<SourceMappedLocation | null>,
};
function OpenInEditorButton({
@@ -31,7 +32,7 @@ function OpenInEditorButton({
const {url, shouldDisableButton} = checkConditions(
editorURL,
symbolicatedSource ? symbolicatedSource : source,
symbolicatedSource ? symbolicatedSource.location : source,
);
return (

View File

@@ -2,11 +2,15 @@
padding: 0.25rem;
}
.CallSite {
.CallSite, .IgnoredCallSite {
display: block;
padding-left: 1rem;
}
.IgnoredCallSite {
opacity: 0.5;
}
.Link {
color: var(--color-link);
white-space: pre;

View File

@@ -16,11 +16,9 @@ import ElementBadges from './ElementBadges';
import styles from './StackTraceView.css';
import type {
ReactStackTrace,
ReactCallSite,
ReactFunctionLocation,
} from 'shared/ReactTypes';
import type {ReactStackTrace, ReactCallSite} from 'shared/ReactTypes';
import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
@@ -42,7 +40,7 @@ export function CallSiteView({
const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
callSite;
const symbolicatedCallSite: null | ReactFunctionLocation =
const symbolicatedCallSite: null | SourceMappedLocation =
fetchFileWithCaching !== null
? use(
symbolicateSourceWithCache(
@@ -56,12 +54,20 @@ export function CallSiteView({
const [linkIsEnabled, viewSource] = useOpenResource(
callSite,
symbolicatedCallSite,
symbolicatedCallSite == null ? null : symbolicatedCallSite.location,
);
const [functionName, url, line, column] =
symbolicatedCallSite !== null ? symbolicatedCallSite : callSite;
symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite;
const ignored =
symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false;
if (ignored) {
// TODO: Make an option to be able to toggle the display of ignore listed rows.
// Ideally this UI should be higher than a single Stack Trace so that there's not
// multiple buttons in a single inspection taking up space.
return null;
}
return (
<div className={styles.CallSite}>
<div className={ignored ? styles.IgnoredCallSite : styles.CallSite}>
{functionName || virtualFunctionName}
{url !== '' && (
<>

View File

@@ -26,6 +26,7 @@ type ResultPosition = {
line: number,
sourceContent: string | null,
sourceURL: string | null,
ignored: boolean,
};
export type SourceMapConsumerType = {
@@ -117,12 +118,15 @@ function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
const sourceURL = sourceMapJSON.sources[sourceIndex] ?? null;
const line = nearestEntry[2] + 1;
const column = nearestEntry[3];
const ignored =
sourceMapJSON.ignoreList != null &&
sourceMapJSON.ignoreList.includes(sourceIndex);
return {
column,
line,
sourceContent: ((sourceContent: any): string | null),
sourceURL: ((sourceURL: any): string | null),
ignored,
};
}

View File

@@ -25,6 +25,7 @@ export type BasicSourceMap = {
+version: number,
+x_facebook_sources?: FBSourcesArray,
+x_react_sources?: ReactSourcesArray,
+ignoreList?: Array<number>,
};
export type IndexSourceMapSection = {

View File

@@ -14,15 +14,20 @@ import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/view
const symbolicationCache: Map<
string,
Promise<ReactFunctionLocation | null>,
Promise<SourceMappedLocation | null>,
> = new Map();
export type SourceMappedLocation = {
location: ReactFunctionLocation,
ignored: boolean, // Whether the file for this location was ignore listed
};
export function symbolicateSourceWithCache(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
line: number, // 1-based
column: number, // 1-based
): Promise<ReactFunctionLocation | null> {
): Promise<SourceMappedLocation | null> {
const key = `${sourceURL}:${line}:${column}`;
const cachedPromise = symbolicationCache.get(key);
if (cachedPromise != null) {
@@ -46,7 +51,7 @@ export async function symbolicateSource(
sourceURL: string,
lineNumber: number, // 1-based
columnNumber: number, // 1-based
): Promise<ReactFunctionLocation | null> {
): Promise<SourceMappedLocation | null> {
const resource = await fetchFileWithCaching(sourceURL).catch(() => null);
if (resource == null) {
return null;
@@ -83,6 +88,7 @@ export async function symbolicateSource(
sourceURL: possiblyURL,
line,
column: columnZeroBased,
ignored,
} = consumer.originalPositionFor({
lineNumber, // 1-based
columnNumber, // 1-based
@@ -97,7 +103,10 @@ export async function symbolicateSource(
// sourceMapURL = https://react.dev/script.js.map
void new URL(possiblyURL); // test if it is a valid URL
return [functionName, possiblyURL, line, column];
return {
location: [functionName, possiblyURL, line, column],
ignored,
};
} catch (e) {
// This is not valid URL
if (
@@ -107,7 +116,10 @@ export async function symbolicateSource(
possiblyURL.slice(1).startsWith(':\\\\')
) {
// This is an absolute path
return [functionName, possiblyURL, line, column];
return {
location: [functionName, possiblyURL, line, column],
ignored,
};
}
// This is a relative path
@@ -116,7 +128,10 @@ export async function symbolicateSource(
possiblyURL,
sourceMapURL,
).toString();
return [functionName, absoluteSourcePath, line, column];
return {
location: [functionName, absoluteSourcePath, line, column],
ignored,
};
}
} catch (e) {
return null;