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

feat(react): Add Sentry.reactErrorHandler #12147

Merged
merged 11 commits into from
May 27, 2024
Merged

Conversation

AbhiPrasad
Copy link
Member

@AbhiPrasad AbhiPrasad commented May 21, 2024

ref #11798

React 19 brings some improvements to error handling with https://react.dev/blog/2024/04/25/react-19#error-handling! This PR exposes a new method, Sentry.reactErrorHandler that helps users interact with these new React 19 error handling hooks.

Background

React Error handling originally relied only on error boundaries, React components that would automatically catch component rendering errors and display fallback UIs.

class ErrorBoundary extends React.Component {
  // ...

  public componentDidCatch(error: any, info: React.ErrorInfo): void {
    // Example "componentStack":
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logErrorToMyService(error, info.componentStack);
  }
}

We added support for this by exporting our own ErrorBoundary component.

If you notice above, the info parameter passed into componentDidCatch has a componentStack property. componentStack is a synthetic stacktrace that represents the component call-chain that threw the error. Fortunately because this is a synthetic stacktrace, we can parse it as an error in the SDK and attach it to the original exception as a linked exception via error.cause.

Important to note: If a component chain is not wrapped in an error boundary though, we do not get access to the componentStack, as the error bubbles up to the global error handler.

React 19

In React 19 they've two new options, onCaughtError and onUncaughtError to the React DOM public API.

onCaughtError is a callback called when React catches an error in an Error Boundary. This effectively works just like having an ErrorBoundary with componentDidCatch.

onUncaughtError is a callback called when an error is thrown and not caught by an Error Boundary. This means we can add componentStack information to these errors without requiring users to add error boundaries everywhere (useful for 3rd party component libraries and similar).

Given onCaughtError, onUncaughtError and componentDidCatch all have identical APIs

declare function componentDidCatch(error: any, info: React.ErrorInfo): void;

This PR introduces Sentry.reactErrorHandler, which looks like so:

import * as Sentry from '@sentry/react';
import { hydrateRoot } from "react-dom/client";

ReactDOM.hydrateRoot(
  document.getElementById("root"),
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  {
    onUncaughtError: Sentry.reactErrorHandler(),
    onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
      // optional callback if users want custom config.
    }),
  }
);

To validate this change, we add a react 19 e2e test.

Next Steps

After we merge this in and update the docs, we can explore automatically instrumenting createRoot from react-dom to add Sentry.reactErrorHandler accordingly, but that is a next step.

Copy link
Contributor

github-actions bot commented May 21, 2024

size-limit report 📦

Path Size
@sentry/browser 21.78 KB (0%)
@sentry/browser (incl. Tracing) 32.79 KB (-0.01% 🔽)
@sentry/browser (incl. Tracing, Replay) 68.26 KB (0%)
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 61.68 KB (+0.01% 🔺)
@sentry/browser (incl. Tracing, Replay with Canvas) 72.31 KB (0%)
@sentry/browser (incl. Tracing, Replay, Feedback) 84.37 KB (0%)
@sentry/browser (incl. Tracing, Replay, Feedback, metrics) 85.84 KB (0%)
@sentry/browser (incl. metrics) 23.17 KB (0%)
@sentry/browser (incl. Feedback) 37.8 KB (0%)
@sentry/browser (incl. sendFeedback) 26.36 KB (0%)
@sentry/browser (incl. FeedbackAsync) 30.79 KB (0%)
@sentry/react 24.5 KB (+0.11% 🔺)
@sentry/react (incl. Tracing) 35.81 KB (+0.08% 🔺)
@sentry/vue 25.73 KB (0%)
@sentry/vue (incl. Tracing) 34.59 KB (0%)
@sentry/svelte 21.92 KB (0%)
CDN Bundle 23.01 KB (0%)
CDN Bundle (incl. Tracing) 34.27 KB (+0.01% 🔺)
CDN Bundle (incl. Tracing, Replay) 68.08 KB (0%)
CDN Bundle (incl. Tracing, Replay, Feedback) 73.09 KB (0%)
CDN Bundle - uncompressed 67.88 KB (0%)
CDN Bundle (incl. Tracing) - uncompressed 101.68 KB (0%)
CDN Bundle (incl. Tracing, Replay) - uncompressed 211.58 KB (0%)
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 223.96 KB (0%)
@sentry/nextjs (client) 35.14 KB (0%)
@sentry/sveltekit (client) 33.39 KB (0%)
@sentry/node 114.64 KB (-0.01% 🔽)
@sentry/aws-serverless 103.32 KB (+0.01% 🔺)

@AbhiPrasad AbhiPrasad mentioned this pull request May 21, 2024
const root = createRoot(container, {
// Callback called when an error is thrown and not caught by an Error Boundary.
onUncaughtError: (error, errorInfo) => {
Sentry.captureReactException(error, errorInfo);
Copy link
Member

Choose a reason for hiding this comment

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

I would vote for a different name here - imho this is a bit misleading, like I could easily see users just using this instead of captureException() 🤔

What if instead we'd do something like this:

const root = createRoot(container, {
  // Callback called when an error is thrown and not caught by an Error Boundary.
  onUncaughtError: Sentry.reactErrorHandler(),
  onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
    // optional callback if users want more/custom config in addition?
  })
});

Copy link
Member Author

Choose a reason for hiding this comment

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

I like Sentry.reactErrorHandler a lot better - will change!

Copy link
Member Author

Choose a reason for hiding this comment

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

I have some concerns with my initial approach that also applies to Sentry.reactErrorHandler. #11798 (comment)

Pinged react team member on this, let's see if we can get feedback.

* Recurse through `error.cause` chain to set cause on an error.
*/
export function setCause(error: Error & { cause?: Error }, cause: Error): void {
const seenErrors = new WeakMap<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.

l: could use a WeakSet here instead, wdyt?

@AbhiPrasad AbhiPrasad marked this pull request as draft May 24, 2024 10:44
AbhiPrasad added a commit that referenced this pull request May 24, 2024
Closes #12200

From my personal testing everything works with with React 19, so we can
bump the peer dep.

For the new error APIs, there is
#12147, but that PR
is draft while we get feedback from the React team.
@AbhiPrasad AbhiPrasad changed the title feat(react): Add Sentry.captureReactException feat(react): Add Sentry.reactErrorHandler May 24, 2024
@AbhiPrasad AbhiPrasad force-pushed the abhi-react-19-api branch 2 times, most recently from cf24410 to 2550dac Compare May 24, 2024 21:17
@AbhiPrasad AbhiPrasad marked this pull request as ready for review May 24, 2024 21:17
@AbhiPrasad AbhiPrasad self-assigned this May 24, 2024
@AbhiPrasad AbhiPrasad enabled auto-merge (squash) May 27, 2024 15:33
@AbhiPrasad AbhiPrasad disabled auto-merge May 27, 2024 15:53
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

3 participants