Files
react/packages/react-server/src/ReactFlightStackConfigV8.js
Sebastian Markbåge 65a46c7eeb [Flight] Track the function name that was called for I/O entries (#33392)
Stacked on #33390.

The stack trace doesn't include the thing you called when calling into
ignore listed content. We consider the ignore listed content
conceptually the abstraction that you called that's interesting.

This extracts the name of the first ignore listed function that was
called from user space. For example `"fetch"`. So we can know what kind
of request this is.

This could be enhanced and tweaked with heuristics in the future. For
example, when you create a Promise yourself and call I/O inside of it
like my `delay` examples, then we use that Promise as the I/O node but
its stack doesn't have the actual I/O performed. It might be better to
use the inner I/O node in that case. E.g. `setTimeout`. Currently I pick
the name from the first party code instead - in my example `delay`.

Another case that could be improved is the case where your whole
component is third-party. In that case we still log the I/O but it has
no context about what kind of I/O since the whole stack is ignored it
just gets the component name for example. We could for example look at
the first name that is in a different package than the package name of
the ignored listed component. So if
`node_modules/my-component-library/index.js` calls into
`node_modules/mysql/connection.js` then we could use the name from the
inner.
2025-06-03 15:04:28 -04:00

210 lines
7.1 KiB
JavaScript

/**
* 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} from 'shared/ReactTypes';
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 === '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, 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;
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;
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
}
}
// 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.
// at name (filename:0:0)
// at filename:0:0
// at async filename:0:0
const frameRegExp =
/^ {3} at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
// DEV-only cache of parsed and filtered stack frames.
const stackTraceCache: WeakMap<Error, ReactStackTrace> = __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 {
// 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;
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. 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.
stack = stack.slice(29);
}
let 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 = frameRegExp.exec(frames[i]);
if (!parsed) {
continue;
}
let name = parsed[1] || '';
if (name === '<anonymous>') {
name = '';
}
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]);
}
stackTraceCache.set(error, parsedFrames);
return parsedFrames;
}