Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reverse engineer original stack frames when virtual frames are re-serialized #30416

Merged
merged 1 commit into from
Jul 22, 2024
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
107 changes: 106 additions & 1 deletion packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,14 @@ describe('ReactFlight', () => {
this.props.expectedMessage,
);
expect(this.state.error.digest).toBe('a dev digest');
expect(this.state.error.environmentName).toBe('Server');
expect(this.state.error.environmentName).toBe(
this.props.expectedEnviromentName || 'Server',
);
if (this.props.expectedErrorStack !== undefined) {
expect(this.state.error.stack).toContain(
this.props.expectedErrorStack,
);
}
} else {
expect(this.state.error.message).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production' +
Expand Down Expand Up @@ -2603,6 +2610,104 @@ describe('ReactFlight', () => {
);
});

it('preserves error stacks passed through server-to-server with source maps', async () => {
async function ServerComponent({transport}) {
// This is a Server Component that receives other Server Components from a third party.
const thirdParty = ReactServer.use(
ReactNoopFlightClient.read(transport, {
findSourceMapURL(url) {
// By giving a source map url we're saying that we can't use the original
// file as the sourceURL, which gives stack traces a rsc://React/ prefix.
return 'source-map://' + url;
},
}),
);
// This will throw a third-party error inside the first-party server component.
await thirdParty.model;
return 'Should never render';
}

async function bar() {
throw new Error('third-party-error');
}

async function foo() {
await bar();
}

const rejectedPromise = foo();

const thirdPartyTransport = ReactNoopFlightServer.render(
{model: rejectedPromise},
{
environmentName: 'third-party',
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
return `digest("${x.message}")`;
},
},
);

let originalError;
try {
await rejectedPromise;
} catch (x) {
originalError = x;
}
expect(originalError.message).toBe('third-party-error');

const transport = ReactNoopFlightServer.render(
<ServerComponent transport={thirdPartyTransport} />,
{
onError(x) {
if (__DEV__) {
return 'a dev digest';
}
return x.digest; // passthrough
},
},
);

await 0;
await 0;
await 0;

const expectedErrorStack = originalError.stack
// Test only the first rows since there's a lot of noise after that is eliminated.
.split('\n')
.slice(0, 4)
.join('\n')
.replaceAll(
' (/',
gate(flags => flags.enableOwnerStacks) ? ' (file:///' : ' (/',
); // The eval will end up normalizing these

let sawReactPrefix = false;
await act(async () => {
ReactNoop.render(
<ErrorBoundary
expectedMessage="third-party-error"
expectedEnviromentName="third-party"
expectedErrorStack={expectedErrorStack}>
{ReactNoopFlightClient.read(transport, {
findSourceMapURL(url) {
if (url.startsWith('rsc://React/')) {
// We don't expect to see any React prefixed URLs here.
sawReactPrefix = true;
}
// My not giving a source map, we should leave it intact.
return null;
},
})}
</ErrorBoundary>,
);
});

expect(sawReactPrefix).toBe(false);
});

it('can change the environment name inside a component', async () => {
let env = 'A';
function Component(props) {
Expand Down
16 changes: 15 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,21 @@ function filterStackTrace(error: Error, skipFrames: number): ReactStackTrace {
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
// the DevTools or framework's ignore lists to filter them out.
return parseStackTrace(error, skipFrames).filter(isNotExternal);
const stack = parseStackTrace(error, skipFrames).filter(isNotExternal);
for (let i = 0; i < stack.length; i++) {
const callsite = stack[i];
const url = callsite[1];
if (url.startsWith('rsc://React/')) {
// This callsite is a virtual fake callsite that came from another Flight client.
// We need to reverse it back into the original location by stripping its prefix
// and suffix.
const suffixIdx = url.lastIndexOf('?');
if (suffixIdx > -1) {
callsite[1] = url.slice(12, suffixIdx);
}
}
}
return stack;
}

initAsyncDebugInfo();
Expand Down
Loading