From 349a99a7a347f280ce40e9297cac5a3bd796901e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 26 Jun 2024 19:27:26 +0200 Subject: [PATCH] Badge Environment Name on Thrown Errors from the Server (#29846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When we replay logs we badge them with e.g. `[Server]`. That way it's easy to identify that the source of the log actually happened on the Server (RSC). However, when we threw an error we didn't have any such thing. The error was rethrown on the client and then handled just like any other client error. This transfers the `environmentName` in DEV to our restored Error "sub-class" (conceptually) along with `digest`. That way you can read `error.environmentName` to print this in your own UI. I also updated our default for `onCaughtError` (and `onError` in Fizz) to use the `printToConsole` helper that the Flight Client uses to log it with the badge format. So by default you get the same experience as console.error for caught errors: Screenshot 2024-06-10 at 9 25 12 PM Screenshot 2024-06-10 at 9 39 30 PM Unfortunately I can't do the same thing for `onUncaughtError` nor `onRecoverableError` because they use `reportError` which doesn't have custom formatting (unless we also prevented default on window.onerror). However maybe that's ok because 1) you should always have an error boundary 2) it's not likely that an RSC error can actually recover because it's not going to be rendered again so shouldn't really happen outside some parent conditionally rendering maybe. The other problem with this approach is that the default is no longer trivial - so reimplementing the default in user space is trickier and ideally we shouldn't expose our default to be called. --- packages/internal-test-utils/consoleMock.js | 17 +++++---- .../shouldIgnoreConsoleError.js | 4 +++ ....js => ReactClientConsoleConfigBrowser.js} | 13 +++++-- ...in.js => ReactClientConsoleConfigPlain.js} | 13 +++++-- ...r.js => ReactClientConsoleConfigServer.js} | 13 +++++-- .../react-client/src/ReactFlightClient.js | 5 +++ .../src/__tests__/ReactFlight-test.js | 2 ++ ...ReactFlightClientConfig.dom-browser-esm.js | 2 +- ...lightClientConfig.dom-browser-turbopack.js | 2 +- .../ReactFlightClientConfig.dom-browser.js | 2 +- .../forks/ReactFlightClientConfig.dom-bun.js | 2 +- ...ctFlightClientConfig.dom-edge-turbopack.js | 2 +- ...eactFlightClientConfig.dom-edge-webpack.js | 2 +- .../ReactFlightClientConfig.dom-legacy.js | 2 +- .../ReactFlightClientConfig.dom-node-esm.js | 2 +- ...ClientConfig.dom-node-turbopack-bundled.js | 2 +- ...ctFlightClientConfig.dom-node-turbopack.js | 2 +- ...eactFlightClientConfig.dom-node-webpack.js | 2 +- .../forks/ReactFlightClientConfig.dom-node.js | 2 +- .../src/createReactNoop.js | 5 +++ .../src/ReactFiberErrorLogger.js | 36 +++++++++++++++---- .../src/forks/ReactFiberConfig.art.js | 1 + .../src/forks/ReactFiberConfig.custom.js | 1 + .../src/forks/ReactFiberConfig.dom.js | 1 + .../src/forks/ReactFiberConfig.fabric.js | 1 + .../src/forks/ReactFiberConfig.native.js | 1 + .../src/forks/ReactFiberConfig.test.js | 1 + packages/react-server/src/ReactFizzServer.js | 13 ++++++- .../react-server/src/ReactFlightServer.js | 9 ++++- .../src/forks/ReactFizzConfig.custom.js | 2 ++ .../src/forks/ReactFizzConfig.dom-edge.js | 2 ++ .../src/forks/ReactFizzConfig.dom-legacy.js | 2 ++ .../src/forks/ReactFizzConfig.dom-node.js | 2 ++ .../src/forks/ReactFizzConfig.dom.js | 2 ++ 34 files changed, 134 insertions(+), 36 deletions(-) rename packages/react-client/src/{ReactFlightClientConsoleConfigBrowser.js => ReactClientConsoleConfigBrowser.js} (82%) rename packages/react-client/src/{ReactFlightClientConsoleConfigPlain.js => ReactClientConsoleConfigPlain.js} (76%) rename packages/react-client/src/{ReactFlightClientConsoleConfigServer.js => ReactClientConsoleConfigServer.js} (83%) diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 45a59d2b4cb29..328cf3d90d0c4 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -418,13 +418,18 @@ export function createLogAssertion( let argIndex = 0; // console.* could have been called with a non-string e.g. `console.error(new Error())` // eslint-disable-next-line react-internal/safe-string-coercion - String(format).replace(/%s/g, () => argIndex++); + String(format).replace(/%s|%c/g, () => argIndex++); if (argIndex !== args.length) { - logsMismatchingFormat.push({ - format, - args, - expectedArgCount: argIndex, - }); + if (format.includes('%c%s')) { + // We intentionally use mismatching formatting when printing badging because we don't know + // the best default to use for different types because the default varies by platform. + } else { + logsMismatchingFormat.push({ + format, + args, + expectedArgCount: argIndex, + }); + } } // Check for extra component stacks diff --git a/packages/internal-test-utils/shouldIgnoreConsoleError.js b/packages/internal-test-utils/shouldIgnoreConsoleError.js index 383650d25a0b7..0b5798d241a1d 100644 --- a/packages/internal-test-utils/shouldIgnoreConsoleError.js +++ b/packages/internal-test-utils/shouldIgnoreConsoleError.js @@ -3,6 +3,10 @@ module.exports = function shouldIgnoreConsoleError(format, args) { if (__DEV__) { if (typeof format === 'string') { + if (format.startsWith('%c%s')) { + // Looks like a badged error message + args.splice(0, 3); + } if ( args[0] != null && ((typeof args[0] === 'object' && diff --git a/packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js b/packages/react-client/src/ReactClientConsoleConfigBrowser.js similarity index 82% rename from packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js rename to packages/react-client/src/ReactClientConsoleConfigBrowser.js index cc934685c8f7b..da87324b6df27 100644 --- a/packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js +++ b/packages/react-client/src/ReactClientConsoleConfigBrowser.js @@ -7,6 +7,8 @@ * @flow */ +import {warn, error} from 'shared/consoleWithStackDev'; + const badgeFormat = '%c%s%c '; // Same badge styling as DevTools. const badgeStyle = @@ -63,7 +65,12 @@ export function printToConsole( ); } - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); - return; + if (methodName === 'error') { + error.apply(console, newArgs); + } else if (methodName === 'warn') { + warn.apply(console, newArgs); + } else { + // eslint-disable-next-line react-internal/no-production-logging + console[methodName].apply(console, newArgs); + } } diff --git a/packages/react-client/src/ReactFlightClientConsoleConfigPlain.js b/packages/react-client/src/ReactClientConsoleConfigPlain.js similarity index 76% rename from packages/react-client/src/ReactFlightClientConsoleConfigPlain.js rename to packages/react-client/src/ReactClientConsoleConfigPlain.js index 1dbdec54cd078..a4e7c3c6d7e3c 100644 --- a/packages/react-client/src/ReactFlightClientConsoleConfigPlain.js +++ b/packages/react-client/src/ReactClientConsoleConfigPlain.js @@ -7,6 +7,8 @@ * @flow */ +import {warn, error} from 'shared/consoleWithStackDev'; + const badgeFormat = '[%s] '; const pad = ' '; @@ -44,7 +46,12 @@ export function printToConsole( newArgs.splice(offset, 0, badgeFormat, pad + badgeName + pad); } - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); - return; + if (methodName === 'error') { + error.apply(console, newArgs); + } else if (methodName === 'warn') { + warn.apply(console, newArgs); + } else { + // eslint-disable-next-line react-internal/no-production-logging + console[methodName].apply(console, newArgs); + } } diff --git a/packages/react-client/src/ReactFlightClientConsoleConfigServer.js b/packages/react-client/src/ReactClientConsoleConfigServer.js similarity index 83% rename from packages/react-client/src/ReactFlightClientConsoleConfigServer.js rename to packages/react-client/src/ReactClientConsoleConfigServer.js index 7567483245fa6..f6ecad92f3eef 100644 --- a/packages/react-client/src/ReactFlightClientConsoleConfigServer.js +++ b/packages/react-client/src/ReactClientConsoleConfigServer.js @@ -7,6 +7,8 @@ * @flow */ +import {warn, error} from 'shared/consoleWithStackDev'; + // This flips color using ANSI, then sets a color styling, then resets. const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c '; // Same badge styling as DevTools. @@ -64,7 +66,12 @@ export function printToConsole( ); } - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); - return; + if (methodName === 'error') { + error.apply(console, newArgs); + } else if (methodName === 'warn') { + warn.apply(console, newArgs); + } else { + // eslint-disable-next-line react-internal/no-production-logging + console[methodName].apply(console, newArgs); + } } diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 866805b5578c1..7d421f0422b56 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1730,6 +1730,7 @@ function resolveErrorDev( digest: string, message: string, stack: string, + env: string, ): void { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json @@ -1769,6 +1770,7 @@ function resolveErrorDev( } (error: any).digest = digest; + (error: any).environmentName = env; const errorWithDigest: ErrorWithDigest = (error: any); const chunks = response._chunks; const chunk = chunks.get(id); @@ -2056,6 +2058,8 @@ function resolveConsoleEntry( task.run(callStack); return; } + // TODO: Set the current owner so that consoleWithStackDev adds the component + // stack during the replay - if needed. } const rootTask = response._debugRootTask; if (rootTask != null) { @@ -2198,6 +2202,7 @@ function processFullRow( errorInfo.digest, errorInfo.message, errorInfo.stack, + errorInfo.env, ); } else { resolveErrorProd(response, id, errorInfo.digest); diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index bd2d98736addd..8a652dab4aaa8 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -127,6 +127,7 @@ describe('ReactFlight', () => { this.props.expectedMessage, ); expect(this.state.error.digest).toBe('a dev digest'); + expect(this.state.error.environmentName).toBe('Server'); } else { expect(this.state.error.message).toBe( 'An error occurred in the Server Components render. The specific message is omitted in production' + @@ -143,6 +144,7 @@ describe('ReactFlight', () => { expectedDigest = '[]'; } expect(this.state.error.digest).toContain(expectedDigest); + expect(this.state.error.environmentName).toBe(undefined); expect(this.state.error.stack).toBe( 'Error: ' + this.state.error.message, ); diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js index 87d87ea523e59..7ae8d5f5cdc7d 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js index 97b4afd13a835..28e2489cf22bb 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js index 51c832bff43a3..3a5ec6800fd57 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js index 50713ae8e8e68..461996a2e0887 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactFlightClientConsoleConfigPlain'; +export * from 'react-client/src/ReactClientConsoleConfigPlain'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js index 269f8ec0c2313..c08af0a6531de 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactFlightClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js index cafa02b686214..db8da42686215 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactFlightClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 017dc33081d5f..ddf6440a20e48 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export type Response = any; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js index 6c68ae163bb51..bf2071d6fc864 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigNode'; -export * from 'react-client/src/ReactFlightClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM'; export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js index f1e7d66ee8117..16f649249dd37 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigNode'; -export * from 'react-client/src/ReactFlightClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js index c6da80ef6060f..68047af97b9f8 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigNode'; -export * from 'react-client/src/ReactFlightClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode'; export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js index 95fd1590ab5c6..37a5322140a8a 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigNode'; -export * from 'react-client/src/ReactFlightClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js index 41c7e8e1d4706..867612c0ac2d7 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js @@ -8,7 +8,7 @@ */ export * from 'react-client/src/ReactFlightClientStreamConfigNode'; -export * from 'react-client/src/ReactFlightClientConsoleConfigServer'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode'; export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index e512fba13fc5d..a7cfc94e00a15 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -635,6 +635,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { NotPendingTransition: (null: TransitionStatus), resetFormInstance(form: Instance) {}, + + printToConsole(methodName, args, badgeName) { + // eslint-disable-next-line react-internal/no-production-logging + console[methodName].apply(console, args); + }, }; const hostConfig = useMutation diff --git a/packages/react-reconciler/src/ReactFiberErrorLogger.js b/packages/react-reconciler/src/ReactFiberErrorLogger.js index a948e5c79b694..addb0aea43dcd 100644 --- a/packages/react-reconciler/src/ReactFiberErrorLogger.js +++ b/packages/react-reconciler/src/ReactFiberErrorLogger.js @@ -20,6 +20,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; +import {printToConsole} from './ReactFiberConfig'; + // Side-channel since I'm not sure we want to make this part of the public API let componentName: null | string = null; let errorBoundaryName: null | string = null; @@ -94,13 +96,33 @@ export function defaultOnCaughtError( }.`; if (enableOwnerStacks) { - console.error( - '%o\n\n%s\n\n%s\n', - error, - componentNameMessage, - recreateMessage, - // We let our consoleWithStackDev wrapper add the component stack to the end. - ); + if ( + typeof error === 'object' && + error !== null && + typeof error.environmentName === 'string' + ) { + // This was a Server error. We print the environment name in a badge just like we do with + // replays of console logs to indicate that the source of this throw as actually the Server. + printToConsole( + 'error', + [ + '%o\n\n%s\n\n%s\n', + error, + componentNameMessage, + recreateMessage, + // We let our consoleWithStackDev wrapper add the component stack to the end. + ], + error.environmentName, + ); + } else { + console.error( + '%o\n\n%s\n\n%s\n', + error, + componentNameMessage, + recreateMessage, + // We let our consoleWithStackDev wrapper add the component stack to the end. + ); + } } else { // The current Fiber is disconnected at this point which means that console printing // cannot add a component stack since it terminates at the deletion node. This is not diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.art.js b/packages/react-reconciler/src/forks/ReactFiberConfig.art.js index 867ca996ec5af..1fb43b5ade8d3 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.art.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.art.js @@ -8,3 +8,4 @@ */ export * from 'react-art/src/ReactFiberConfigART'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 0bfe93a008cf1..24c80469c72a5 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -80,6 +80,7 @@ export const suspendInstance = $$$config.suspendInstance; export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady; export const NotPendingTransition = $$$config.NotPendingTransition; export const resetFormInstance = $$$config.resetFormInstance; +export const printToConsole = $$$config.printToConsole; // ------------------- // Microtasks diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js index 4932b1a787bb9..fa7dbe3123284 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js @@ -8,3 +8,4 @@ */ export * from 'react-dom-bindings/src/client/ReactFiberConfigDOM'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js b/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js index f1787a68e845b..2fb8768972da4 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js @@ -8,3 +8,4 @@ */ export * from 'react-native-renderer/src/ReactFiberConfigFabric'; +export * from 'react-client/src/ReactClientConsoleConfigPlain'; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.native.js b/packages/react-reconciler/src/forks/ReactFiberConfig.native.js index 3f8a28688b716..3e06abc660ef7 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.native.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.native.js @@ -8,3 +8,4 @@ */ export * from 'react-native-renderer/src/ReactFiberConfigNative'; +export * from 'react-client/src/ReactClientConsoleConfigPlain'; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.test.js b/packages/react-reconciler/src/forks/ReactFiberConfig.test.js index 85020417c2c7d..238434a50df8c 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.test.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.test.js @@ -8,3 +8,4 @@ */ export * from 'react-test-renderer/src/ReactFiberConfigTestHost'; +export * from 'react-client/src/ReactClientConsoleConfigPlain'; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index eedae9a46f1b0..75b195e1e25fb 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -78,6 +78,7 @@ import { resetResumableState, completeResumableState, emitEarlyPreloads, + printToConsole, } from './ReactFizzConfig'; import { constructClassInstance, @@ -363,7 +364,17 @@ export opaque type Request = { const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800; function defaultErrorHandler(error: mixed) { - console['error'](error); // Don't transform to our wrapper + if ( + typeof error === 'object' && + error !== null && + typeof error.environmentName === 'string' + ) { + // This was a Server error. We print the environment name in a badge just like we do with + // replays of console logs to indicate that the source of this throw as actually the Server. + printToConsole('error', [error], error.environmentName); + } else { + console['error'](error); // Don't transform to our wrapper + } return null; } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index cb138fd30a710..02847f204bc3e 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2774,11 +2774,18 @@ function emitErrorChunk( if (__DEV__) { let message; let stack = ''; + let env = request.environmentName(); try { if (error instanceof Error) { // eslint-disable-next-line react-internal/safe-string-coercion message = String(error.message); stack = getStack(error); + const errorEnv = (error: any).environmentName; + if (typeof errorEnv === 'string') { + // This probably came from another FlightClient as a pass through. + // Keep the environment name. + env = errorEnv; + } } else if (typeof error === 'object' && error !== null) { message = describeObjectForErrorMessage(error); } else { @@ -2788,7 +2795,7 @@ function emitErrorChunk( } catch (x) { message = 'An error occurred but serializing the error message failed.'; } - errorInfo = {digest, message, stack}; + errorInfo = {digest, message, stack, env}; } else { errorInfo = {digest}; } diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js index 07ebb3295f32f..c7964f187d42c 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.custom.js +++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js @@ -40,6 +40,8 @@ export const isPrimaryRenderer = false; export const supportsRequestStorage = false; export const requestStorage: AsyncLocalStorage = (null: any); +export const printToConsole = $$$config.printToConsole; + export const resetResumableState = $$$config.resetResumableState; export const completeResumableState = $$$config.completeResumableState; export const getChildFormatContext = $$$config.getChildFormatContext; diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js b/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js index 7c5ba9bce7e27..244202002edd7 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js @@ -10,6 +10,8 @@ import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; + // For now, we get this from the global scope, but this will likely move to a module. export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; export const requestStorage: AsyncLocalStorage = diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js index 84d49396efcdf..5695669839f81 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js @@ -10,5 +10,7 @@ import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy'; +export * from 'react-client/src/ReactClientConsoleConfigPlain'; + export const supportsRequestStorage = false; export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js index 8c9718e8234c3..5ee4566ad09ba 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js @@ -13,6 +13,8 @@ import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +export * from 'react-client/src/ReactClientConsoleConfigServer'; + export const supportsRequestStorage = true; export const requestStorage: AsyncLocalStorage = new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom.js b/packages/react-server/src/forks/ReactFizzConfig.dom.js index 2bf9be13273d6..17ddc166a7922 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom.js @@ -10,5 +10,7 @@ import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; +export * from 'react-client/src/ReactClientConsoleConfigBrowser'; + export const supportsRequestStorage = false; export const requestStorage: AsyncLocalStorage = (null: any);