mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[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.
This commit is contained in:
committed by
GitHub
parent
d3f800d47a
commit
557745eb0b
@@ -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,20 +306,20 @@ 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',
|
||||
),
|
||||
).toEqual([
|
||||
'',
|
||||
'l',
|
||||
'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
|
||||
1,
|
||||
10389,
|
||||
@@ -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' +
|
||||
@@ -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,
|
||||
@@ -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,13 +366,13 @@ 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)',
|
||||
),
|
||||
).toEqual([
|
||||
'',
|
||||
'HotReload',
|
||||
'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
|
||||
307,
|
||||
11,
|
||||
@@ -381,13 +381,13 @@ 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',
|
||||
),
|
||||
).toEqual([
|
||||
'',
|
||||
'tt',
|
||||
'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
|
||||
1,
|
||||
165558,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
331
packages/react-devtools-shared/src/backend/utils/parseStackTrace.js
vendored
Normal file
331
packages/react-devtools-shared/src/backend/utils/parseStackTrace.js
vendored
Normal file
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
/**
|
||||
* 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 === '<anonymous>') {
|
||||
name = '';
|
||||
} else if (name.startsWith('async ')) {
|
||||
name = name.slice(5);
|
||||
isAsync = true;
|
||||
}
|
||||
let filename = parsed[2] || parsed[5] || '';
|
||||
if (filename === '<anonymous>') {
|
||||
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 += '<anonymous>';
|
||||
}
|
||||
}
|
||||
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() || '<anonymous>';
|
||||
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 === '<anonymous>') {
|
||||
name = '';
|
||||
}
|
||||
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
|
||||
if (filename === '<anonymous>') {
|
||||
filename = '';
|
||||
if (callSite.isEval()) {
|
||||
const origin = callSite.getEvalOrigin();
|
||||
if (origin) {
|
||||
filename = origin.toString() + ', <anonymous>';
|
||||
}
|
||||
}
|
||||
}
|
||||
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 =
|
||||
/^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
|
||||
|
||||
const stackTraceCache: WeakMap<Error, ReactStackTrace> = new WeakMap();
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user