@samitouri / QOS-React / commits / 557745eb0b

[DevTools] Add structure full stack parsing to DevTools (#34093)

We'll need complete parsing of stack traces for both owner stacks and async debug info so we need to expand the stack parsing capabilities a bit. This refactors the source location extraction to use some helpers we can use for other things too. 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.

Sebastian Markbåge committed Aug 4, 2025 at 09:37 UTC 557745eb0b50c2c7b126813fdae9c1929afc87f9
5 files changed +351 -204
packages/react-devtools-shared/src/__tests__/utils-test.js
+12 -12
@@ -19,8 +19,8 @@ import {
19 formatWithStyles,
20 gt,
21 gte,
22 - parseSourceFromComponentStack,
22 } from 'react-devtools-shared/src/backend/utils';
23 +import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
24 import {
25 REACT_SUSPENSE_LIST_TYPE as SuspenseList,
26 REACT_STRICT_MODE_TYPE as StrictMode,
@@ -306,20 +306,20 @@ describe('utils', () => {
306 });
307 });
308
309 - describe('parseSourceFromComponentStack', () => {
309 + describe('extractLocationFromComponentStack', () => {
310 it('should return null if passed empty string', () => {
311 - expect(parseSourceFromComponentStack('')).toEqual(null);
311 + expect(extractLocationFromComponentStack('')).toEqual(null);
312 });
313
314 it('should construct the source from the first frame if available', () => {
315 expect(
316 - parseSourceFromComponentStack(
316 + extractLocationFromComponentStack(
317 'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' +
318 'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' +
319 'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n',
320 ),
321 ).toEqual([
322 - '',
322 + 'l',
323 'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
324 1,
325 10389,
@@ -328,7 +328,7 @@ describe('utils', () => {
328
329 it('should construct the source from highest available frame', () => {
330 expect(
331 - parseSourceFromComponentStack(
331 + extractLocationFromComponentStack(
332 ' at Q\n' +
333 ' at a\n' +
334 ' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' +
@@ -342,7 +342,7 @@ describe('utils', () => {
342 ' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)',
343 ),
344 ).toEqual([
345 - '',
345 + 'm',
346 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
347 5,
348 9236,
@@ -351,7 +351,7 @@ describe('utils', () => {
351
352 it('should construct the source from frame, which has only url specified', () => {
353 expect(
354 - parseSourceFromComponentStack(
354 + extractLocationFromComponentStack(
355 ' at Q\n' +
356 ' at a\n' +
357 ' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n',
@@ -366,13 +366,13 @@ describe('utils', () => {
366
367 it('should parse sourceURL correctly if it includes parentheses', () => {
368 expect(
369 - parseSourceFromComponentStack(
369 + extractLocationFromComponentStack(
370 'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' +
371 ' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' +
372 ' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)',
373 ),
374 ).toEqual([
375 - '',
375 + 'HotReload',
376 'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
377 307,
378 11,
@@ -381,13 +381,13 @@ describe('utils', () => {
381
382 it('should support Firefox stack', () => {
383 expect(
384 - parseSourceFromComponentStack(
384 + extractLocationFromComponentStack(
385 'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' +
386 'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' +
387 'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513',
388 ),
389 ).toEqual([
390 - '',
390 + 'tt',
391 'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
392 1,
393 165558,
packages/react-devtools-shared/src/backend/fiber/renderer.js
+7 -5
@@ -54,10 +54,12 @@ import {
54 formatDurationToMicrosecondsGranularity,
55 gt,
56 gte,
57 - parseSourceFromComponentStack,
58 - parseSourceFromOwnerStack,
57 serializeToString,
58 } from 'react-devtools-shared/src/backend/utils';
59 +import {
60 + extractLocationFromComponentStack,
61 + extractLocationFromOwnerStack,
62 +} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
63 import {
64 cleanForBridge,
65 copyWithDelete,
@@ -6340,7 +6342,7 @@ export function attach(
6342 if (stackFrame === null) {
6343 return null;
6344 }
6343 - const source = parseSourceFromComponentStack(stackFrame);
6345 + const source = extractLocationFromComponentStack(stackFrame);
6346 fiberInstance.source = source;
6347 return source;
6348 }
@@ -6369,7 +6371,7 @@ export function attach(
6371 // any intermediate utility functions. This won't point to the top of the component function
6372 // but it's at least somewhere within it.
6373 if (isError(unresolvedSource)) {
6372 - return (instance.source = parseSourceFromOwnerStack(
6374 + return (instance.source = extractLocationFromOwnerStack(
6375 (unresolvedSource: any),
6376 ));
6377 }
@@ -6377,7 +6379,7 @@ export function attach(
6379 const idx = unresolvedSource.lastIndexOf('\n');
6380 const lastLine =
6381 idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1);
6380 - return (instance.source = parseSourceFromComponentStack(lastLine));
6382 + return (instance.source = extractLocationFromComponentStack(lastLine));
6383 }
6384
6385 // $FlowFixMe: refined.
packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js
+1 -4
@@ -13,12 +13,9 @@ export function formatOwnerStack(error: Error): string {
13 const prevPrepareStackTrace = Error.prepareStackTrace;
14 // $FlowFixMe[incompatible-type] It does accept undefined.
15 Error.prepareStackTrace = undefined;
16 - const stack = error.stack;
16 + let stack = error.stack;
17 Error.prepareStackTrace = prevPrepareStackTrace;
18 - return formatOwnerStackString(stack);
19 -}
18
21 -export function formatOwnerStackString(stack: string): string {
19 if (stack.startsWith('Error: react-stack-top-frame\n')) {
20 // V8's default formatting prefixes with the error message which we
21 // don't want/need.
packages/react-devtools-shared/src/backend/utils/index.js
-183
@@ -12,14 +12,11 @@ import {compareVersions} from 'compare-versions';
12 import {dehydrate} from 'react-devtools-shared/src/hydration';
13 import isArray from 'shared/isArray';
14
15 -import type {ReactFunctionLocation} from 'shared/ReactTypes';
15 import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
16
17 export {default as formatWithStyles} from './formatWithStyles';
18 export {default as formatConsoleArguments} from './formatConsoleArguments';
19
21 -import {formatOwnerStackString} from '../shared/DevToolsOwnerStack';
22 -
20 // TODO: update this to the first React version that has a corresponding DevTools backend
21 const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
22 export function hasAssignedBackend(version?: string): boolean {
@@ -258,186 +255,6 @@ export const isReactNativeEnvironment = (): boolean => {
255 return window.document == null;
256 };
257
261 -function extractLocation(url: string): null | {
262 - functionName?: string,
263 - sourceURL: string,
264 - line?: string,
265 - column?: string,
266 -} {
267 - if (url.indexOf(':') === -1) {
268 - return null;
269 - }
270 -
271 - // remove any parentheses from start and end
272 - const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, '');
273 - const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec(
274 - withoutParentheses,
275 - );
276 -
277 - if (locationParts == null) {
278 - return null;
279 - }
280 -
281 - const functionName = ''; // TODO: Parse this in the regexp.
282 - const [, , sourceURL, line, column] = locationParts;
283 - return {functionName, sourceURL, line, column};
284 -}
285 -
286 -const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
287 -function parseSourceFromChromeStack(
288 - stack: string,
289 -): ReactFunctionLocation | null {
290 - const frames = stack.split('\n');
291 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
292 - for (const frame of frames) {
293 - const sanitizedFrame = frame.trim();
294 -
295 - const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/);
296 - const possibleLocation = locationInParenthesesMatch
297 - ? locationInParenthesesMatch[1]
298 - : sanitizedFrame;
299 -
300 - const location = extractLocation(possibleLocation);
301 - // Continue the search until at least sourceURL is found
302 - if (location == null) {
303 - continue;
304 - }
305 -
306 - const {functionName, sourceURL, line = '1', column = '1'} = location;
307 -
308 - return [
309 - functionName || '',
310 - sourceURL,
311 - parseInt(line, 10),
312 - parseInt(column, 10),
313 - ];
314 - }
315 -
316 - return null;
317 -}
318 -
319 -function parseSourceFromFirefoxStack(
320 - stack: string,
321 -): ReactFunctionLocation | null {
322 - const frames = stack.split('\n');
323 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
324 - for (const frame of frames) {
325 - const sanitizedFrame = frame.trim();
326 - const frameWithoutFunctionName = sanitizedFrame.replace(
327 - /((.*".+"[^@]*)?[^@]*)(?:@)/,
328 - '',
329 - );
330 -
331 - const location = extractLocation(frameWithoutFunctionName);
332 - // Continue the search until at least sourceURL is found
333 - if (location == null) {
334 - continue;
335 - }
336 -
337 - const {functionName, sourceURL, line = '1', column = '1'} = location;
338 -
339 - return [
340 - functionName || '',
341 - sourceURL,
342 - parseInt(line, 10),
343 - parseInt(column, 10),
344 - ];
345 - }
346 -
347 - return null;
348 -}
349 -
350 -export function parseSourceFromComponentStack(
351 - componentStack: string,
352 -): ReactFunctionLocation | null {
353 - if (componentStack.match(CHROME_STACK_REGEXP)) {
354 - return parseSourceFromChromeStack(componentStack);
355 - }
356 -
357 - return parseSourceFromFirefoxStack(componentStack);
358 -}
359 -
360 -let collectedLocation: ReactFunctionLocation | null = null;
361 -
362 -function collectStackTrace(
363 - error: Error,
364 - structuredStackTrace: CallSite[],
365 -): string {
366 - let result: null | ReactFunctionLocation = null;
367 - // Collect structured stack traces from the callsites.
368 - // We mirror how V8 serializes stack frames and how we later parse them.
369 - for (let i = 0; i < structuredStackTrace.length; i++) {
370 - const callSite = structuredStackTrace[i];
371 - const name = callSite.getFunctionName();
372 - if (
373 - name != null &&
374 - (name.includes('react_stack_bottom_frame') ||
375 - name.includes('react-stack-bottom-frame'))
376 - ) {
377 - // We pick the last frame that matches before the bottom frame since
378 - // that will be immediately inside the component as opposed to some helper.
379 - // If we don't find a bottom frame then we bail to string parsing.
380 - collectedLocation = result;
381 - // Skip everything after the bottom frame since it'll be internals.
382 - break;
383 - } else {
384 - const sourceURL = callSite.getScriptNameOrSourceURL();
385 - const line =
386 - // $FlowFixMe[prop-missing]
387 - typeof callSite.getEnclosingLineNumber === 'function'
388 - ? (callSite: any).getEnclosingLineNumber()
389 - : callSite.getLineNumber();
390 - const col =
391 - // $FlowFixMe[prop-missing]
392 - typeof callSite.getEnclosingColumnNumber === 'function'
393 - ? (callSite: any).getEnclosingColumnNumber()
394 - : callSite.getColumnNumber();
395 - if (!sourceURL || !line || !col) {
396 - // Skip eval etc. without source url. They don't have location.
397 - continue;
398 - }
399 - result = [name, sourceURL, line, col];
400 - }
401 - }
402 - // At the same time we generate a string stack trace just in case someone
403 - // else reads it.
404 - const name = error.name || 'Error';
405 - const message = error.message || '';
406 - let stack = name + ': ' + message;
407 - for (let i = 0; i < structuredStackTrace.length; i++) {
408 - stack += '\n at ' + structuredStackTrace[i].toString();
409 - }
410 - return stack;
411 -}
412 -
413 -export function parseSourceFromOwnerStack(
414 - error: Error,
415 -): ReactFunctionLocation | null {
416 - // First attempt to collected the structured data using prepareStackTrace.
417 - collectedLocation = null;
418 - const previousPrepare = Error.prepareStackTrace;
419 - Error.prepareStackTrace = collectStackTrace;
420 - let stack;
421 - try {
422 - stack = error.stack;
423 - } catch (e) {
424 - // $FlowFixMe[incompatible-type] It does accept undefined.
425 - Error.prepareStackTrace = undefined;
426 - stack = error.stack;
427 - } finally {
428 - Error.prepareStackTrace = previousPrepare;
429 - }
430 - if (collectedLocation !== null) {
431 - return collectedLocation;
432 - }
433 - if (stack == null) {
434 - return null;
435 - }
436 - // Fallback to parsing the string form.
437 - const componentStack = formatOwnerStackString(stack);
438 - return parseSourceFromComponentStack(componentStack);
439 -}
440 -
258 // 0.123456789 => 0.123
259 // Expects high-resolution timestamp in milliseconds, like from performance.now()
260 // Mainly used for optimizing the size of serialized profiling payload
packages/react-devtools-shared/src/backend/utils/parseStackTrace.js new
+331
@@ -0,0 +1,331 @@
1 +/**
2 +/**
3 + * Copyright (c) Meta Platforms, Inc. and affiliates.
4 + *
5 + * This source code is licensed under the MIT license found in the
6 + * LICENSE file in the root directory of this source tree.
7 + *
8 + * @flow
9 + */
10 +
11 +import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes';
12 +
13 +function parseStackTraceFromChromeStack(
14 + stack: string,
15 + skipFrames: number,
16 +): ReactStackTrace {
17 + if (stack.startsWith('Error: react-stack-top-frame\n')) {
18 + // V8's default formatting prefixes with the error message which we
19 + // don't want/need.
20 + stack = stack.slice(29);
21 + }
22 + let idx = stack.indexOf('react_stack_bottom_frame');
23 + if (idx === -1) {
24 + idx = stack.indexOf('react-stack-bottom-frame');
25 + }
26 + if (idx !== -1) {
27 + idx = stack.lastIndexOf('\n', idx);
28 + }
29 + if (idx !== -1) {
30 + // Cut off everything after the bottom frame since it'll be internals.
31 + stack = stack.slice(0, idx);
32 + }
33 + const frames = stack.split('\n');
34 + const parsedFrames: ReactStackTrace = [];
35 + // We skip top frames here since they may or may not be parseable but we
36 + // want to skip the same number of frames regardless. I.e. we can't do it
37 + // in the caller.
38 + for (let i = skipFrames; i < frames.length; i++) {
39 + const parsed = chromeFrameRegExp.exec(frames[i]);
40 + if (!parsed) {
41 + continue;
42 + }
43 + let name = parsed[1] || '';
44 + let isAsync = parsed[8] === 'async ';
45 + if (name === '<anonymous>') {
46 + name = '';
47 + } else if (name.startsWith('async ')) {
48 + name = name.slice(5);
49 + isAsync = true;
50 + }
51 + let filename = parsed[2] || parsed[5] || '';
52 + if (filename === '<anonymous>') {
53 + filename = '';
54 + }
55 + const line = +(parsed[3] || parsed[6]);
56 + const col = +(parsed[4] || parsed[7]);
57 + parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
58 + }
59 + return parsedFrames;
60 +}
61 +
62 +const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/;
63 +function parseStackTraceFromFirefoxStack(
64 + stack: string,
65 + skipFrames: number,
66 +): ReactStackTrace {
67 + let idx = stack.indexOf('react_stack_bottom_frame');
68 + if (idx === -1) {
69 + idx = stack.indexOf('react-stack-bottom-frame');
70 + }
71 + if (idx !== -1) {
72 + idx = stack.lastIndexOf('\n', idx);
73 + }
74 + if (idx !== -1) {
75 + // Cut off everything after the bottom frame since it'll be internals.
76 + stack = stack.slice(0, idx);
77 + }
78 + const frames = stack.split('\n');
79 + const parsedFrames: ReactStackTrace = [];
80 + // We skip top frames here since they may or may not be parseable but we
81 + // want to skip the same number of frames regardless. I.e. we can't do it
82 + // in the caller.
83 + for (let i = skipFrames; i < frames.length; i++) {
84 + const parsed = firefoxFrameRegExp.exec(frames[i]);
85 + if (!parsed) {
86 + continue;
87 + }
88 + const name = parsed[1] || '';
89 + const filename = parsed[2] || '';
90 + const line = +parsed[3];
91 + const col = +parsed[4];
92 + parsedFrames.push([name, filename, line, col, 0, 0, false]);
93 + }
94 + return parsedFrames;
95 +}
96 +
97 +const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
98 +export function parseStackTraceFromString(
99 + stack: string,
100 + skipFrames: number,
101 +): ReactStackTrace {
102 + if (stack.match(CHROME_STACK_REGEXP)) {
103 + return parseStackTraceFromChromeStack(stack, skipFrames);
104 + }
105 + return parseStackTraceFromFirefoxStack(stack, skipFrames);
106 +}
107 +
108 +let framesToSkip: number = 0;
109 +let collectedStackTrace: null | ReactStackTrace = null;
110 +
111 +const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
112 +
113 +function getMethodCallName(callSite: CallSite): string {
114 + const typeName = callSite.getTypeName();
115 + const methodName = callSite.getMethodName();
116 + const functionName = callSite.getFunctionName();
117 + let result = '';
118 + if (functionName) {
119 + if (
120 + typeName &&
121 + identifierRegExp.test(functionName) &&
122 + functionName !== typeName
123 + ) {
124 + result += typeName + '.';
125 + }
126 + result += functionName;
127 + if (
128 + methodName &&
129 + functionName !== methodName &&
130 + !functionName.endsWith('.' + methodName) &&
131 + !functionName.endsWith(' ' + methodName)
132 + ) {
133 + result += ' [as ' + methodName + ']';
134 + }
135 + } else {
136 + if (typeName) {
137 + result += typeName + '.';
138 + }
139 + if (methodName) {
140 + result += methodName;
141 + } else {
142 + result += '<anonymous>';
143 + }
144 + }
145 + return result;
146 +}
147 +
148 +function collectStackTrace(
149 + error: Error,
150 + structuredStackTrace: CallSite[],
151 +): string {
152 + const result: ReactStackTrace = [];
153 + // Collect structured stack traces from the callsites.
154 + // We mirror how V8 serializes stack frames and how we later parse them.
155 + for (let i = framesToSkip; i < structuredStackTrace.length; i++) {
156 + const callSite = structuredStackTrace[i];
157 + let name = callSite.getFunctionName() || '<anonymous>';
158 + if (
159 + name.includes('react_stack_bottom_frame') ||
160 + name.includes('react-stack-bottom-frame')
161 + ) {
162 + // Skip everything after the bottom frame since it'll be internals.
163 + break;
164 + } else if (callSite.isNative()) {
165 + // $FlowFixMe[prop-missing]
166 + const isAsync = callSite.isAsync();
167 + result.push([name, '', 0, 0, 0, 0, isAsync]);
168 + } else {
169 + // We encode complex function calls as if they're part of the function
170 + // name since we cannot simulate the complex ones and they look the same
171 + // as function names in UIs on the client as well as stacks.
172 + if (callSite.isConstructor()) {
173 + name = 'new ' + name;
174 + } else if (!callSite.isToplevel()) {
175 + name = getMethodCallName(callSite);
176 + }
177 + if (name === '<anonymous>') {
178 + name = '';
179 + }
180 + let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
181 + if (filename === '<anonymous>') {
182 + filename = '';
183 + if (callSite.isEval()) {
184 + const origin = callSite.getEvalOrigin();
185 + if (origin) {
186 + filename = origin.toString() + ', <anonymous>';
187 + }
188 + }
189 + }
190 + const line = callSite.getLineNumber() || 0;
191 + const col = callSite.getColumnNumber() || 0;
192 + const enclosingLine: number =
193 + // $FlowFixMe[prop-missing]
194 + typeof callSite.getEnclosingLineNumber === 'function'
195 + ? (callSite: any).getEnclosingLineNumber() || 0
196 + : 0;
197 + const enclosingCol: number =
198 + // $FlowFixMe[prop-missing]
199 + typeof callSite.getEnclosingColumnNumber === 'function'
200 + ? (callSite: any).getEnclosingColumnNumber() || 0
201 + : 0;
202 + // $FlowFixMe[prop-missing]
203 + const isAsync = callSite.isAsync();
204 + result.push([
205 + name,
206 + filename,
207 + line,
208 + col,
209 + enclosingLine,
210 + enclosingCol,
211 + isAsync,
212 + ]);
213 + }
214 + }
215 + collectedStackTrace = result;
216 +
217 + // At the same time we generate a string stack trace just in case someone
218 + // else reads it. Ideally, we'd call the previous prepareStackTrace to
219 + // ensure it's in the expected format but it's common for that to be
220 + // source mapped and since we do a lot of eager parsing of errors, it
221 + // would be slow in those environments. We could maybe just rely on those
222 + // environments having to disable source mapping globally to speed things up.
223 + // For now, we just generate a default V8 formatted stack trace without
224 + // source mapping as a fallback.
225 + const name = error.name || 'Error';
226 + const message = error.message || '';
227 + let stack = name + ': ' + message;
228 + for (let i = 0; i < structuredStackTrace.length; i++) {
229 + stack += '\n at ' + structuredStackTrace[i].toString();
230 + }
231 + return stack;
232 +}
233 +
234 +// This matches either of these V8 formats.
235 +// at name (filename:0:0)
236 +// at filename:0:0
237 +// at async filename:0:0
238 +const chromeFrameRegExp =
239 + /^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
240 +
241 +const stackTraceCache: WeakMap<Error, ReactStackTrace> = new WeakMap();
242 +
243 +export function parseStackTrace(
244 + error: Error,
245 + skipFrames: number,
246 +): ReactStackTrace {
247 + // We can only get structured data out of error objects once. So we cache the information
248 + // so we can get it again each time. It also helps performance when the same error is
249 + // referenced more than once.
250 + const existing = stackTraceCache.get(error);
251 + if (existing !== undefined) {
252 + return existing;
253 + }
254 + // We override Error.prepareStackTrace with our own version that collects
255 + // the structured data. We need more information than the raw stack gives us
256 + // and we need to ensure that we don't get the source mapped version.
257 + collectedStackTrace = null;
258 + framesToSkip = skipFrames;
259 + const previousPrepare = Error.prepareStackTrace;
260 + Error.prepareStackTrace = collectStackTrace;
261 + let stack;
262 + try {
263 + stack = String(error.stack);
264 + } finally {
265 + Error.prepareStackTrace = previousPrepare;
266 + }
267 +
268 + if (collectedStackTrace !== null) {
269 + const result = collectedStackTrace;
270 + collectedStackTrace = null;
271 + stackTraceCache.set(error, result);
272 + return result;
273 + }
274 +
275 + // If the stack has already been read, or this is not actually a V8 compatible
276 + // engine then we might not get a normalized stack and it might still have been
277 + // source mapped. Regardless we try our best to parse it.
278 +
279 + const parsedFrames = parseStackTraceFromString(stack, skipFrames);
280 + stackTraceCache.set(error, parsedFrames);
281 + return parsedFrames;
282 +}
283 +
284 +export function extractLocationFromOwnerStack(
285 + error: Error,
286 +): ReactFunctionLocation | null {
287 + const stackTrace = parseStackTrace(error, 0);
288 + const stack = error.stack;
289 + if (
290 + !stack.includes('react_stack_bottom_frame') &&
291 + !stack.includes('react-stack-bottom-frame')
292 + ) {
293 + // This didn't have a bottom to it, we can't trust it.
294 + return null;
295 + }
296 + // We start from the bottom since that will have the best location for the owner itself.
297 + for (let i = stackTrace.length - 1; i >= 0; i--) {
298 + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
299 + // Take the first match with a colon in the file name.
300 + if (fileName.indexOf(':') !== -1) {
301 + return [
302 + functionName,
303 + fileName,
304 + // Use enclosing line if available, since that points to the start of the function.
305 + encLine || line,
306 + encCol || col,
307 + ];
308 + }
309 + }
310 + return null;
311 +}
312 +
313 +export function extractLocationFromComponentStack(
314 + stack: string,
315 +): ReactFunctionLocation | null {
316 + const stackTrace = parseStackTraceFromString(stack, 0);
317 + for (let i = 0; i < stackTrace.length; i++) {
318 + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
319 + // Take the first match with a colon in the file name.
320 + if (fileName.indexOf(':') !== -1) {
321 + return [
322 + functionName,
323 + fileName,
324 + // Use enclosing line if available. (Never the case here because we parse from string.)
325 + encLine || line,
326 + encCol || col,
327 + ];
328 + }
329 + }
330 + return null;
331 +}