Skip to content

Commit e5d2245

Browse files
authored
[Flight] Include environment name both in the virtual URL and findSourceMapURL (#30452)
This way you can use the environment to know where to look for the source map in case you have multiple server environments. This becomes part of the public protocol since it's part of what you'll parse out of the `rsc://React/` prefixed URLs inside of `captureOwnerStack`.
1 parent 4b62400 commit e5d2245

File tree

3 files changed

+90
-29
lines changed

3 files changed

+90
-29
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,10 @@ Chunk.prototype.then = function <T>(
232232
}
233233
};
234234

235-
export type FindSourceMapURLCallback = (fileName: string) => null | string;
235+
export type FindSourceMapURLCallback = (
236+
fileName: string,
237+
environmentName: string,
238+
) => null | string;
236239

237240
export type Response = {
238241
_bundlerConfig: SSRModuleMap,
@@ -689,7 +692,15 @@ function createElement(
689692
writable: true,
690693
value: null,
691694
});
695+
let env = '';
692696
if (enableOwnerStacks) {
697+
if (owner !== null && owner.env != null) {
698+
// Interestingly we don't actually have the environment name of where
699+
// this JSX was created if it doesn't have an owner but if it does
700+
// it must be the same environment as the owner. We could send it separately
701+
// but it seems a bit unnecessary for this edge case.
702+
env = owner.env;
703+
}
693704
let normalizedStackTrace: null | Error = null;
694705
if (stack !== null) {
695706
// We create a fake stack and then create an Error object inside of it.
@@ -698,7 +709,11 @@ function createElement(
698709
// source mapping information.
699710
// This can unfortunately happen within a user space callstack which will
700711
// remain on the stack.
701-
normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack);
712+
normalizedStackTrace = createFakeJSXCallStackInDEV(
713+
response,
714+
stack,
715+
env,
716+
);
702717
}
703718
Object.defineProperty(element, '_debugStack', {
704719
configurable: false,
@@ -713,7 +728,12 @@ function createElement(
713728
console,
714729
getTaskName(type),
715730
);
716-
const callStack = buildFakeCallStack(response, stack, createTaskFn);
731+
const callStack = buildFakeCallStack(
732+
response,
733+
stack,
734+
env,
735+
createTaskFn,
736+
);
717737
// This owner should ideally have already been initialized to avoid getting
718738
// user stack frames on the stack.
719739
const ownerTask =
@@ -1836,6 +1856,7 @@ function resolveErrorDev(
18361856
const callStack = buildFakeCallStack(
18371857
response,
18381858
stack,
1859+
env,
18391860
// $FlowFixMe[incompatible-use]
18401861
Error.bind(
18411862
null,
@@ -1892,6 +1913,7 @@ function resolvePostponeDev(
18921913
id: number,
18931914
reason: string,
18941915
stack: ReactStackTrace,
1916+
env: string,
18951917
): void {
18961918
if (!__DEV__) {
18971919
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -1917,6 +1939,7 @@ function resolvePostponeDev(
19171939
const callStack = buildFakeCallStack(
19181940
response,
19191941
stack,
1942+
env,
19201943
// $FlowFixMe[incompatible-use]
19211944
Error.bind(null, reason || ''),
19221945
);
@@ -1961,6 +1984,7 @@ function createFakeFunction<T>(
19611984
sourceMap: null | string,
19621985
line: number,
19631986
col: number,
1987+
environmentName: string,
19641988
): FakeFunction<T> {
19651989
// This creates a fake copy of a Server Module. It represents a module that has already
19661990
// executed on the server but we re-execute a blank copy for its stack frames on the client.
@@ -2013,7 +2037,13 @@ function createFakeFunction<T>(
20132037
// 1) A printed stack trace string needs a unique URL to be able to source map it.
20142038
// 2) If source maps are disabled or fails, you should at least be able to tell
20152039
// which file it was.
2016-
code += '\n//# sourceURL=rsc://React/' + filename + '?' + fakeFunctionIdx++;
2040+
code +=
2041+
'\n//# sourceURL=rsc://React/' +
2042+
encodeURIComponent(environmentName) +
2043+
'/' +
2044+
filename +
2045+
'?' +
2046+
fakeFunctionIdx++;
20172047
code += '\n//# sourceMappingURL=' + sourceMap;
20182048
} else if (filename) {
20192049
code += '\n//# sourceURL=' + filename;
@@ -2037,19 +2067,28 @@ function createFakeFunction<T>(
20372067
function buildFakeCallStack<T>(
20382068
response: Response,
20392069
stack: ReactStackTrace,
2070+
environmentName: string,
20402071
innerCall: () => T,
20412072
): () => T {
20422073
let callStack = innerCall;
20432074
for (let i = 0; i < stack.length; i++) {
20442075
const frame = stack[i];
2045-
const frameKey = frame.join('-');
2076+
const frameKey = frame.join('-') + '-' + environmentName;
20462077
let fn = fakeFunctionCache.get(frameKey);
20472078
if (fn === undefined) {
20482079
const [name, filename, line, col] = frame;
2049-
const sourceMap = response._debugFindSourceMapURL
2050-
? response._debugFindSourceMapURL(filename)
2080+
const findSourceMapURL = response._debugFindSourceMapURL;
2081+
const sourceMap = findSourceMapURL
2082+
? findSourceMapURL(filename, environmentName)
20512083
: null;
2052-
fn = createFakeFunction(name, filename, sourceMap, line, col);
2084+
fn = createFakeFunction(
2085+
name,
2086+
filename,
2087+
sourceMap,
2088+
line,
2089+
col,
2090+
environmentName,
2091+
);
20532092
// TODO: This cache should technically live on the response since the _debugFindSourceMapURL
20542093
// function is an input and can vary by response.
20552094
fakeFunctionCache.set(frameKey, fn);
@@ -2079,7 +2118,7 @@ function initializeFakeTask(
20792118
}
20802119

20812120
const stack = debugInfo.stack;
2082-
2121+
const env = componentInfo.env == null ? '' : componentInfo.env;
20832122
const ownerTask =
20842123
componentInfo.owner == null
20852124
? null
@@ -2089,7 +2128,7 @@ function initializeFakeTask(
20892128
console,
20902129
getServerComponentTaskName(componentInfo),
20912130
);
2092-
const callStack = buildFakeCallStack(response, stack, createTaskFn);
2131+
const callStack = buildFakeCallStack(response, stack, env, createTaskFn);
20932132

20942133
let componentTask;
20952134
if (ownerTask === null) {
@@ -2111,10 +2150,12 @@ const createFakeJSXCallStack = {
21112150
'react-stack-bottom-frame': function (
21122151
response: Response,
21132152
stack: ReactStackTrace,
2153+
environmentName: string,
21142154
): Error {
21152155
const callStackForError = buildFakeCallStack(
21162156
response,
21172157
stack,
2158+
environmentName,
21182159
fakeJSXCallSite,
21192160
);
21202161
return callStackForError();
@@ -2124,6 +2165,7 @@ const createFakeJSXCallStack = {
21242165
const createFakeJSXCallStackInDEV: (
21252166
response: Response,
21262167
stack: ReactStackTrace,
2168+
environmentName: string,
21272169
) => Error = __DEV__
21282170
? // We use this technique to trick minifiers to preserve the function name.
21292171
(createFakeJSXCallStack['react-stack-bottom-frame'].bind(
@@ -2147,12 +2189,11 @@ function initializeFakeStack(
21472189
return;
21482190
}
21492191
if (debugInfo.stack != null) {
2192+
const stack = debugInfo.stack;
2193+
const env = debugInfo.env == null ? '' : debugInfo.env;
21502194
// $FlowFixMe[cannot-write]
21512195
// $FlowFixMe[prop-missing]
2152-
debugInfo.debugStack = createFakeJSXCallStackInDEV(
2153-
response,
2154-
debugInfo.stack,
2155-
);
2196+
debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
21562197
}
21572198
if (debugInfo.owner != null) {
21582199
// Initialize any owners not yet initialized.
@@ -2221,6 +2262,7 @@ function resolveConsoleEntry(
22212262
const callStack = buildFakeCallStack(
22222263
response,
22232264
stackTrace,
2265+
env,
22242266
printToConsole.bind(null, methodName, args, env),
22252267
);
22262268
if (owner != null) {
@@ -2460,6 +2502,7 @@ function processFullStringRow(
24602502
id,
24612503
postponeInfo.reason,
24622504
postponeInfo.stack,
2505+
postponeInfo.env,
24632506
);
24642507
} else {
24652508
resolvePostponeProd(response, id);

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,10 +1241,10 @@ describe('ReactFlight', () => {
12411241
const ClientErrorBoundary = clientReference(MyErrorBoundary);
12421242

12431243
function App() {
1244-
return (
1245-
<ClientErrorBoundary>
1246-
<ServerComponent />
1247-
</ClientErrorBoundary>
1244+
return ReactServer.createElement(
1245+
ClientErrorBoundary,
1246+
null,
1247+
ReactServer.createElement(ServerComponent),
12481248
);
12491249
}
12501250

@@ -1301,13 +1301,16 @@ describe('ReactFlight', () => {
13011301
],
13021302
findSourceMapURLCalls: gate(flags => flags.enableOwnerStacks)
13031303
? [
1304-
[__filename],
1305-
[__filename],
1304+
[__filename, 'Server'],
1305+
[__filename, 'Server'],
13061306
// TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
1307-
['inspected-page.html:29:11), <anonymous>'],
1308-
['file://~/(some)(really)(exotic-directory)/ReactFlight-test.js'],
1309-
['file:///testing.js'],
1310-
[__filename],
1307+
['inspected-page.html:29:11), <anonymous>', 'Server'],
1308+
[
1309+
'file://~/(some)(really)(exotic-directory)/ReactFlight-test.js',
1310+
'Server',
1311+
],
1312+
['file:///testing.js', 'Server'],
1313+
[__filename, 'Server'],
13111314
]
13121315
: [],
13131316
});
@@ -2836,18 +2839,20 @@ describe('ReactFlight', () => {
28362839
); // The eval will end up normalizing these
28372840

28382841
let sawReactPrefix = false;
2842+
const environments = [];
28392843
await act(async () => {
28402844
ReactNoop.render(
28412845
<ErrorBoundary
28422846
expectedMessage="third-party-error"
28432847
expectedEnviromentName="third-party"
28442848
expectedErrorStack={expectedErrorStack}>
28452849
{ReactNoopFlightClient.read(transport, {
2846-
findSourceMapURL(url) {
2850+
findSourceMapURL(url, environmentName) {
28472851
if (url.startsWith('rsc://React/')) {
28482852
// We don't expect to see any React prefixed URLs here.
28492853
sawReactPrefix = true;
28502854
}
2855+
environments.push(environmentName);
28512856
// My not giving a source map, we should leave it intact.
28522857
return null;
28532858
},
@@ -2857,6 +2862,16 @@ describe('ReactFlight', () => {
28572862
});
28582863

28592864
expect(sawReactPrefix).toBe(false);
2865+
if (__DEV__ && gate(flags => flags.enableOwnerStacks)) {
2866+
expect(environments.slice(0, 4)).toEqual([
2867+
'Server',
2868+
'third-party',
2869+
'third-party',
2870+
'third-party',
2871+
]);
2872+
} else {
2873+
expect(environments).toEqual([]);
2874+
}
28602875
});
28612876

28622877
it('can change the environment name inside a component', async () => {

packages/react-server/src/ReactFlightServer.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,12 @@ function filterStackTrace(
163163
if (url.startsWith('rsc://React/')) {
164164
// This callsite is a virtual fake callsite that came from another Flight client.
165165
// We need to reverse it back into the original location by stripping its prefix
166-
// and suffix.
166+
// and suffix. We don't need the environment name because it's available on the
167+
// parent object that will contain the stack.
168+
const envIdx = url.indexOf('/', 12);
167169
const suffixIdx = url.lastIndexOf('?');
168-
if (suffixIdx > -1) {
169-
url = callsite[1] = url.slice(12, suffixIdx);
170+
if (envIdx > -1 && suffixIdx > -1) {
171+
url = callsite[1] = url.slice(envIdx + 1, suffixIdx);
170172
}
171173
}
172174
if (!filterStackFrame(url, functionName)) {
@@ -2887,14 +2889,15 @@ function emitPostponeChunk(
28872889
if (__DEV__) {
28882890
let reason = '';
28892891
let stack: ReactStackTrace;
2892+
const env = request.environmentName();
28902893
try {
28912894
// eslint-disable-next-line react-internal/safe-string-coercion
28922895
reason = String(postponeInstance.message);
28932896
stack = filterStackTrace(request, postponeInstance, 0);
28942897
} catch (x) {
28952898
stack = [];
28962899
}
2897-
row = serializeRowHeader('P', id) + stringify({reason, stack}) + '\n';
2900+
row = serializeRowHeader('P', id) + stringify({reason, stack, env}) + '\n';
28982901
} else {
28992902
// No reason included in prod.
29002903
row = serializeRowHeader('P', id) + '\n';

0 commit comments

Comments
 (0)