Skip to content

Commit 349a99a

Browse files
authored
Badge Environment Name on Thrown Errors from the Server (#29846)
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: <img width="810" alt="Screenshot 2024-06-10 at 9 25 12 PM" src="https://github.com/facebook/react/assets/63648/8490fedc-09f6-4286-9332-fbe6b0faa2d3"> <img width="815" alt="Screenshot 2024-06-10 at 9 39 30 PM" src="https://github.com/facebook/react/assets/63648/bdcfc554-504a-4b1d-82bf-b717e74975ac"> 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.
1 parent 7045700 commit 349a99a

34 files changed

+134
-36
lines changed

packages/internal-test-utils/consoleMock.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,18 @@ export function createLogAssertion(
418418
let argIndex = 0;
419419
// console.* could have been called with a non-string e.g. `console.error(new Error())`
420420
// eslint-disable-next-line react-internal/safe-string-coercion
421-
String(format).replace(/%s/g, () => argIndex++);
421+
String(format).replace(/%s|%c/g, () => argIndex++);
422422
if (argIndex !== args.length) {
423-
logsMismatchingFormat.push({
424-
format,
425-
args,
426-
expectedArgCount: argIndex,
427-
});
423+
if (format.includes('%c%s')) {
424+
// We intentionally use mismatching formatting when printing badging because we don't know
425+
// the best default to use for different types because the default varies by platform.
426+
} else {
427+
logsMismatchingFormat.push({
428+
format,
429+
args,
430+
expectedArgCount: argIndex,
431+
});
432+
}
428433
}
429434

430435
// Check for extra component stacks

packages/internal-test-utils/shouldIgnoreConsoleError.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
module.exports = function shouldIgnoreConsoleError(format, args) {
44
if (__DEV__) {
55
if (typeof format === 'string') {
6+
if (format.startsWith('%c%s')) {
7+
// Looks like a badged error message
8+
args.splice(0, 3);
9+
}
610
if (
711
args[0] != null &&
812
((typeof args[0] === 'object' &&

packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js renamed to packages/react-client/src/ReactClientConsoleConfigBrowser.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @flow
88
*/
99

10+
import {warn, error} from 'shared/consoleWithStackDev';
11+
1012
const badgeFormat = '%c%s%c ';
1113
// Same badge styling as DevTools.
1214
const badgeStyle =
@@ -63,7 +65,12 @@ export function printToConsole(
6365
);
6466
}
6567

66-
// eslint-disable-next-line react-internal/no-production-logging
67-
console[methodName].apply(console, newArgs);
68-
return;
68+
if (methodName === 'error') {
69+
error.apply(console, newArgs);
70+
} else if (methodName === 'warn') {
71+
warn.apply(console, newArgs);
72+
} else {
73+
// eslint-disable-next-line react-internal/no-production-logging
74+
console[methodName].apply(console, newArgs);
75+
}
6976
}

packages/react-client/src/ReactFlightClientConsoleConfigPlain.js renamed to packages/react-client/src/ReactClientConsoleConfigPlain.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @flow
88
*/
99

10+
import {warn, error} from 'shared/consoleWithStackDev';
11+
1012
const badgeFormat = '[%s] ';
1113
const pad = ' ';
1214

@@ -44,7 +46,12 @@ export function printToConsole(
4446
newArgs.splice(offset, 0, badgeFormat, pad + badgeName + pad);
4547
}
4648

47-
// eslint-disable-next-line react-internal/no-production-logging
48-
console[methodName].apply(console, newArgs);
49-
return;
49+
if (methodName === 'error') {
50+
error.apply(console, newArgs);
51+
} else if (methodName === 'warn') {
52+
warn.apply(console, newArgs);
53+
} else {
54+
// eslint-disable-next-line react-internal/no-production-logging
55+
console[methodName].apply(console, newArgs);
56+
}
5057
}

packages/react-client/src/ReactFlightClientConsoleConfigServer.js renamed to packages/react-client/src/ReactClientConsoleConfigServer.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @flow
88
*/
99

10+
import {warn, error} from 'shared/consoleWithStackDev';
11+
1012
// This flips color using ANSI, then sets a color styling, then resets.
1113
const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c ';
1214
// Same badge styling as DevTools.
@@ -64,7 +66,12 @@ export function printToConsole(
6466
);
6567
}
6668

67-
// eslint-disable-next-line react-internal/no-production-logging
68-
console[methodName].apply(console, newArgs);
69-
return;
69+
if (methodName === 'error') {
70+
error.apply(console, newArgs);
71+
} else if (methodName === 'warn') {
72+
warn.apply(console, newArgs);
73+
} else {
74+
// eslint-disable-next-line react-internal/no-production-logging
75+
console[methodName].apply(console, newArgs);
76+
}
7077
}

packages/react-client/src/ReactFlightClient.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,7 @@ function resolveErrorDev(
17301730
digest: string,
17311731
message: string,
17321732
stack: string,
1733+
env: string,
17331734
): void {
17341735
if (!__DEV__) {
17351736
// 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(
17691770
}
17701771

17711772
(error: any).digest = digest;
1773+
(error: any).environmentName = env;
17721774
const errorWithDigest: ErrorWithDigest = (error: any);
17731775
const chunks = response._chunks;
17741776
const chunk = chunks.get(id);
@@ -2056,6 +2058,8 @@ function resolveConsoleEntry(
20562058
task.run(callStack);
20572059
return;
20582060
}
2061+
// TODO: Set the current owner so that consoleWithStackDev adds the component
2062+
// stack during the replay - if needed.
20592063
}
20602064
const rootTask = response._debugRootTask;
20612065
if (rootTask != null) {
@@ -2198,6 +2202,7 @@ function processFullRow(
21982202
errorInfo.digest,
21992203
errorInfo.message,
22002204
errorInfo.stack,
2205+
errorInfo.env,
22012206
);
22022207
} else {
22032208
resolveErrorProd(response, id, errorInfo.digest);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ describe('ReactFlight', () => {
127127
this.props.expectedMessage,
128128
);
129129
expect(this.state.error.digest).toBe('a dev digest');
130+
expect(this.state.error.environmentName).toBe('Server');
130131
} else {
131132
expect(this.state.error.message).toBe(
132133
'An error occurred in the Server Components render. The specific message is omitted in production' +
@@ -143,6 +144,7 @@ describe('ReactFlight', () => {
143144
expectedDigest = '[]';
144145
}
145146
expect(this.state.error.digest).toContain(expectedDigest);
147+
expect(this.state.error.environmentName).toBe(undefined);
146148
expect(this.state.error.stack).toBe(
147149
'Error: ' + this.state.error.message,
148150
);

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1212
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
1313
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser';
1414
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1212
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
1313
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser';
1414
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser';

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
11-
export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
11+
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
1313
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser';
1414
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser';

0 commit comments

Comments
 (0)