@samitouri / QOS-React / commits / e5d22459ff

[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`.

Sebastian Markbåge committed Jul 25, 2024 at 11:14 UTC e5d22459ffd094ffd71c3e8857b62b3c6754c58d
3 files changed +90 -29
packages/react-client/src/ReactFlightClient.js
+57 -14
@@ -232,7 +232,10 @@ Chunk.prototype.then = function <T>(
232 }
233 };
234
235 -export type FindSourceMapURLCallback = (fileName: string) => null | string;
235 +export type FindSourceMapURLCallback = (
236 + fileName: string,
237 + environmentName: string,
238 +) => null | string;
239
240 export type Response = {
241 _bundlerConfig: SSRModuleMap,
@@ -689,7 +692,15 @@ function createElement(
692 writable: true,
693 value: null,
694 });
695 + let env = '';
696 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 + }
704 let normalizedStackTrace: null | Error = null;
705 if (stack !== null) {
706 // We create a fake stack and then create an Error object inside of it.
@@ -698,7 +709,11 @@ function createElement(
709 // source mapping information.
710 // This can unfortunately happen within a user space callstack which will
711 // remain on the stack.
701 - normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack);
712 + normalizedStackTrace = createFakeJSXCallStackInDEV(
713 + response,
714 + stack,
715 + env,
716 + );
717 }
718 Object.defineProperty(element, '_debugStack', {
719 configurable: false,
@@ -713,7 +728,12 @@ function createElement(
728 console,
729 getTaskName(type),
730 );
716 - const callStack = buildFakeCallStack(response, stack, createTaskFn);
731 + const callStack = buildFakeCallStack(
732 + response,
733 + stack,
734 + env,
735 + createTaskFn,
736 + );
737 // This owner should ideally have already been initialized to avoid getting
738 // user stack frames on the stack.
739 const ownerTask =
@@ -1836,6 +1856,7 @@ function resolveErrorDev(
1856 const callStack = buildFakeCallStack(
1857 response,
1858 stack,
1859 + env,
1860 // $FlowFixMe[incompatible-use]
1861 Error.bind(
1862 null,
@@ -1892,6 +1913,7 @@ function resolvePostponeDev(
1913 id: number,
1914 reason: string,
1915 stack: ReactStackTrace,
1916 + env: string,
1917 ): void {
1918 if (!__DEV__) {
1919 // 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(
1939 const callStack = buildFakeCallStack(
1940 response,
1941 stack,
1942 + env,
1943 // $FlowFixMe[incompatible-use]
1944 Error.bind(null, reason || ''),
1945 );
@@ -1961,6 +1984,7 @@ function createFakeFunction<T>(
1984 sourceMap: null | string,
1985 line: number,
1986 col: number,
1987 + environmentName: string,
1988 ): FakeFunction<T> {
1989 // This creates a fake copy of a Server Module. It represents a module that has already
1990 // 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>(
2037 // 1) A printed stack trace string needs a unique URL to be able to source map it.
2038 // 2) If source maps are disabled or fails, you should at least be able to tell
2039 // 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++;
2047 code += '\n//# sourceMappingURL=' + sourceMap;
2048 } else if (filename) {
2049 code += '\n//# sourceURL=' + filename;
@@ -2037,19 +2067,28 @@ function createFakeFunction<T>(
2067 function buildFakeCallStack<T>(
2068 response: Response,
2069 stack: ReactStackTrace,
2070 + environmentName: string,
2071 innerCall: () => T,
2072 ): () => T {
2073 let callStack = innerCall;
2074 for (let i = 0; i < stack.length; i++) {
2075 const frame = stack[i];
2045 - const frameKey = frame.join('-');
2076 + const frameKey = frame.join('-') + '-' + environmentName;
2077 let fn = fakeFunctionCache.get(frameKey);
2078 if (fn === undefined) {
2079 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)
2083 : 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 + );
2092 // TODO: This cache should technically live on the response since the _debugFindSourceMapURL
2093 // function is an input and can vary by response.
2094 fakeFunctionCache.set(frameKey, fn);
@@ -2079,7 +2118,7 @@ function initializeFakeTask(
2118 }
2119
2120 const stack = debugInfo.stack;
2082 -
2121 + const env = componentInfo.env == null ? '' : componentInfo.env;
2122 const ownerTask =
2123 componentInfo.owner == null
2124 ? null
@@ -2089,7 +2128,7 @@ function initializeFakeTask(
2128 console,
2129 getServerComponentTaskName(componentInfo),
2130 );
2092 - const callStack = buildFakeCallStack(response, stack, createTaskFn);
2131 + const callStack = buildFakeCallStack(response, stack, env, createTaskFn);
2132
2133 let componentTask;
2134 if (ownerTask === null) {
@@ -2111,10 +2150,12 @@ const createFakeJSXCallStack = {
2150 'react-stack-bottom-frame': function (
2151 response: Response,
2152 stack: ReactStackTrace,
2153 + environmentName: string,
2154 ): Error {
2155 const callStackForError = buildFakeCallStack(
2156 response,
2157 stack,
2158 + environmentName,
2159 fakeJSXCallSite,
2160 );
2161 return callStackForError();
@@ -2124,6 +2165,7 @@ const createFakeJSXCallStack = {
2165 const createFakeJSXCallStackInDEV: (
2166 response: Response,
2167 stack: ReactStackTrace,
2168 + environmentName: string,
2169 ) => Error = __DEV__
2170 ? // We use this technique to trick minifiers to preserve the function name.
2171 (createFakeJSXCallStack['react-stack-bottom-frame'].bind(
@@ -2147,12 +2189,11 @@ function initializeFakeStack(
2189 return;
2190 }
2191 if (debugInfo.stack != null) {
2192 + const stack = debugInfo.stack;
2193 + const env = debugInfo.env == null ? '' : debugInfo.env;
2194 // $FlowFixMe[cannot-write]
2195 // $FlowFixMe[prop-missing]
2152 - debugInfo.debugStack = createFakeJSXCallStackInDEV(
2153 - response,
2154 - debugInfo.stack,
2155 - );
2196 + debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
2197 }
2198 if (debugInfo.owner != null) {
2199 // Initialize any owners not yet initialized.
@@ -2221,6 +2262,7 @@ function resolveConsoleEntry(
2262 const callStack = buildFakeCallStack(
2263 response,
2264 stackTrace,
2265 + env,
2266 printToConsole.bind(null, methodName, args, env),
2267 );
2268 if (owner != null) {
@@ -2460,6 +2502,7 @@ function processFullStringRow(
2502 id,
2503 postponeInfo.reason,
2504 postponeInfo.stack,
2505 + postponeInfo.env,
2506 );
2507 } else {
2508 resolvePostponeProd(response, id);
packages/react-client/src/__tests__/ReactFlight-test.js
+26 -11
@@ -1241,10 +1241,10 @@ describe('ReactFlight', () => {
1241 const ClientErrorBoundary = clientReference(MyErrorBoundary);
1242
1243 function App() {
1244 - return (
1245 - <ClientErrorBoundary>
1246 - <ServerComponent />
1247 - </ClientErrorBoundary>
1244 + return ReactServer.createElement(
1245 + ClientErrorBoundary,
1246 + null,
1247 + ReactServer.createElement(ServerComponent),
1248 );
1249 }
1250
@@ -1301,13 +1301,16 @@ describe('ReactFlight', () => {
1301 ],
1302 findSourceMapURLCalls: gate(flags => flags.enableOwnerStacks)
1303 ? [
1304 - [__filename],
1305 - [__filename],
1304 + [__filename, 'Server'],
1305 + [__filename, 'Server'],
1306 // 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'],
1314 ]
1315 : [],
1316 });
@@ -2836,6 +2839,7 @@ describe('ReactFlight', () => {
2839 ); // The eval will end up normalizing these
2840
2841 let sawReactPrefix = false;
2842 + const environments = [];
2843 await act(async () => {
2844 ReactNoop.render(
2845 <ErrorBoundary
@@ -2843,11 +2847,12 @@ describe('ReactFlight', () => {
2847 expectedEnviromentName="third-party"
2848 expectedErrorStack={expectedErrorStack}>
2849 {ReactNoopFlightClient.read(transport, {
2846 - findSourceMapURL(url) {
2850 + findSourceMapURL(url, environmentName) {
2851 if (url.startsWith('rsc://React/')) {
2852 // We don't expect to see any React prefixed URLs here.
2853 sawReactPrefix = true;
2854 }
2855 + environments.push(environmentName);
2856 // My not giving a source map, we should leave it intact.
2857 return null;
2858 },
@@ -2857,6 +2862,16 @@ describe('ReactFlight', () => {
2862 });
2863
2864 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 + }
2875 });
2876
2877 it('can change the environment name inside a component', async () => {
packages/react-server/src/ReactFlightServer.js
+7 -4
@@ -163,10 +163,12 @@ function filterStackTrace(
163 if (url.startsWith('rsc://React/')) {
164 // This callsite is a virtual fake callsite that came from another Flight client.
165 // 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);
169 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);
172 }
173 }
174 if (!filterStackFrame(url, functionName)) {
@@ -2887,6 +2889,7 @@ function emitPostponeChunk(
2889 if (__DEV__) {
2890 let reason = '';
2891 let stack: ReactStackTrace;
2892 + const env = request.environmentName();
2893 try {
2894 // eslint-disable-next-line react-internal/safe-string-coercion
2895 reason = String(postponeInstance.message);
@@ -2894,7 +2897,7 @@ function emitPostponeChunk(
2897 } catch (x) {
2898 stack = [];
2899 }
2897 - row = serializeRowHeader('P', id) + stringify({reason, stack}) + '\n';
2900 + row = serializeRowHeader('P', id) + stringify({reason, stack, env}) + '\n';
2901 } else {
2902 // No reason included in prod.
2903 row = serializeRowHeader('P', id) + '\n';