mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[Flight] Parse Stack Trace from Structured CallSite if available (#33135)
This is first step to include more enclosing line/column in the parsed data. We install our own `prepareStackTrace` to collect structured callsite data and only fall back to parsing the string if it was already evaluated or if `prepareStackTrace` doesn't work in this environment. We still mirror the default V8 format for encoding the function name part. A lot of this is covered by tests already.
This commit is contained in:
parent
53c9f81049
commit
0ff1d13b80
17
packages/react-server/src/ReactFlightServer.js
vendored
17
packages/react-server/src/ReactFlightServer.js
vendored
|
|
@ -152,11 +152,27 @@ function defaultFilterStackFrame(
|
|||
);
|
||||
}
|
||||
|
||||
// DEV-only cache of parsed and filtered stack frames.
|
||||
const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
|
||||
? new WeakMap()
|
||||
: (null: any);
|
||||
|
||||
function filterStackTrace(
|
||||
request: Request,
|
||||
error: Error,
|
||||
skipFrames: number,
|
||||
): ReactStackTrace {
|
||||
const existing = stackTraceCache.get(error);
|
||||
if (existing !== undefined) {
|
||||
// Return a clone because the Flight protocol isn't yet resilient to deduping
|
||||
// objects in the debug info. TODO: Support deduping stacks.
|
||||
const clone = existing.slice(0);
|
||||
for (let i = 0; i < clone.length; i++) {
|
||||
// $FlowFixMe[invalid-tuple-arity]
|
||||
clone[i] = clone[i].slice(0);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
// Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
|
||||
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
|
||||
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
|
||||
|
|
@ -183,6 +199,7 @@ function filterStackTrace(
|
|||
i--;
|
||||
}
|
||||
}
|
||||
stackTraceCache.set(error, stack);
|
||||
return stack;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,23 +9,104 @@
|
|||
|
||||
import type {ReactStackTrace} from 'shared/ReactTypes';
|
||||
|
||||
import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace';
|
||||
let framesToSkip: number = 0;
|
||||
let collectedStackTrace: null | ReactStackTrace = null;
|
||||
|
||||
function getStack(error: Error): string {
|
||||
// We override Error.prepareStackTrace with our own version that normalizes
|
||||
// the stack to V8 formatting even if the server uses other formatting.
|
||||
// It also ensures that source maps are NOT applied to this since that can
|
||||
// be slow we're better off doing that lazily from the client instead of
|
||||
// eagerly on the server. If the stack has already been read, then we might
|
||||
// not get a normalized stack and it might still have been source mapped.
|
||||
const previousPrepare = Error.prepareStackTrace;
|
||||
Error.prepareStackTrace = DefaultPrepareStackTrace;
|
||||
try {
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
return String(error.stack);
|
||||
} finally {
|
||||
Error.prepareStackTrace = previousPrepare;
|
||||
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 === 'react-stack-bottom-frame') {
|
||||
// Skip everything after the bottom frame since it'll be internals.
|
||||
break;
|
||||
} else if (callSite.isNative()) {
|
||||
result.push([name, '', 0, 0]);
|
||||
} 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() && !filename) {
|
||||
const origin = callSite.getEvalOrigin();
|
||||
if (origin) {
|
||||
filename = origin.toString() + ', <anonymous>';
|
||||
}
|
||||
}
|
||||
const line = callSite.getLineNumber() || 0;
|
||||
const col = callSite.getColumnNumber() || 0;
|
||||
result.push([name, filename, line, col]);
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
collectedStackTrace = result;
|
||||
return stack;
|
||||
}
|
||||
|
||||
// This matches either of these V8 formats.
|
||||
|
|
@ -39,7 +120,32 @@ export function parseStackTrace(
|
|||
error: Error,
|
||||
skipFrames: number,
|
||||
): ReactStackTrace {
|
||||
let stack = getStack(error);
|
||||
// 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 {
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
stack = String(error.stack);
|
||||
} finally {
|
||||
Error.prepareStackTrace = previousPrepare;
|
||||
}
|
||||
|
||||
if (collectedStackTrace !== null) {
|
||||
const result = collectedStackTrace;
|
||||
collectedStackTrace = null;
|
||||
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. This works best if the
|
||||
// environment just uses default V8 formatting and no source mapping.
|
||||
|
||||
if (stack.startsWith('Error: react-stack-top-frame\n')) {
|
||||
// V8's default formatting prefixes with the error message which we
|
||||
// don't want/need.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user