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 45a59d2b4c..328cf3d90d 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 383650d25a..0b5798d241 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 cc934685c8..da87324b6d 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 1dbdec54cd..a4e7c3c6d7 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 7567483245..f6ecad92f3 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 866805b557..7d421f0422 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 bd2d98736a..8a652dab4a 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 87d87ea523..7ae8d5f5cd 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 97b4afd13a..28e2489cf2 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 51c832bff4..3a5ec6800f 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 50713ae8e8..461996a2e0 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 269f8ec0c2..c08af0a653 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 cafa02b686..db8da42686 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 017dc33081..ddf6440a20 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 6c68ae163b..bf2071d6fc 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 f1e7d66ee8..16f649249d 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 c6da80ef60..68047af97b 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 95fd1590ab..37a5322140 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 41c7e8e1d4..867612c0ac 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 e512fba13f..a7cfc94e00 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 a948e5c79b..addb0aea43 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 867ca996ec..1fb43b5ade 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 0bfe93a008..24c80469c7 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 4932b1a787..fa7dbe3123 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 f1787a68e8..2fb8768972 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 3f8a28688b..3e06abc660 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 85020417c2..238434a50d 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 eedae9a46f..75b195e1e2 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 cb138fd30a..02847f204b 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 07ebb3295f..c7964f187d 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 7c5ba9bce7..244202002e 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 84d49396ef..5695669839 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 8c9718e823..5ee4566ad0 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 2bf9be1327..17ddc166a7 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);