Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Badge Environment Name on Thrown Errors from the Server #29846

Merged
merged 10 commits into from
Jun 26, 2024
17 changes: 11 additions & 6 deletions packages/internal-test-utils/consoleMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this mirror the check in shouldIgnoreConsoleError i.e.

Suggested change
if (format.includes('%c%s')) {
if (format.startsWith('%c%s%c')) {

or are these files unrelated?

To be honest, I don't quite follow why we need to ignore this case and why there's no good default for our test suite. Could you add test to packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js to illustrate what this is doing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In shouldIgnoreConsoleError the more specific one was ok because we didn't test any paths that used the server printer:

https://github.com/sebmarkbage/react/blob/ba2389424a1deb617e62d8473e4c2e718a2ed984/packages/react-client/src/ReactClientConsoleConfigServer.js#L13

But tbh, none of this makes any sense. We should find a way to remove shouldIgnoreConsoleError. There must be some systemic issue in our tests that we can address so we don't need this anymore. I keep just hacking in whatever I need to keep it running.

Regarding the argument mismatch I feel like we should just remove this assertion. We've found a case where this is valid. So if we keep it we need some way to disable it. Isn't the lint warning enough for the common mistakes and we can disable the lint warning as needed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But tbh, none of this makes any sense. We should find a way to remove shouldIgnoreConsoleError

I'm all for that. I just don't know which test hits this path so I can't help.

Isn't the lint warning enough for the common mistakes and we can disable the lint warning as needed?

It should be. Maybe the additional runtime check was for calls that aren't checked by the lint rule. I can double check.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the usual assertConsoleErrorDev for now but probably want a specific assertConsoleBadgedErrorDev in the future to encode the behavior clearer: sebmarkbage#6

// 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
Expand Down
4 changes: 4 additions & 0 deletions packages/internal-test-utils/shouldIgnoreConsoleError.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ensuring we're filtering the same as before. No change in what gets filtered.

if (
args[0] != null &&
((typeof args[0] === 'object' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @flow
*/

import {warn, error} from 'shared/consoleWithStackDev';

const badgeFormat = '%c%s%c ';
// Same badge styling as DevTools.
const badgeStyle =
Expand Down Expand Up @@ -63,7 +65,14 @@ export function printToConsole(
);
}

// eslint-disable-next-line react-internal/no-production-logging
console[methodName].apply(console, newArgs);
return;
if (methodName === 'error') {
// eslint-disable-next-line react-internal/no-production-logging
error.apply(console, newArgs);
} else if (methodName === 'warn') {
// eslint-disable-next-line react-internal/no-production-logging
warn.apply(console, newArgs);
} else {
// eslint-disable-next-line react-internal/no-production-logging
console[methodName].apply(console, newArgs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @flow
*/

import {warn, error} from 'shared/consoleWithStackDev';

const badgeFormat = '[%s] ';
const pad = ' ';

Expand Down Expand Up @@ -44,7 +46,14 @@ 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') {
// eslint-disable-next-line react-internal/no-production-logging
error.apply(console, newArgs);
} else if (methodName === 'warn') {
// eslint-disable-next-line react-internal/no-production-logging
warn.apply(console, newArgs);
} else {
// eslint-disable-next-line react-internal/no-production-logging
console[methodName].apply(console, newArgs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -64,7 +66,14 @@ export function printToConsole(
);
}

// eslint-disable-next-line react-internal/no-production-logging
console[methodName].apply(console, newArgs);
return;
if (methodName === 'error') {
// eslint-disable-next-line react-internal/no-production-logging
error.apply(console, newArgs);
} else if (methodName === 'warn') {
// eslint-disable-next-line react-internal/no-production-logging
warn.apply(console, newArgs);
} else {
// eslint-disable-next-line react-internal/no-production-logging
console[methodName].apply(console, newArgs);
}
}
5 changes: 5 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -2198,6 +2202,7 @@ function processFullRow(
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
errorInfo.env,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
Expand Down
2 changes: 2 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' +
Expand All @@ -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,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 5 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 29 additions & 7 deletions packages/react-reconciler/src/ReactFiberErrorLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,13 +96,33 @@ export function defaultOnCaughtError(
}.`;

if (enableOwnerStacks) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only added this feature to the enableOwnerStacks flag because we the other branch uses the uninstrumented console['error'](...) and we don't have a version of printToConsole that excludes adding a component stack. So to avoid forking all that behavior for the old branch I just made this a "new world" feature.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/

export * from 'react-art/src/ReactFiberConfigART';
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/

export * from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/

export * from 'react-native-renderer/src/ReactFiberConfigFabric';
export * from 'react-client/src/ReactClientConsoleConfigPlain';
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/

export * from 'react-native-renderer/src/ReactFiberConfigNative';
export * from 'react-client/src/ReactClientConsoleConfigPlain';
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/

export * from 'react-test-renderer/src/ReactFiberConfigTestHost';
export * from 'react-client/src/ReactClientConsoleConfigPlain';
13 changes: 12 additions & 1 deletion packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
resetResumableState,
completeResumableState,
emitEarlyPreloads,
printToConsole,
} from './ReactFizzConfig';
import {
constructClassInstance,
Expand Down Expand Up @@ -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;
}

Expand Down
Loading
Loading