[Flight] Respect displayName of Promise instances on the server (#34825)

This lets you assign a name to a Promise that's passed into first party
code from third party since it otherwise would have no other stack frame
to indicate its name since the whole creation stack would be in third
party.

We already respect the `displayName` on the client but it's more
complicated on the server because we don't only consider the exact
instance passed to `use()` but the whole await sequence and we can pick
any Promise along the way for consideration. Therefore this also adds a
change where we pick the Promise node for consideration if it has a name
but no stack. Where we otherwise would've picked the I/O node.

Another thing that this PR does is treat anonymous stack frames (empty
url) as third party for purposes of heuristics like "hasUnfilteredFrame"
and the name assignment. This lets you include these in the actual
generated stacks (by overriding `filterStackFrame`) but we don't
actually want them to be considered first party code in the heuristics
since it ends up favoring those stacks and using internals like
`Function.all` in name assignment.
This commit is contained in:
Sebastian Markbåge
2025-10-13 12:29:00 -04:00
committed by GitHub
parent d7215b4970
commit 026abeaa5f
2 changed files with 613 additions and 446 deletions

View File

@@ -252,7 +252,11 @@ function findCalledFunctionNameFromStackTrace(
const url = devirtualizeURL(callsite[1]);
const lineNumber = callsite[2];
const columnNumber = callsite[3];
if (filterStackFrame(url, functionName, lineNumber, columnNumber)) {
if (
filterStackFrame(url, functionName, lineNumber, columnNumber) &&
// Don't consider anonymous code first party even if the filter wants to include them in the stack.
url !== ''
) {
if (bestMatch === '') {
// If we had no good stack frames for internal calls, just use the last
// first party function name.
@@ -308,7 +312,10 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
const isAsync = callsite[6];
if (
!isAsync &&
filterStackFrame(url, functionName, lineNumber, columnNumber)
filterStackFrame(url, functionName, lineNumber, columnNumber) &&
// Ignore anonymous stack frames like internals. They are also not in first party
// code even though it might be useful to include them in the final stack.
url !== ''
) {
return true;
}
@@ -367,7 +374,10 @@ export function isAwaitInUserspace(
const url = devirtualizeURL(callsite[1]);
const lineNumber = callsite[2];
const columnNumber = callsite[3];
return filterStackFrame(url, functionName, lineNumber, columnNumber);
return (
filterStackFrame(url, functionName, lineNumber, columnNumber) &&
url !== ''
);
}
return false;
}
@@ -2347,6 +2357,7 @@ function visitAsyncNode(
}
const awaited = node.awaited;
let match: void | null | PromiseNode | IONode = previousIONode;
const promise = node.promise.deref();
if (awaited !== null) {
const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);
if (ioNode === undefined) {
@@ -2361,17 +2372,27 @@ function visitAsyncNode(
if (ioNode.tag === PROMISE_NODE) {
// If the ioNode was a Promise, then that means we found one in user space since otherwise
// we would've returned an IO node. We assume this has the best stack.
// Note: This might also be a Promise with a displayName but potentially a worse stack.
// We could potentially favor the outer Promise if it has a stack but not the inner.
match = ioNode;
} else if (
node.stack === null ||
!hasUnfilteredFrame(request, node.stack)
(node.stack !== null && hasUnfilteredFrame(request, node.stack)) ||
(promise !== undefined &&
// $FlowFixMe[prop-missing]
typeof promise.displayName === 'string' &&
(ioNode.stack === null ||
!hasUnfilteredFrame(request, ioNode.stack)))
) {
// If this Promise has a stack trace then we favor that over the I/O node since we're
// mainly dealing with Promises as the abstraction.
// If it has no stack but at least has a displayName and the io doesn't have a better
// stack anyway, then also use this Promise instead since at least it has a name.
match = node;
} else {
// If this Promise was created inside only third party code, then try to use
// the inner I/O node instead. This could happen if third party calls into first
// party to perform some I/O.
match = ioNode;
} else {
match = node;
}
} else if (request.status === ABORTING) {
if (node.start < request.abortTime && node.end > request.abortTime) {
@@ -2379,8 +2400,11 @@ function visitAsyncNode(
// Promise that was aborted. This won't necessarily have I/O associated with it but
// it's a point of interest.
if (
node.stack !== null &&
hasUnfilteredFrame(request, node.stack)
(node.stack !== null &&
hasUnfilteredFrame(request, node.stack)) ||
(promise !== undefined &&
// $FlowFixMe[prop-missing]
typeof promise.displayName === 'string')
) {
match = node;
}
@@ -2389,7 +2413,6 @@ function visitAsyncNode(
}
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
// the thing that generated this node and its virtual children.
const promise = node.promise.deref();
if (promise !== undefined) {
const debugInfo = promise._debugInfo;
if (debugInfo != null && !visited.has(debugInfo)) {
@@ -4497,17 +4520,33 @@ function serializeIONode(
let stack = null;
let name = '';
if (ioNode.promise !== null) {
// Pick an explicit name from the Promise itself if it exists.
// Note that we don't use the promiseRef passed in since that's sometimes the awaiting Promise
// which is the value observed but it's likely not the one with the name on it.
const promise = ioNode.promise.deref();
if (
promise !== undefined &&
// $FlowFixMe[prop-missing]
typeof promise.displayName === 'string'
) {
name = promise.displayName;
}
}
if (ioNode.stack !== null) {
// The stack can contain some leading internal frames for the construction of the promise that we skip.
const fullStack = stripLeadingPromiseCreationFrames(ioNode.stack);
stack = filterStackTrace(request, fullStack);
name = findCalledFunctionNameFromStackTrace(request, fullStack);
// The name can include the object that this was called on but sometimes that's
// just unnecessary context.
if (name.startsWith('Window.')) {
name = name.slice(7);
} else if (name.startsWith('<anonymous>.')) {
name = name.slice(7);
if (name === '') {
// If we didn't have an explicit name, try finding one from the stack.
name = findCalledFunctionNameFromStackTrace(request, fullStack);
// The name can include the object that this was called on but sometimes that's
// just unnecessary context.
if (name.startsWith('Window.')) {
name = name.slice(7);
} else if (name.startsWith('<anonymous>.')) {
name = name.slice(7);
}
}
}
const owner = ioNode.owner;

File diff suppressed because it is too large Load Diff