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

fix(react): Handle case where error.cause already defined #7557

Merged
merged 4 commits into from Mar 22, 2023

Conversation

AbhiPrasad
Copy link
Member

ref: #1401 (comment)

If error.cause is already defined, attempt to walk down the error chain to set the ReactErrorBoundary error.

@AbhiPrasad AbhiPrasad requested a review from a team March 21, 2023 17:45
@AbhiPrasad AbhiPrasad self-assigned this Mar 21, 2023
@AbhiPrasad AbhiPrasad requested review from mydea and lforst and removed request for a team March 21, 2023 17:45
@github-actions
Copy link
Contributor

github-actions bot commented Mar 21, 2023

size-limit report 📦

Path Size
@sentry/browser - ES5 CDN Bundle (gzipped + minified) 20.44 KB (+0.01% 🔺)
@sentry/browser - ES5 CDN Bundle (minified) 63.52 KB (0%)
@sentry/browser - ES6 CDN Bundle (gzipped + minified) 19.13 KB (+0.01% 🔺)
@sentry/browser - ES6 CDN Bundle (minified) 56.57 KB (0%)
@sentry/browser - Webpack (gzipped + minified) 21.59 KB (0%)
@sentry/browser - Webpack (minified) 71.65 KB (0%)
@sentry/react - Webpack (gzipped + minified) 21.61 KB (0%)
@sentry/nextjs Client - Webpack (gzipped + minified) 51.85 KB (0%)
@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified) 27.77 KB (0%)
@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified) 26.08 KB (0%)
@sentry/replay ES6 CDN Bundle (gzipped + minified) 44.21 KB (0%)
@sentry/replay - Webpack (gzipped + minified) 38.28 KB (0%)
@sentry/browser + @sentry/tracing + @sentry/replay - ES6 CDN Bundle (gzipped + minified) 62.4 KB (0%)
@sentry/browser + @sentry/replay - ES6 CDN Bundle (gzipped + minified) 55.94 KB (0%)

@@ -66,6 +66,13 @@ const INITIAL_STATE = {
eventId: null,
};

function setCause(error: Error & { cause?: Error }, cause: Error): void {
Copy link
Member

Choose a reason for hiding this comment

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

Unlikely that it is gonna happen but this could infinitely recurse. Are we worried about that?

Copy link
Member Author

Choose a reason for hiding this comment

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

you are right, let me be a little more defensive and track the visited nodes.

Copy link
Member Author

Choose a reason for hiding this comment

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

done with d0c99b2

@@ -66,6 +66,25 @@ const INITIAL_STATE = {
eventId: null,
};

function setCause(error: Error & { cause?: Error }, cause: Error): void {
const seenErrors = new Map<Error, boolean>();
Copy link
Member

Choose a reason for hiding this comment

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

Should we make this a WeakMap to avoid GC issues? Probably doesn't matter but doesn't hurt I guess..?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah I will update

@AbhiPrasad AbhiPrasad enabled auto-merge (squash) March 22, 2023 15:16
@AbhiPrasad AbhiPrasad changed the title feat(react): Handle case where error.cause already defined fix(react): Handle case where error.cause already defined Mar 22, 2023
@AbhiPrasad AbhiPrasad merged commit 8e78e6e into develop Mar 22, 2023
33 checks passed
@AbhiPrasad AbhiPrasad deleted the abhi-react-cause-nested branch March 22, 2023 15:38
@@ -66,6 +66,25 @@ const INITIAL_STATE = {
eventId: null,
};

function setCause(error: Error & { cause?: Error }, cause: Error): void {
Copy link

Choose a reason for hiding this comment

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

This looks weird to me:

  • fn signature: setCause(error, cause)
  • usage: setCause(error, errorBoundaryError);

errorBoundaryError is NOT the cause of the error being captured by the boundary. errorBoundaryError is an artificial error that you created at the top of the tree.

I have no idea why you try to put this error as a cause deeply in the error stack, but that looks really suspicious to me. errorBoundaryError should rather be the parent top-level error, not the most deeply nested cause.

Comment on lines +338 to +341
const cause = firstError.cause;
expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
expect(cause.name).toContain('React ErrorBoundary');
expect(cause.message).toEqual(thirdError.message);
Copy link

Choose a reason for hiding this comment

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

The errorBoundaryError shouldn't be deeply nested, it should be the parent instead. The fact that an error has been captured by the boundary is not the "root cause" of the problem, it's the consequence.

The root cause (deeply nested cause) is something low level like "undefined is not a function", not "Sentry Error Boundary captured an error"

Copy link
Member Author

Choose a reason for hiding this comment

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

We patch the error boundary error at the end so that it does not affect issue grouping - since there are some flaws to the synthetic stacktrace generated by the error boundary error (since componenStack is generated under the hood by throwing errors and walking up the component tree, it can sometimes point to the wrong line of code).

This is the most ergonomic way to get the error into sentry (and sourcemapped so you can see actual filenames/source context) without affecting existing/new grouping configurations.

You're right that this is conceptually wrong - but IMO the drawback is acceptable to make sure users get the best experience possible while staying within the limitations of both the componentStack and the sentry event schema.

Copy link
Member Author

Choose a reason for hiding this comment

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

Re: the imperfections in component stacks, see: facebook/react#18561

The idea is to throw as early as possible. Ideally it's the first line such as the built-in super call of a class or the destructuring of props in the argument position. It could however be slightly later which might be slightly confusing to see the line of the first hook for example. Source code aware tooling could adjust this number to the previous function definition before the offset. If it doesn't throw at all such as if no hooks and no props are used, then we can't figure out the line number.

Relying on the original error is the best way we can get the best issue grouping for our users.

Copy link

Choose a reason for hiding this comment

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

Thanks for explaining. Not sure exactly what's going on here but if you know what you are doing 👍 . Will test this on Sentry soon and see if it works as I expect it to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants