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

Remove Component Stack from React Logged Warnings and Error Reporting #30308

Merged
merged 6 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
442 changes: 370 additions & 72 deletions packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js

Large diffs are not rendered by default.

45 changes: 43 additions & 2 deletions packages/internal-test-utils/consoleMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ const patchConsoleMethod = (
return;
}

// Append Component Stacks. Simulates a framework or DevTools appending them.
if (
typeof format === 'string' &&
(methodName === 'error' || methodName === 'warn')
) {
const React = require('react');
if (React.captureOwnerStack) {
// enableOwnerStacks enabled. When it's always on, we can assume this case.
const stack = React.captureOwnerStack();
if (stack) {
format += '%s';
args.push(stack);
}
} else {
// Otherwise we have to use internals to emulate parent stacks.
const ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
if (ReactSharedInternals && ReactSharedInternals.getCurrentStack) {
const stack = ReactSharedInternals.getCurrentStack();
if (stack !== '') {
format += '%s';
args.push(stack);
}
}
}
}

// Capture the call stack now so we can warn about it later.
// The call stack has helpful information for the test author.
// Don't throw yet though b'c it might be accidentally caught and suppressed.
Expand Down Expand Up @@ -204,7 +232,7 @@ export function assertConsoleLogsCleared() {
if (warnings.length > 0) {
message += `\nconsole.warn was called without assertConsoleWarnDev:\n${diff(
'',
warnings.join('\n'),
warnings.map(normalizeComponentStack).join('\n'),
{
omitAnnotationLines: true,
},
Expand All @@ -213,7 +241,7 @@ export function assertConsoleLogsCleared() {
if (errors.length > 0) {
message += `\nconsole.error was called without assertConsoleErrorDev:\n${diff(
'',
errors.join('\n'),
errors.map(normalizeComponentStack).join('\n'),
{
omitAnnotationLines: true,
},
Expand Down Expand Up @@ -249,6 +277,19 @@ function normalizeCodeLocInfo(str) {
});
}

function normalizeComponentStack(entry) {
if (
typeof entry[0] === 'string' &&
entry[0].endsWith('%s') &&
isLikelyAComponentStack(entry[entry.length - 1])
) {
const clone = entry.slice(0);
clone[clone.length - 1] = normalizeCodeLocInfo(entry[entry.length - 1]);
return clone;
}
return entry;
}

const isLikelyAComponentStack = message =>
typeof message === 'string' &&
(message.indexOf('<component stack>') > -1 ||
Expand Down
26 changes: 21 additions & 5 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1436,12 +1436,23 @@ describe('ReactFlight', () => {

it('should warn in DEV a child is missing keys on server component', () => {
function NoKey({children}) {
return <div key="this has a key but parent doesn't" />;
return ReactServer.createElement('div', {
key: "this has a key but parent doesn't",
});
}
expect(() => {
// While we're on the server we need to have the Server version active to track component stacks.
jest.resetModules();
jest.mock('react', () => ReactServer);
const transport = ReactNoopFlightServer.render(
<div>{Array(6).fill(<NoKey />)}</div>,
ReactServer.createElement(
'div',
null,
Array(6).fill(ReactServer.createElement(NoKey)),
),
);
jest.resetModules();
jest.mock('react', () => React);
ReactNoopFlightClient.read(transport);
}).toErrorDev('Each child in a list should have a unique "key" prop.');
});
Expand Down Expand Up @@ -2814,7 +2825,7 @@ describe('ReactFlight', () => {
});

// @gate (enableOwnerStacks && enableServerComponentLogs) || !__DEV__
it('should not include component stacks in replayed logs (unless DevTools add them)', () => {
it('should include only one component stack in replayed logs (if DevTools or polyfill adds them)', () => {
class MyError extends Error {
toJSON() {
return 123;
Expand All @@ -2839,6 +2850,9 @@ describe('ReactFlight', () => {
return ReactServer.createElement(Bar);
}

// While we're on the server we need to have the Server version active to track component stacks.
jest.resetModules();
jest.mock('react', () => ReactServer);
const transport = ReactNoopFlightServer.render(
ReactServer.createElement(App),
);
Expand All @@ -2857,6 +2871,8 @@ describe('ReactFlight', () => {
]);

// Replay logs on the client
jest.resetModules();
jest.mock('react', () => React);
ReactNoopFlightClient.read(transport);
assertConsoleErrorDev(
[
Expand All @@ -2866,8 +2882,8 @@ describe('ReactFlight', () => {
' <div>Womp womp: {Error}</div>\n' +
' ^^^^^^^',
],
// We should not have a stack in the replay because that should be added either by console.createTask
// or React DevTools on the client. Neither of which we do here.
// We should have a stack in the replay but we don't yet set the owner from the Flight replaying
// so our simulated polyfill doesn't end up getting any component stacks yet.
{withoutStack: true},
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
expect.stringContaining('%s'),
expect.stringContaining('An error occurred in the <Foo> component'),
expect.stringContaining('Consider adding an error boundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down Expand Up @@ -208,7 +209,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
'The above error occurred in the <Foo> component',
),
expect.stringContaining('ErrorBoundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down Expand Up @@ -274,7 +276,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
expect.stringContaining('%s'),
expect.stringContaining('An error occurred in the <Foo> component'),
expect.stringContaining('Consider adding an error boundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down Expand Up @@ -344,7 +347,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
'The above error occurred in the <Foo> component',
),
expect.stringContaining('ErrorBoundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down Expand Up @@ -410,7 +414,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
expect.stringContaining('%s'),
expect.stringContaining('An error occurred in the <Foo> component'),
expect.stringContaining('Consider adding an error boundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down Expand Up @@ -478,7 +483,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
'The above error occurred in the <Foo> component',
),
expect.stringContaining('ErrorBoundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
// Addendum by React:
expect.stringContaining('An error occurred in the <Foo> component'),
expect.stringContaining('Consider adding an error boundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);

Expand Down Expand Up @@ -239,7 +240,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
'The above error occurred in the <Foo> component',
),
expect.stringContaining('ErrorBoundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down Expand Up @@ -309,7 +311,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
// Addendum by React:
expect.stringContaining('An error occurred in the <Foo> component'),
expect.stringContaining('Consider adding an error boundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);

Expand Down Expand Up @@ -390,7 +393,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
'The above error occurred in the <Foo> component',
),
expect.stringContaining('ErrorBoundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down Expand Up @@ -460,7 +464,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
// Addendum by React:
expect.stringContaining('An error occurred in the <Foo> component'),
expect.stringContaining('Consider adding an error boundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);

Expand Down Expand Up @@ -540,7 +545,8 @@ describe('ReactDOMConsoleErrorReporting', () => {
'The above error occurred in the <Foo> component',
),
expect.stringContaining('ErrorBoundary'),
expect.stringContaining('Foo'),
// The component stack is not added without the polyfill/devtools.
// expect.stringContaining('Foo'),
],
]);
} else {
Expand Down
17 changes: 9 additions & 8 deletions packages/react-dom/src/__tests__/ReactUpdates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1857,12 +1857,14 @@ describe('ReactUpdates', () => {
}

let error = null;
let stack = null;
let ownerStack = null;
let nativeStack = null;
const originalConsoleError = console.error;
console.error = (e, s) => {
console.error = e => {
error = e;
stack = s;
ownerStack = gate(flags => flags.enableOwnerStacks)
? React.captureOwnerStack()
: null;
nativeStack = new Error().stack;
Scheduler.log('stop');
};
Expand All @@ -1878,12 +1880,11 @@ describe('ReactUpdates', () => {
expect(error).toContain('Maximum update depth exceeded');
// The currently executing effect should be on the native stack
expect(nativeStack).toContain('at myEffect');
if (!gate(flags => flags.enableOwnerStacks)) {
// The currently running component's name is not in the owner
// stack because it's just its JSX callsite.
expect(stack).toContain('at NonTerminating');
if (gate(flags => flags.enableOwnerStacks)) {
expect(ownerStack).toContain('at App');
} else {
expect(ownerStack).toBe(null);
}
expect(stack).toContain('at App');
});

it('can have nested updates if they do not cross the limit', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,16 @@ describe('ReactIncrementalErrorLogging', () => {
'Consider adding an error boundary to your tree ' +
'to customize error handling behavior.',
),
expect.stringMatching(
new RegExp(
gate(flags => flags.enableOwnerStacks)
? '\\s+(in|at) ErrorThrowingComponent'
: '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
'\\s+(in|at) span(.*)\n' +
'\\s+(in|at) div(.*)',
),
),
// The component stack is not added without the polyfill/devtools.
// expect.stringMatching(
// new RegExp(
// gate(flags => flags.enableOwnerStacks)
// ? '\\s+(in|at) ErrorThrowingComponent'
// : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
// '\\s+(in|at) span(.*)\n' +
// '\\s+(in|at) div(.*)',
// ),
// ),
);
}
});
Expand Down Expand Up @@ -139,15 +140,16 @@ describe('ReactIncrementalErrorLogging', () => {
'Consider adding an error boundary to your tree ' +
'to customize error handling behavior.',
),
expect.stringMatching(
new RegExp(
gate(flags => flags.enableOwnerStacks)
? '\\s+(in|at) ErrorThrowingComponent'
: '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
'\\s+(in|at) span(.*)\n' +
'\\s+(in|at) div(.*)',
),
),
// The component stack is not added without the polyfill/devtools.
// expect.stringMatching(
// new RegExp(
// gate(flags => flags.enableOwnerStacks)
// ? '\\s+(in|at) ErrorThrowingComponent'
// : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
// '\\s+(in|at) span(.*)\n' +
// '\\s+(in|at) div(.*)',
// ),
// ),
);
}
});
Expand Down Expand Up @@ -199,16 +201,17 @@ describe('ReactIncrementalErrorLogging', () => {
'React will try to recreate this component tree from scratch ' +
'using the error boundary you provided, ErrorBoundary.',
),
expect.stringMatching(
new RegExp(
gate(flags => flags.enableOwnerStacks)
? '\\s+(in|at) ErrorThrowingComponent'
: '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
'\\s+(in|at) span(.*)\n' +
'\\s+(in|at) ErrorBoundary(.*)\n' +
'\\s+(in|at) div(.*)',
),
),
// The component stack is not added without the polyfill/devtools.
// expect.stringMatching(
// new RegExp(
// gate(flags => flags.enableOwnerStacks)
// ? '\\s+(in|at) ErrorThrowingComponent'
// : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
// '\\s+(in|at) span(.*)\n' +
// '\\s+(in|at) ErrorBoundary(.*)\n' +
// '\\s+(in|at) div(.*)',
// ),
// ),
);
} else {
expect(logCapturedErrorCalls[0]).toEqual(
Expand Down Expand Up @@ -282,13 +285,14 @@ describe('ReactIncrementalErrorLogging', () => {
'React will try to recreate this component tree from scratch ' +
'using the error boundary you provided, ErrorBoundary.',
),
expect.stringMatching(
gate(flag => flag.enableOwnerStacks)
? new RegExp('\\s+(in|at) Foo')
: new RegExp(
'\\s+(in|at) Foo (.*)\n' + '\\s+(in|at) ErrorBoundary(.*)',
),
),
// The component stack is not added without the polyfill/devtools.
// expect.stringMatching(
// gate(flag => flag.enableOwnerStacks)
// ? new RegExp('\\s+(in|at) Foo')
// : new RegExp(
// '\\s+(in|at) Foo (.*)\n' + '\\s+(in|at) ErrorBoundary(.*)',
// ),
// ),
);
} else {
expect(console.error).toHaveBeenCalledWith(
Expand Down
Loading