mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[Flight] Ignore async stack frames when determining if a Promise was created from user space (#33739)
We use the stack of a Promise as the start of the I/O instead of the actual I/O since that can symbolize the start of the operation even if the actual I/O is batched, deduped or pooled. It can also group multiple I/O operations into one. We want the deepest possible Promise since otherwise it would just be the Component's Promise. However, we don't really need deeper than the boundary between first party and third party. We can't just take the outer most that has third party things on the stack though because third party can have callbacks into first party and then we want the inner one. So we take the inner most Promise that depends on I/O that has a first party stack on it. The realization is that for the purposes of determining whether we have a first party stack we need to ignore async stack frames. They can appear on the stack when we resume third party code inside a resumption frame of a first party stack. <img width="832" alt="Screenshot 2025-07-08 at 6 34 25 PM" src="https://github.com/user-attachments/assets/1636f980-be4c-4340-ad49-8d2b31953436" /> --------- Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
This commit is contained in:
parent
49ded1d12a
commit
150f022444
|
|
@ -53,6 +53,15 @@ const React = require('react');
|
||||||
const activeDebugChannels =
|
const activeDebugChannels =
|
||||||
process.env.NODE_ENV === 'development' ? new Map() : null;
|
process.env.NODE_ENV === 'development' ? new Map() : null;
|
||||||
|
|
||||||
|
function filterStackFrame(sourceURL, functionName) {
|
||||||
|
return (
|
||||||
|
sourceURL !== '' &&
|
||||||
|
!sourceURL.startsWith('node:') &&
|
||||||
|
!sourceURL.includes('node_modules') &&
|
||||||
|
!sourceURL.endsWith('library.js')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getDebugChannel(req) {
|
function getDebugChannel(req) {
|
||||||
if (process.env.NODE_ENV !== 'development') {
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -123,6 +132,7 @@ async function renderApp(
|
||||||
const payload = {root, returnValue, formState};
|
const payload = {root, returnValue, formState};
|
||||||
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
const {pipe} = renderToPipeableStream(payload, moduleMap, {
|
||||||
debugChannel: await promiseForDebugChannel,
|
debugChannel: await promiseForDebugChannel,
|
||||||
|
filterStackFrame,
|
||||||
});
|
});
|
||||||
pipe(res);
|
pipe(res);
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +188,9 @@ async function prerenderApp(res, returnValue, formState, noCache) {
|
||||||
);
|
);
|
||||||
// For client-invoked server actions we refresh the tree and return a return value.
|
// For client-invoked server actions we refresh the tree and return a return value.
|
||||||
const payload = {root, returnValue, formState};
|
const payload = {root, returnValue, formState};
|
||||||
const {prelude} = await prerenderToNodeStream(payload, moduleMap);
|
const {prelude} = await prerenderToNodeStream(payload, moduleMap, {
|
||||||
|
filterStackFrame,
|
||||||
|
});
|
||||||
prelude.pipe(res);
|
prelude.pipe(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {GenerateImage} from './GenerateImage.js';
|
||||||
import {like, greet, increment} from './actions.js';
|
import {like, greet, increment} from './actions.js';
|
||||||
|
|
||||||
import {getServerState} from './ServerState.js';
|
import {getServerState} from './ServerState.js';
|
||||||
|
import {sdkMethod} from './library.js';
|
||||||
|
|
||||||
const promisedText = new Promise(resolve =>
|
const promisedText = new Promise(resolve =>
|
||||||
setTimeout(() => resolve('deferred text'), 50)
|
setTimeout(() => resolve('deferred text'), 50)
|
||||||
|
|
@ -180,6 +181,7 @@ let veryDeepObject = [
|
||||||
export default async function App({prerender, noCache}) {
|
export default async function App({prerender, noCache}) {
|
||||||
const res = await fetch('http://localhost:3001/todos');
|
const res = await fetch('http://localhost:3001/todos');
|
||||||
const todos = await res.json();
|
const todos = await res.json();
|
||||||
|
await sdkMethod('http://localhost:3001/todos');
|
||||||
|
|
||||||
console.log('Expand me:', veryDeepObject);
|
console.log('Expand me:', veryDeepObject);
|
||||||
|
|
||||||
|
|
|
||||||
9
fixtures/flight/src/library.js
Normal file
9
fixtures/flight/src/library.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export async function sdkMethod(input, init) {
|
||||||
|
return fetch(input, init).then(async response => {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
10
packages/react-server/src/ReactFlightServer.js
vendored
10
packages/react-server/src/ReactFlightServer.js
vendored
|
|
@ -260,7 +260,15 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
|
||||||
const url = devirtualizeURL(callsite[1]);
|
const url = devirtualizeURL(callsite[1]);
|
||||||
const lineNumber = callsite[2];
|
const lineNumber = callsite[2];
|
||||||
const columnNumber = callsite[3];
|
const columnNumber = callsite[3];
|
||||||
if (filterStackFrame(url, functionName, lineNumber, columnNumber)) {
|
// Ignore async stack frames because they're not "real". We'd expect to have at least
|
||||||
|
// one non-async frame if we're actually executing inside a first party function.
|
||||||
|
// Otherwise we might just be in the resume of a third party function that resumed
|
||||||
|
// inside a first party stack.
|
||||||
|
const isAsync = callsite[6];
|
||||||
|
if (
|
||||||
|
!isAsync &&
|
||||||
|
filterStackFrame(url, functionName, lineNumber, columnNumber)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,9 @@ function collectStackTracePrivate(
|
||||||
// Skip everything after the bottom frame since it'll be internals.
|
// Skip everything after the bottom frame since it'll be internals.
|
||||||
break;
|
break;
|
||||||
} else if (callSite.isNative()) {
|
} else if (callSite.isNative()) {
|
||||||
result.push([name, '', 0, 0, 0, 0]);
|
// $FlowFixMe[prop-missing]
|
||||||
|
const isAsync = callSite.isAsync();
|
||||||
|
result.push([name, '', 0, 0, 0, 0, isAsync]);
|
||||||
} else {
|
} else {
|
||||||
// We encode complex function calls as if they're part of the function
|
// 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
|
// name since we cannot simulate the complex ones and they look the same
|
||||||
|
|
@ -98,7 +100,17 @@ function collectStackTracePrivate(
|
||||||
typeof callSite.getEnclosingColumnNumber === 'function'
|
typeof callSite.getEnclosingColumnNumber === 'function'
|
||||||
? (callSite: any).getEnclosingColumnNumber() || 0
|
? (callSite: any).getEnclosingColumnNumber() || 0
|
||||||
: 0;
|
: 0;
|
||||||
result.push([name, filename, line, col, enclosingLine, enclosingCol]);
|
// $FlowFixMe[prop-missing]
|
||||||
|
const isAsync = callSite.isAsync();
|
||||||
|
result.push([
|
||||||
|
name,
|
||||||
|
filename,
|
||||||
|
line,
|
||||||
|
col,
|
||||||
|
enclosingLine,
|
||||||
|
enclosingCol,
|
||||||
|
isAsync,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collectedStackTrace = result;
|
collectedStackTrace = result;
|
||||||
|
|
@ -221,8 +233,12 @@ export function parseStackTrace(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let name = parsed[1] || '';
|
let name = parsed[1] || '';
|
||||||
|
let isAsync = parsed[8] === 'async ';
|
||||||
if (name === '<anonymous>') {
|
if (name === '<anonymous>') {
|
||||||
name = '';
|
name = '';
|
||||||
|
} else if (name.startsWith('async ')) {
|
||||||
|
name = name.slice(5);
|
||||||
|
isAsync = true;
|
||||||
}
|
}
|
||||||
let filename = parsed[2] || parsed[5] || '';
|
let filename = parsed[2] || parsed[5] || '';
|
||||||
if (filename === '<anonymous>') {
|
if (filename === '<anonymous>') {
|
||||||
|
|
@ -230,7 +246,7 @@ export function parseStackTrace(
|
||||||
}
|
}
|
||||||
const line = +(parsed[3] || parsed[6]);
|
const line = +(parsed[3] || parsed[6]);
|
||||||
const col = +(parsed[4] || parsed[7]);
|
const col = +(parsed[4] || parsed[7]);
|
||||||
parsedFrames.push([name, filename, line, col, 0, 0]);
|
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
|
||||||
}
|
}
|
||||||
stackTraceCache.set(error, parsedFrames);
|
stackTraceCache.set(error, parsedFrames);
|
||||||
return parsedFrames;
|
return parsedFrames;
|
||||||
|
|
|
||||||
|
|
@ -2515,4 +2515,237 @@ describe('ReactFlightAsyncDebugInfo', () => {
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can track IO in third-party code', async () => {
|
||||||
|
async function thirdParty(endpoint) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve('third-party ' + endpoint);
|
||||||
|
}, 10);
|
||||||
|
}).then(async value => {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function Component() {
|
||||||
|
const value = await thirdParty('hi');
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = ReactServerDOMServer.renderToPipeableStream(
|
||||||
|
<Component />,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
filterStackFrame(filename, functionName) {
|
||||||
|
if (functionName === 'thirdParty') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return filterStackFrame(filename, functionName);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const readable = new Stream.PassThrough(streamOptions);
|
||||||
|
|
||||||
|
const result = ReactServerDOMClient.createFromNodeStream(readable, {
|
||||||
|
moduleMap: {},
|
||||||
|
moduleLoading: {},
|
||||||
|
});
|
||||||
|
stream.pipe(readable);
|
||||||
|
|
||||||
|
expect(await result).toBe('third-party hi');
|
||||||
|
|
||||||
|
await finishLoadingStream(readable);
|
||||||
|
if (
|
||||||
|
__DEV__ &&
|
||||||
|
gate(
|
||||||
|
flags =>
|
||||||
|
flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"env": "Server",
|
||||||
|
"key": null,
|
||||||
|
"name": "Component",
|
||||||
|
"props": {},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"Object.<anonymous>",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2540,
|
||||||
|
40,
|
||||||
|
2519,
|
||||||
|
42,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awaited": {
|
||||||
|
"end": 0,
|
||||||
|
"env": "Server",
|
||||||
|
"name": "",
|
||||||
|
"owner": {
|
||||||
|
"env": "Server",
|
||||||
|
"key": null,
|
||||||
|
"name": "Component",
|
||||||
|
"props": {},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"Object.<anonymous>",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2540,
|
||||||
|
40,
|
||||||
|
2519,
|
||||||
|
42,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2526,
|
||||||
|
15,
|
||||||
|
2525,
|
||||||
|
15,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Component",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2535,
|
||||||
|
19,
|
||||||
|
2534,
|
||||||
|
5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"start": 0,
|
||||||
|
"value": {
|
||||||
|
"value": undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"env": "Server",
|
||||||
|
"owner": {
|
||||||
|
"env": "Server",
|
||||||
|
"key": null,
|
||||||
|
"name": "Component",
|
||||||
|
"props": {},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"Object.<anonymous>",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2540,
|
||||||
|
40,
|
||||||
|
2519,
|
||||||
|
42,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2526,
|
||||||
|
15,
|
||||||
|
2525,
|
||||||
|
15,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Component",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2535,
|
||||||
|
19,
|
||||||
|
2534,
|
||||||
|
5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awaited": {
|
||||||
|
"end": 0,
|
||||||
|
"env": "Server",
|
||||||
|
"name": "thirdParty",
|
||||||
|
"owner": {
|
||||||
|
"env": "Server",
|
||||||
|
"key": null,
|
||||||
|
"name": "Component",
|
||||||
|
"props": {},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"Object.<anonymous>",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2540,
|
||||||
|
40,
|
||||||
|
2519,
|
||||||
|
42,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"Component",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2535,
|
||||||
|
25,
|
||||||
|
2534,
|
||||||
|
5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"start": 0,
|
||||||
|
"value": {
|
||||||
|
"value": "third-party hi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"env": "Server",
|
||||||
|
"owner": {
|
||||||
|
"env": "Server",
|
||||||
|
"key": null,
|
||||||
|
"name": "Component",
|
||||||
|
"props": {},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"Object.<anonymous>",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2540,
|
||||||
|
40,
|
||||||
|
2519,
|
||||||
|
42,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stack": [
|
||||||
|
[
|
||||||
|
"Component",
|
||||||
|
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||||
|
2535,
|
||||||
|
25,
|
||||||
|
2534,
|
||||||
|
5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"time": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ export type ReactCallSite = [
|
||||||
number, // column number
|
number, // column number
|
||||||
number, // enclosing line number
|
number, // enclosing line number
|
||||||
number, // enclosing column number
|
number, // enclosing column number
|
||||||
|
boolean, // async resume
|
||||||
];
|
];
|
||||||
|
|
||||||
export type ReactStackTrace = Array<ReactCallSite>;
|
export type ReactStackTrace = Array<ReactCallSite>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user