From b4532229400747e59d8b752b6508493f721fe646 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 3 Aug 2025 21:47:27 -0400 Subject: [PATCH 1/5] Add structure full stack parsing to DevTools This is a fork of ReactFlightStackConfigV8 which also supports DevTools requirements like checking both react_stack_bottom_frame and react-stack-bottom-frame as well as supporting Firefox stacks. It also supports extracting the first frame of a component stack or the last frame of an owner stack for the source location. --- .../src/backend/utils/parseStackTrace.js | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 packages/react-devtools-shared/src/backend/utils/parseStackTrace.js diff --git a/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js new file mode 100644 index 00000000000..1bcfb9e5d35 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js @@ -0,0 +1,334 @@ +/** +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes'; + +function parseStackTraceFromChromeStack( + stack: string, + skipFrames: number, +): ReactStackTrace { + if (stack.startsWith('Error: react-stack-top-frame\n')) { + // V8's default formatting prefixes with the error message which we + // don't want/need. + stack = stack.slice(29); + } + let idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); + } + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); + } + const frames = stack.split('\n'); + const parsedFrames: ReactStackTrace = []; + // We skip top frames here since they may or may not be parseable but we + // want to skip the same number of frames regardless. I.e. we can't do it + // in the caller. + for (let i = skipFrames; i < frames.length; i++) { + const parsed = chromeFrameRegExp.exec(frames[i]); + if (!parsed) { + continue; + } + let name = parsed[1] || ''; + let isAsync = parsed[8] === 'async '; + if (name === '') { + name = ''; + } else if (name.startsWith('async ')) { + name = name.slice(5); + isAsync = true; + } + let filename = parsed[2] || parsed[5] || ''; + if (filename === '') { + filename = ''; + } + const line = +(parsed[3] || parsed[6]); + const col = +(parsed[4] || parsed[7]); + parsedFrames.push([name, filename, line, col, 0, 0, isAsync]); + } + return parsedFrames; +} + +const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/; +function parseStackTraceFromFirefoxStack( + stack: string, + skipFrames: number, +): ReactStackTrace { + let idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); + } + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); + } + const frames = stack.split('\n'); + const parsedFrames: ReactStackTrace = []; + // We skip top frames here since they may or may not be parseable but we + // want to skip the same number of frames regardless. I.e. we can't do it + // in the caller. + for (let i = skipFrames; i < frames.length; i++) { + const parsed = firefoxFrameRegExp.exec(frames[i]); + if (!parsed) { + continue; + } + const name = parsed[1] || ''; + const filename = parsed[2] || ''; + const line = +parsed[3]; + const col = +parsed[4]; + parsedFrames.push([name, filename, line, col, 0, 0, false]); + } + return parsedFrames; +} + +const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; +export function parseStackTraceFromString( + stack: string, + skipFrames: number, +): ReactStackTrace { + if (stack.match(CHROME_STACK_REGEXP)) { + return parseStackTraceFromChromeStack(stack, skipFrames); + } + return parseStackTraceFromFirefoxStack(stack, skipFrames); +} + +let framesToSkip: number = 0; +let collectedStackTrace: null | ReactStackTrace = null; + +const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + +function getMethodCallName(callSite: CallSite): string { + const typeName = callSite.getTypeName(); + const methodName = callSite.getMethodName(); + const functionName = callSite.getFunctionName(); + let result = ''; + if (functionName) { + if ( + typeName && + identifierRegExp.test(functionName) && + functionName !== typeName + ) { + result += typeName + '.'; + } + result += functionName; + if ( + methodName && + functionName !== methodName && + !functionName.endsWith('.' + methodName) && + !functionName.endsWith(' ' + methodName) + ) { + result += ' [as ' + methodName + ']'; + } + } else { + if (typeName) { + result += typeName + '.'; + } + if (methodName) { + result += methodName; + } else { + result += ''; + } + } + return result; +} + +function collectStackTrace( + error: Error, + structuredStackTrace: CallSite[], +): string { + const result: ReactStackTrace = []; + // Collect structured stack traces from the callsites. + // We mirror how V8 serializes stack frames and how we later parse them. + for (let i = framesToSkip; i < structuredStackTrace.length; i++) { + const callSite = structuredStackTrace[i]; + let name = callSite.getFunctionName() || ''; + if ( + name.includes('react_stack_bottom_frame') || + name.includes('react-stack-bottom-frame') + ) { + // Skip everything after the bottom frame since it'll be internals. + break; + } else if (callSite.isNative()) { + // $FlowFixMe[prop-missing] + const isAsync = callSite.isAsync(); + result.push([name, '', 0, 0, 0, 0, isAsync]); + } else { + // We encode complex function calls as if they're part of the function + // name since we cannot simulate the complex ones and they look the same + // as function names in UIs on the client as well as stacks. + if (callSite.isConstructor()) { + name = 'new ' + name; + } else if (!callSite.isToplevel()) { + name = getMethodCallName(callSite); + } + if (name === '') { + name = ''; + } + let filename = callSite.getScriptNameOrSourceURL() || ''; + if (filename === '') { + filename = ''; + if (callSite.isEval()) { + const origin = callSite.getEvalOrigin(); + if (origin) { + filename = origin.toString() + ', '; + } + } + } + const line = callSite.getLineNumber() || 0; + const col = callSite.getColumnNumber() || 0; + const enclosingLine: number = + // $FlowFixMe[prop-missing] + typeof callSite.getEnclosingLineNumber === 'function' + ? (callSite: any).getEnclosingLineNumber() || 0 + : 0; + const enclosingCol: number = + // $FlowFixMe[prop-missing] + typeof callSite.getEnclosingColumnNumber === 'function' + ? (callSite: any).getEnclosingColumnNumber() || 0 + : 0; + // $FlowFixMe[prop-missing] + const isAsync = callSite.isAsync(); + result.push([ + name, + filename, + line, + col, + enclosingLine, + enclosingCol, + isAsync, + ]); + } + } + collectedStackTrace = result; + + // At the same time we generate a string stack trace just in case someone + // else reads it. Ideally, we'd call the previous prepareStackTrace to + // ensure it's in the expected format but it's common for that to be + // source mapped and since we do a lot of eager parsing of errors, it + // would be slow in those environments. We could maybe just rely on those + // environments having to disable source mapping globally to speed things up. + // For now, we just generate a default V8 formatted stack trace without + // source mapping as a fallback. + const name = error.name || 'Error'; + const message = error.message || ''; + let stack = name + ': ' + message; + for (let i = 0; i < structuredStackTrace.length; i++) { + stack += '\n at ' + structuredStackTrace[i].toString(); + } + return stack; +} + +// This matches either of these V8 formats. +// at name (filename:0:0) +// at filename:0:0 +// at async filename:0:0 +const chromeFrameRegExp = + /^ {3} at (?:(.+) \((?:(.+):(\d+):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; + +// DEV-only cache of parsed and filtered stack frames. +const stackTraceCache: WeakMap = __DEV__ + ? new WeakMap() + : (null: any); + +export function parseStackTrace( + error: Error, + skipFrames: number, +): ReactStackTrace { + // We can only get structured data out of error objects once. So we cache the information + // so we can get it again each time. It also helps performance when the same error is + // referenced more than once. + const existing = stackTraceCache.get(error); + if (existing !== undefined) { + return existing; + } + // We override Error.prepareStackTrace with our own version that collects + // the structured data. We need more information than the raw stack gives us + // and we need to ensure that we don't get the source mapped version. + collectedStackTrace = null; + framesToSkip = skipFrames; + const previousPrepare = Error.prepareStackTrace; + Error.prepareStackTrace = collectStackTrace; + let stack; + try { + stack = String(error.stack); + } finally { + Error.prepareStackTrace = previousPrepare; + } + + if (collectedStackTrace !== null) { + const result = collectedStackTrace; + collectedStackTrace = null; + stackTraceCache.set(error, result); + return result; + } + + // If the stack has already been read, or this is not actually a V8 compatible + // engine then we might not get a normalized stack and it might still have been + // source mapped. Regardless we try our best to parse it. + + const parsedFrames = parseStackTraceFromString(stack, skipFrames); + stackTraceCache.set(error, parsedFrames); + return parsedFrames; +} + +export function extractLocationFromOwnerStack( + error: Error, +): ReactFunctionLocation | null { + const stackTrace = parseStackTrace(error, 0); + const stack = error.stack; + if ( + !stack.includes('react_stack_bottom_frame') && + !stack.includes('react-stack-bottom-frame') + ) { + // This didn't have a bottom to it, we can't trust it. + return null; + } + // We start from the bottom since that will have the best location for the owner itself. + for (let i = stackTrace.length - 1; i >= 0; i--) { + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i]; + // Take the first match with a colon in the file name. + if (fileName.indexOf(':') !== -1) { + return [ + functionName, + fileName, + // Use enclosing line if available, since that points to the start of the function. + encLine || line, + encCol || col, + ]; + } + } + return null; +} + +export function extractLocationFromComponentStack( + stack: string, +): ReactFunctionLocation | null { + const stackTrace = parseStackTraceFromString(stack, 0); + for (let i = 0; i < stackTrace.length; i++) { + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i]; + // Take the first match with a colon in the file name. + if (fileName.indexOf(':') !== -1) { + return [ + functionName, + fileName, + // Use enclosing line if available. (Never the case here because we parse from string.) + encLine || line, + encCol || col, + ]; + } + } + return null; +} From fb3dd38db27ea20fc2b42c45d132af1012d4960c Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 3 Aug 2025 21:54:21 -0400 Subject: [PATCH 2/5] Use new helpers to extract location --- .../src/__tests__/utils-test.js | 16 +- .../src/backend/fiber/renderer.js | 12 +- .../src/backend/shared/DevToolsOwnerStack.js | 5 +- .../src/backend/utils/index.js | 183 ------------------ 4 files changed, 16 insertions(+), 200 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/utils-test.js b/packages/react-devtools-shared/src/__tests__/utils-test.js index 57865f90f82..ebdb386b10c 100644 --- a/packages/react-devtools-shared/src/__tests__/utils-test.js +++ b/packages/react-devtools-shared/src/__tests__/utils-test.js @@ -19,8 +19,8 @@ import { formatWithStyles, gt, gte, - parseSourceFromComponentStack, } from 'react-devtools-shared/src/backend/utils'; +import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { REACT_SUSPENSE_LIST_TYPE as SuspenseList, REACT_STRICT_MODE_TYPE as StrictMode, @@ -306,14 +306,14 @@ describe('utils', () => { }); }); - describe('parseSourceFromComponentStack', () => { + describe('extractLocationFromComponentStack', () => { it('should return null if passed empty string', () => { - expect(parseSourceFromComponentStack('')).toEqual(null); + expect(extractLocationFromComponentStack('')).toEqual(null); }); it('should construct the source from the first frame if available', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' + 'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' + 'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n', @@ -328,7 +328,7 @@ describe('utils', () => { it('should construct the source from highest available frame', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( ' at Q\n' + ' at a\n' + ' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' + @@ -351,7 +351,7 @@ describe('utils', () => { it('should construct the source from frame, which has only url specified', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( ' at Q\n' + ' at a\n' + ' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n', @@ -366,7 +366,7 @@ describe('utils', () => { it('should parse sourceURL correctly if it includes parentheses', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' + ' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' + ' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)', @@ -381,7 +381,7 @@ describe('utils', () => { it('should support Firefox stack', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' + 'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' + 'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513', diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 5abfcc8c8b7..2b3c949a4fc 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -54,10 +54,12 @@ import { formatDurationToMicrosecondsGranularity, gt, gte, - parseSourceFromComponentStack, - parseSourceFromOwnerStack, serializeToString, } from 'react-devtools-shared/src/backend/utils'; +import { + extractLocationFromComponentStack, + extractLocationFromOwnerStack, +} from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { cleanForBridge, copyWithDelete, @@ -6340,7 +6342,7 @@ export function attach( if (stackFrame === null) { return null; } - const source = parseSourceFromComponentStack(stackFrame); + const source = extractLocationFromComponentStack(stackFrame); fiberInstance.source = source; return source; } @@ -6369,7 +6371,7 @@ export function attach( // any intermediate utility functions. This won't point to the top of the component function // but it's at least somewhere within it. if (isError(unresolvedSource)) { - return (instance.source = parseSourceFromOwnerStack( + return (instance.source = extractLocationFromOwnerStack( (unresolvedSource: any), )); } @@ -6377,7 +6379,7 @@ export function attach( const idx = unresolvedSource.lastIndexOf('\n'); const lastLine = idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1); - return (instance.source = parseSourceFromComponentStack(lastLine)); + return (instance.source = extractLocationFromComponentStack(lastLine)); } // $FlowFixMe: refined. diff --git a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js index fdd7bce2f8d..36102dcf963 100644 --- a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js +++ b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js @@ -13,12 +13,9 @@ export function formatOwnerStack(error: Error): string { const prevPrepareStackTrace = Error.prepareStackTrace; // $FlowFixMe[incompatible-type] It does accept undefined. Error.prepareStackTrace = undefined; - const stack = error.stack; + let stack = error.stack; Error.prepareStackTrace = prevPrepareStackTrace; - return formatOwnerStackString(stack); -} -export function formatOwnerStackString(stack: string): string { if (stack.startsWith('Error: react-stack-top-frame\n')) { // V8's default formatting prefixes with the error message which we // don't want/need. diff --git a/packages/react-devtools-shared/src/backend/utils/index.js b/packages/react-devtools-shared/src/backend/utils/index.js index 490790e89d9..fcb8d448a0c 100644 --- a/packages/react-devtools-shared/src/backend/utils/index.js +++ b/packages/react-devtools-shared/src/backend/utils/index.js @@ -12,14 +12,11 @@ import {compareVersions} from 'compare-versions'; import {dehydrate} from 'react-devtools-shared/src/hydration'; import isArray from 'shared/isArray'; -import type {ReactFunctionLocation} from 'shared/ReactTypes'; import type {DehydratedData} from 'react-devtools-shared/src/frontend/types'; export {default as formatWithStyles} from './formatWithStyles'; export {default as formatConsoleArguments} from './formatConsoleArguments'; -import {formatOwnerStackString} from '../shared/DevToolsOwnerStack'; - // TODO: update this to the first React version that has a corresponding DevTools backend const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9'; export function hasAssignedBackend(version?: string): boolean { @@ -258,186 +255,6 @@ export const isReactNativeEnvironment = (): boolean => { return window.document == null; }; -function extractLocation(url: string): null | { - functionName?: string, - sourceURL: string, - line?: string, - column?: string, -} { - if (url.indexOf(':') === -1) { - return null; - } - - // remove any parentheses from start and end - const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, ''); - const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec( - withoutParentheses, - ); - - if (locationParts == null) { - return null; - } - - const functionName = ''; // TODO: Parse this in the regexp. - const [, , sourceURL, line, column] = locationParts; - return {functionName, sourceURL, line, column}; -} - -const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; -function parseSourceFromChromeStack( - stack: string, -): ReactFunctionLocation | null { - const frames = stack.split('\n'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const frame of frames) { - const sanitizedFrame = frame.trim(); - - const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/); - const possibleLocation = locationInParenthesesMatch - ? locationInParenthesesMatch[1] - : sanitizedFrame; - - const location = extractLocation(possibleLocation); - // Continue the search until at least sourceURL is found - if (location == null) { - continue; - } - - const {functionName, sourceURL, line = '1', column = '1'} = location; - - return [ - functionName || '', - sourceURL, - parseInt(line, 10), - parseInt(column, 10), - ]; - } - - return null; -} - -function parseSourceFromFirefoxStack( - stack: string, -): ReactFunctionLocation | null { - const frames = stack.split('\n'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const frame of frames) { - const sanitizedFrame = frame.trim(); - const frameWithoutFunctionName = sanitizedFrame.replace( - /((.*".+"[^@]*)?[^@]*)(?:@)/, - '', - ); - - const location = extractLocation(frameWithoutFunctionName); - // Continue the search until at least sourceURL is found - if (location == null) { - continue; - } - - const {functionName, sourceURL, line = '1', column = '1'} = location; - - return [ - functionName || '', - sourceURL, - parseInt(line, 10), - parseInt(column, 10), - ]; - } - - return null; -} - -export function parseSourceFromComponentStack( - componentStack: string, -): ReactFunctionLocation | null { - if (componentStack.match(CHROME_STACK_REGEXP)) { - return parseSourceFromChromeStack(componentStack); - } - - return parseSourceFromFirefoxStack(componentStack); -} - -let collectedLocation: ReactFunctionLocation | null = null; - -function collectStackTrace( - error: Error, - structuredStackTrace: CallSite[], -): string { - let result: null | ReactFunctionLocation = null; - // Collect structured stack traces from the callsites. - // We mirror how V8 serializes stack frames and how we later parse them. - for (let i = 0; i < structuredStackTrace.length; i++) { - const callSite = structuredStackTrace[i]; - const name = callSite.getFunctionName(); - if ( - name != null && - (name.includes('react_stack_bottom_frame') || - name.includes('react-stack-bottom-frame')) - ) { - // We pick the last frame that matches before the bottom frame since - // that will be immediately inside the component as opposed to some helper. - // If we don't find a bottom frame then we bail to string parsing. - collectedLocation = result; - // Skip everything after the bottom frame since it'll be internals. - break; - } else { - const sourceURL = callSite.getScriptNameOrSourceURL(); - const line = - // $FlowFixMe[prop-missing] - typeof callSite.getEnclosingLineNumber === 'function' - ? (callSite: any).getEnclosingLineNumber() - : callSite.getLineNumber(); - const col = - // $FlowFixMe[prop-missing] - typeof callSite.getEnclosingColumnNumber === 'function' - ? (callSite: any).getEnclosingColumnNumber() - : callSite.getColumnNumber(); - if (!sourceURL || !line || !col) { - // Skip eval etc. without source url. They don't have location. - continue; - } - result = [name, sourceURL, line, col]; - } - } - // At the same time we generate a string stack trace just in case someone - // else reads it. - const name = error.name || 'Error'; - const message = error.message || ''; - let stack = name + ': ' + message; - for (let i = 0; i < structuredStackTrace.length; i++) { - stack += '\n at ' + structuredStackTrace[i].toString(); - } - return stack; -} - -export function parseSourceFromOwnerStack( - error: Error, -): ReactFunctionLocation | null { - // First attempt to collected the structured data using prepareStackTrace. - collectedLocation = null; - const previousPrepare = Error.prepareStackTrace; - Error.prepareStackTrace = collectStackTrace; - let stack; - try { - stack = error.stack; - } catch (e) { - // $FlowFixMe[incompatible-type] It does accept undefined. - Error.prepareStackTrace = undefined; - stack = error.stack; - } finally { - Error.prepareStackTrace = previousPrepare; - } - if (collectedLocation !== null) { - return collectedLocation; - } - if (stack == null) { - return null; - } - // Fallback to parsing the string form. - const componentStack = formatOwnerStackString(stack); - return parseSourceFromComponentStack(componentStack); -} - // 0.123456789 => 0.123 // Expects high-resolution timestamp in milliseconds, like from performance.now() // Mainly used for optimizing the size of serialized profiling payload From 67b16d3fbe5b3a9f96be6d35020effec8f543c42 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 3 Aug 2025 21:56:41 -0400 Subject: [PATCH 3/5] DevTools is more lenient on the prefix --- .../react-devtools-shared/src/backend/utils/parseStackTrace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js index 1bcfb9e5d35..94e6659e145 100644 --- a/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js +++ b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js @@ -236,7 +236,7 @@ function collectStackTrace( // at filename:0:0 // at async filename:0:0 const chromeFrameRegExp = - /^ {3} at (?:(.+) \((?:(.+):(\d+):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; + /^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; // DEV-only cache of parsed and filtered stack frames. const stackTraceCache: WeakMap = __DEV__ From 4deb30c7474650bc3c7287ae44a360cf748df138 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 3 Aug 2025 21:57:51 -0400 Subject: [PATCH 4/5] We're now parsing function names --- .../react-devtools-shared/src/__tests__/utils-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/utils-test.js b/packages/react-devtools-shared/src/__tests__/utils-test.js index ebdb386b10c..83b31903e06 100644 --- a/packages/react-devtools-shared/src/__tests__/utils-test.js +++ b/packages/react-devtools-shared/src/__tests__/utils-test.js @@ -319,7 +319,7 @@ describe('utils', () => { 'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n', ), ).toEqual([ - '', + 'l', 'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js', 1, 10389, @@ -342,7 +342,7 @@ describe('utils', () => { ' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)', ), ).toEqual([ - '', + 'm', 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js', 5, 9236, @@ -372,7 +372,7 @@ describe('utils', () => { ' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)', ), ).toEqual([ - '', + 'HotReload', 'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js', 307, 11, @@ -387,7 +387,7 @@ describe('utils', () => { 'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513', ), ).toEqual([ - '', + 'tt', 'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js', 1, 165558, From 83755689662063106c3e7aed1311da8051444558 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 3 Aug 2025 22:02:33 -0400 Subject: [PATCH 5/5] The weakmap is not just for DEV of DevTools --- .../src/backend/utils/parseStackTrace.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js index 94e6659e145..92b4156de7f 100644 --- a/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js +++ b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js @@ -238,10 +238,7 @@ function collectStackTrace( const chromeFrameRegExp = /^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; -// DEV-only cache of parsed and filtered stack frames. -const stackTraceCache: WeakMap = __DEV__ - ? new WeakMap() - : (null: any); +const stackTraceCache: WeakMap = new WeakMap(); export function parseStackTrace( error: Error,