Skip to content

Commit 026abea

Browse files
authored
[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.
1 parent d7215b4 commit 026abea

File tree

2 files changed

+415
-248
lines changed

2 files changed

+415
-248
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ function findCalledFunctionNameFromStackTrace(
252252
const url = devirtualizeURL(callsite[1]);
253253
const lineNumber = callsite[2];
254254
const columnNumber = callsite[3];
255-
if (filterStackFrame(url, functionName, lineNumber, columnNumber)) {
255+
if (
256+
filterStackFrame(url, functionName, lineNumber, columnNumber) &&
257+
// Don't consider anonymous code first party even if the filter wants to include them in the stack.
258+
url !== ''
259+
) {
256260
if (bestMatch === '') {
257261
// If we had no good stack frames for internal calls, just use the last
258262
// first party function name.
@@ -308,7 +312,10 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
308312
const isAsync = callsite[6];
309313
if (
310314
!isAsync &&
311-
filterStackFrame(url, functionName, lineNumber, columnNumber)
315+
filterStackFrame(url, functionName, lineNumber, columnNumber) &&
316+
// Ignore anonymous stack frames like internals. They are also not in first party
317+
// code even though it might be useful to include them in the final stack.
318+
url !== ''
312319
) {
313320
return true;
314321
}
@@ -367,7 +374,10 @@ export function isAwaitInUserspace(
367374
const url = devirtualizeURL(callsite[1]);
368375
const lineNumber = callsite[2];
369376
const columnNumber = callsite[3];
370-
return filterStackFrame(url, functionName, lineNumber, columnNumber);
377+
return (
378+
filterStackFrame(url, functionName, lineNumber, columnNumber) &&
379+
url !== ''
380+
);
371381
}
372382
return false;
373383
}
@@ -2347,6 +2357,7 @@ function visitAsyncNode(
23472357
}
23482358
const awaited = node.awaited;
23492359
let match: void | null | PromiseNode | IONode = previousIONode;
2360+
const promise = node.promise.deref();
23502361
if (awaited !== null) {
23512362
const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);
23522363
if (ioNode === undefined) {
@@ -2361,26 +2372,39 @@ function visitAsyncNode(
23612372
if (ioNode.tag === PROMISE_NODE) {
23622373
// If the ioNode was a Promise, then that means we found one in user space since otherwise
23632374
// we would've returned an IO node. We assume this has the best stack.
2375+
// Note: This might also be a Promise with a displayName but potentially a worse stack.
2376+
// We could potentially favor the outer Promise if it has a stack but not the inner.
23642377
match = ioNode;
23652378
} else if (
2366-
node.stack === null ||
2367-
!hasUnfilteredFrame(request, node.stack)
2379+
(node.stack !== null && hasUnfilteredFrame(request, node.stack)) ||
2380+
(promise !== undefined &&
2381+
// $FlowFixMe[prop-missing]
2382+
typeof promise.displayName === 'string' &&
2383+
(ioNode.stack === null ||
2384+
!hasUnfilteredFrame(request, ioNode.stack)))
23682385
) {
2386+
// If this Promise has a stack trace then we favor that over the I/O node since we're
2387+
// mainly dealing with Promises as the abstraction.
2388+
// If it has no stack but at least has a displayName and the io doesn't have a better
2389+
// stack anyway, then also use this Promise instead since at least it has a name.
2390+
match = node;
2391+
} else {
23692392
// If this Promise was created inside only third party code, then try to use
23702393
// the inner I/O node instead. This could happen if third party calls into first
23712394
// party to perform some I/O.
23722395
match = ioNode;
2373-
} else {
2374-
match = node;
23752396
}
23762397
} else if (request.status === ABORTING) {
23772398
if (node.start < request.abortTime && node.end > request.abortTime) {
23782399
// We aborted this render. If this Promise spanned the abort time it was probably the
23792400
// Promise that was aborted. This won't necessarily have I/O associated with it but
23802401
// it's a point of interest.
23812402
if (
2382-
node.stack !== null &&
2383-
hasUnfilteredFrame(request, node.stack)
2403+
(node.stack !== null &&
2404+
hasUnfilteredFrame(request, node.stack)) ||
2405+
(promise !== undefined &&
2406+
// $FlowFixMe[prop-missing]
2407+
typeof promise.displayName === 'string')
23842408
) {
23852409
match = node;
23862410
}
@@ -2389,7 +2413,6 @@ function visitAsyncNode(
23892413
}
23902414
// We need to forward after we visit awaited nodes because what ever I/O we requested that's
23912415
// the thing that generated this node and its virtual children.
2392-
const promise = node.promise.deref();
23932416
if (promise !== undefined) {
23942417
const debugInfo = promise._debugInfo;
23952418
if (debugInfo != null && !visited.has(debugInfo)) {
@@ -4497,17 +4520,33 @@ function serializeIONode(
44974520

44984521
let stack = null;
44994522
let name = '';
4523+
if (ioNode.promise !== null) {
4524+
// Pick an explicit name from the Promise itself if it exists.
4525+
// Note that we don't use the promiseRef passed in since that's sometimes the awaiting Promise
4526+
// which is the value observed but it's likely not the one with the name on it.
4527+
const promise = ioNode.promise.deref();
4528+
if (
4529+
promise !== undefined &&
4530+
// $FlowFixMe[prop-missing]
4531+
typeof promise.displayName === 'string'
4532+
) {
4533+
name = promise.displayName;
4534+
}
4535+
}
45004536
if (ioNode.stack !== null) {
45014537
// The stack can contain some leading internal frames for the construction of the promise that we skip.
45024538
const fullStack = stripLeadingPromiseCreationFrames(ioNode.stack);
45034539
stack = filterStackTrace(request, fullStack);
4504-
name = findCalledFunctionNameFromStackTrace(request, fullStack);
4505-
// The name can include the object that this was called on but sometimes that's
4506-
// just unnecessary context.
4507-
if (name.startsWith('Window.')) {
4508-
name = name.slice(7);
4509-
} else if (name.startsWith('<anonymous>.')) {
4510-
name = name.slice(7);
4540+
if (name === '') {
4541+
// If we didn't have an explicit name, try finding one from the stack.
4542+
name = findCalledFunctionNameFromStackTrace(request, fullStack);
4543+
// The name can include the object that this was called on but sometimes that's
4544+
// just unnecessary context.
4545+
if (name.startsWith('Window.')) {
4546+
name = name.slice(7);
4547+
} else if (name.startsWith('<anonymous>.')) {
4548+
name = name.slice(7);
4549+
}
45114550
}
45124551
}
45134552
const owner = ioNode.owner;

0 commit comments

Comments
 (0)