diff --git a/packages/react-server/src/ReactFizzComponentStack.js b/packages/react-server/src/ReactFizzComponentStack.js index 8b6147292d18..467cf48646c9 100644 --- a/packages/react-server/src/ReactFizzComponentStack.js +++ b/packages/react-server/src/ReactFizzComponentStack.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactComponentInfo} from 'shared/ReactTypes'; +import type {ReactComponentInfo, ReactAsyncInfo} from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; import { @@ -37,7 +37,8 @@ export type ComponentStackNode = { | string | Function | LazyComponent - | ReactComponentInfo, + | ReactComponentInfo + | ReactAsyncInfo, owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only stack?: null | string | Error, // DEV only }; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 8a4ba1e045fc..2f58c14e5df2 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -21,6 +21,7 @@ import type { ReactFormState, ReactComponentInfo, ReactDebugInfo, + ReactAsyncInfo, ViewTransitionProps, ActivityProps, SuspenseProps, @@ -181,6 +182,7 @@ import { enableAsyncIterableChildren, enableViewTransition, enableFizzBlockingRender, + enableAsyncDebugInfo, } from 'shared/ReactFeatureFlags'; import assign from 'shared/assign'; @@ -985,6 +987,45 @@ function getStackFromNode(stackNode: ComponentStackNode): string { return getStackByComponentStackNode(stackNode); } +function pushHaltedAwaitOnComponentStack( + task: Task, + debugInfo: void | null | ReactDebugInfo, +): void { + if (!__DEV__) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.', + ); + } + if (debugInfo != null) { + for (let i = debugInfo.length - 1; i >= 0; i--) { + const info = debugInfo[i]; + if (typeof info.name === 'string') { + // This is a Server Component. Any awaits in previous Server Components already resolved. + break; + } + if (typeof info.time === 'number') { + // This had an end time. Any awaits before this must have already resolved. + break; + } + if (info.awaited != null) { + const asyncInfo: ReactAsyncInfo = (info: any); + const bestStack = + asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo; + if (bestStack.debugStack !== undefined) { + task.componentStack = { + parent: task.componentStack, + type: asyncInfo, + owner: bestStack.owner, + stack: bestStack.debugStack, + }; + task.debugTask = (bestStack.debugTask: any); + } + } + } + } +} + function pushServerComponentStack( task: Task, debugInfo: void | null | ReactDebugInfo, @@ -4612,6 +4653,20 @@ function abortTask(task: Task, request: Request, error: mixed): void { } const errorInfo = getThrownInfo(task.componentStack); + if (__DEV__ && enableAsyncDebugInfo) { + // If the task is not rendering, then this is an async abort. Conceptually it's as if + // the abort happened inside the async gap. The abort reason's stack frame won't have that + // on the stack so instead we use the owner stack and debug task of any halted async debug info. + const node: any = task.node; + if (node !== null && typeof node === 'object') { + // Push a fake component stack frame that represents the await. + pushHaltedAwaitOnComponentStack(task, node._debugInfo); + if (task.thenableState !== null) { + // TODO: If we were stalled inside use() of a Client Component then we should + // rerender to get the stack trace from the use() call. + } + } + } if (boundary === null) { if (request.status !== CLOSING && request.status !== CLOSED) { @@ -4631,7 +4686,12 @@ function abortTask(task: Task, request: Request, error: mixed): void { if (trackedPostpones !== null && segment !== null) { // We are prerendering. We don't want to fatal when the shell postpones // we just need to mark it as postponed. - logPostpone(request, postponeInstance.message, errorInfo, null); + logPostpone( + request, + postponeInstance.message, + errorInfo, + task.debugTask, + ); trackPostpone(request, trackedPostpones, task, segment); finishedTask(request, null, task.row, segment); } else { @@ -4639,8 +4699,8 @@ function abortTask(task: Task, request: Request, error: mixed): void { 'The render was aborted with postpone when the shell is incomplete. Reason: ' + postponeInstance.message, ); - logRecoverableError(request, fatal, errorInfo, null); - fatalError(request, fatal, errorInfo, null); + logRecoverableError(request, fatal, errorInfo, task.debugTask); + fatalError(request, fatal, errorInfo, task.debugTask); } } else if ( enableHalt && @@ -4650,12 +4710,12 @@ function abortTask(task: Task, request: Request, error: mixed): void { const trackedPostpones = request.trackedPostpones; // We are aborting a prerender and must treat the shell as halted // We log the error but we still resolve the prerender - logRecoverableError(request, error, errorInfo, null); + logRecoverableError(request, error, errorInfo, task.debugTask); trackPostpone(request, trackedPostpones, task, segment); finishedTask(request, null, task.row, segment); } else { - logRecoverableError(request, error, errorInfo, null); - fatalError(request, error, errorInfo, null); + logRecoverableError(request, error, errorInfo, task.debugTask); + fatalError(request, error, errorInfo, task.debugTask); } return; } else { @@ -4672,7 +4732,12 @@ function abortTask(task: Task, request: Request, error: mixed): void { error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, errorInfo, null); + logPostpone( + request, + postponeInstance.message, + errorInfo, + task.debugTask, + ); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { @@ -4710,11 +4775,16 @@ function abortTask(task: Task, request: Request, error: mixed): void { error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, errorInfo, null); + logPostpone( + request, + postponeInstance.message, + errorInfo, + task.debugTask, + ); } else { // We are aborting a prerender and must halt this boundary. // We treat this like other postpones during prerendering - logRecoverableError(request, error, errorInfo, null); + logRecoverableError(request, error, errorInfo, task.debugTask); } trackPostpone(request, trackedPostpones, task, segment); // If this boundary was still pending then we haven't already cancelled its fallbacks. @@ -4737,7 +4807,12 @@ function abortTask(task: Task, request: Request, error: mixed): void { error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, errorInfo, null); + logPostpone( + request, + postponeInstance.message, + errorInfo, + task.debugTask, + ); if (request.trackedPostpones !== null && segment !== null) { trackPostpone(request, request.trackedPostpones, task, segment); finishedTask(request, task.blockedBoundary, task.row, segment); @@ -4753,7 +4828,12 @@ function abortTask(task: Task, request: Request, error: mixed): void { // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error, errorInfo, null); + errorDigest = logRecoverableError( + request, + error, + errorInfo, + task.debugTask, + ); } boundary.status = CLIENT_RENDERED; encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 832eab2abee1..dec0682dad13 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2149,7 +2149,11 @@ function visitAsyncNode( owner: node.owner, stack: filterStackTrace(request, node.stack), }); - markOperationEndTime(request, task, endTime); + // Mark the end time of the await. If we're aborting then we don't emit this + // to signal that this never resolved inside this render. + if (request.status !== ABORTING) { + markOperationEndTime(request, task, endTime); + } } } } @@ -2210,7 +2214,12 @@ function emitAsyncSequence( } } emitDebugChunk(request, task.id, debugInfo); - markOperationEndTime(request, task, awaitedNode.end); + // Mark the end time of the await. If we're aborting then we don't emit this + // to signal that this never resolved inside this render. + if (request.status !== ABORTING) { + // If we're currently aborting, then this never resolved into user space. + markOperationEndTime(request, task, awaitedNode.end); + } } } @@ -3910,6 +3919,13 @@ function serializeIONode( // The environment name may have changed from when the I/O was actually started. const env = (0, request.environmentName)(); + const endTime = + ioNode.tag === UNRESOLVED_PROMISE_NODE + ? // Mark the end time as now. It's arbitrary since it's not resolved but this + // marks when we stopped trying. + performance.now() + : ioNode.end; + request.pendingChunks++; const id = request.nextChunkId++; emitIOInfoChunk( @@ -3917,7 +3933,7 @@ function serializeIONode( id, name, ioNode.start, - ioNode.end, + endTime, value, env, owner, @@ -4741,7 +4757,6 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void { env: env, }; emitDebugChunk(request, task.id, asyncInfo); - markOperationEndTime(request, task, performance.now()); } else { emitAsyncSequence(request, task, sequence, debugInfo, null, null); }