Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion fixtures/flight/server/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ const React = require('react');
const activeDebugChannels =
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) {
if (process.env.NODE_ENV !== 'development') {
return undefined;
Expand Down Expand Up @@ -123,6 +132,7 @@ async function renderApp(
const payload = {root, returnValue, formState};
const {pipe} = renderToPipeableStream(payload, moduleMap, {
debugChannel: await promiseForDebugChannel,
filterStackFrame,
});
pipe(res);
}
Expand Down Expand Up @@ -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.
const payload = {root, returnValue, formState};
const {prelude} = await prerenderToNodeStream(payload, moduleMap);
const {prelude} = await prerenderToNodeStream(payload, moduleMap, {
filterStackFrame,
});
prelude.pipe(res);
}

Expand Down
2 changes: 2 additions & 0 deletions fixtures/flight/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {GenerateImage} from './GenerateImage.js';
import {like, greet, increment} from './actions.js';

import {getServerState} from './ServerState.js';
import {sdkMethod} from './library.js';

const promisedText = new Promise(resolve =>
setTimeout(() => resolve('deferred text'), 50)
Expand Down Expand Up @@ -180,6 +181,7 @@ let veryDeepObject = [
export default async function App({prerender, noCache}) {
const res = await fetch('http://localhost:3001/todos');
const todos = await res.json();
await sdkMethod('http://localhost:3001/todos');

console.log('Expand me:', veryDeepObject);

Expand Down
9 changes: 9 additions & 0 deletions fixtures/flight/src/library.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function sdkMethod(input, init) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is a bit weird because it's using an async function without actually awaiting anywhere. You might think of optimizing it to not be an async function and just return the Promise.

Unfortunately, then it doesn't get any name at all and no stack for the I/O (just the await). Because now there's no Promise with a good stack around it.

Ideally we'd probably use the .then call as the best available stack for the I/O but not sure how that makes sense since it's not the cause spawning the I/O. Maybe it's really the Promise that it is awaiting that's the best possible Promise in that case. I.e. the first fetch. But I'm not going to address that now.

return fetch(input, init).then(async response => {
await new Promise(resolve => {
setTimeout(resolve, 10);
});

return response;
});
}
10 changes: 9 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,15 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean {
const url = devirtualizeURL(callsite[1]);
const lineNumber = callsite[2];
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;
}
}
Expand Down
22 changes: 19 additions & 3 deletions packages/react-server/src/ReactFlightStackConfigV8.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ function collectStackTrace(
// Skip everything after the bottom frame since it'll be internals.
break;
} 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 {
// 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
Expand Down Expand Up @@ -98,7 +100,17 @@ function collectStackTrace(
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber() || 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,
]);
}
}
// At the same time we generate a string stack trace just in case someone
Expand Down Expand Up @@ -193,16 +205,20 @@ export function parseStackTrace(
continue;
}
let name = parsed[1] || '';
let isAsync = parsed[8] === 'async ';
if (name === '<anonymous>') {
name = '';
} else if (name.startsWith('async ')) {
name = name.slice(5);
isAsync = true;
}
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]);
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
}
stackTraceCache.set(error, parsedFrames);
return parsedFrames;
Expand Down
233 changes: 233 additions & 0 deletions packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
]
`);
}
});
});
1 change: 1 addition & 0 deletions packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export type ReactCallSite = [
number, // column number
number, // enclosing line number
number, // enclosing column number
boolean, // async resume
];

export type ReactStackTrace = Array<ReactCallSite>;
Expand Down
Loading