getsentry / sentry-javascript

Official Sentry SDKs for JavaScript
https://sentry.io
MIT License
7.86k stars 1.55k forks source link

[react] Sentry.captureException only works if in same file as Sentry.init #8031

Closed ci-vamp closed 1 year ago

ci-vamp commented 1 year ago

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/react

SDK Version

7.50.0

Framework Version

17.0.2

Link to Sentry event

No response

SDK Setup

Sentry.init({
  dsn: 'MY_DSN',
  debug: true,
  autoSessionTracking: true,
  normalizeDepth: 5,
  tracesSampleRate: 1.0,
  enabled: true,
  environment: 'development',
  beforeSend(event, hint) {
    console.log('beforeSend', { event, hint });

    return event;
  }
});

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Steps to Reproduce

  1. init Sentry in index.tsx (root where react render is called)
  2. try using Sentry.captureException anywhere else in the application

Expected Result

Sentry.captureException sends an exception event to sentry

Actual Result

nothing is sent. nothing in network tab, nothing in beforeSend hook

the only time anything is sent is if i use Sentry.captureException in the index.tsx directly below the Sentry.init setup.

i have tried with debug and using beforeSend but the only one that shows up is the one in index.tsx.

AbhiPrasad commented 1 year ago

Hey @ci-vamp, thanks for writing in!

I tried reproducing this in a codesandbox: https://codesandbox.io/s/gh-8031-react-captureexception-4xpt5z?file=/src/App.tsx, and I saw able to get this working. Could you provide a reproduction so we can investigate further? Thanks!

ci-vamp commented 1 year ago

debugging this here is what i find:

(root, index.tsx, Sentry.captureException)

image image

(App, App.tsx, Sentry.captureException)

image image image
ci-vamp commented 1 year ago

@AbhiPrasad the code sandbox example is not equivalent.

you are using react 18.2 we are on 17.2. could that have an impact?

i tried downgrading to sentry 6.9.x and issue persisted

ci-vamp commented 1 year ago

you can see that in index.tsx (where Sentry.init is called) when it reaches the singleton and client for captureException it is fully populated (DSN etc)

however, when it reaches the same point from the call in App.tsx nothing is populated

AbhiPrasad commented 1 year ago

you are using react 18.2 we are on 17.2. could that have an impact

It shouldn't have an impact. I updated the codesandbox to use React 17: https://codesandbox.io/s/gh-8031-react-captureexception-4xpt5z?file=/src/index.tsx

however, when it reaches the same point from the call in App.tsx nothing is populated

Ah what might be happening then is that Sentry is not initialized before that code is executed. Basically the imported code runs before Sentry.init is being called. You can debug this by placing some console.log statements by the Sentry calls to see execution order.

Make sure that Sentry.init is being called as early as possible!

ci-vamp commented 1 year ago

@AbhiPrasad

Sentry.init is the first thing in index.tsx (after imports) which i think is the normal behavior for a react app.

root/ index.tsx <-- imports App, inits Sentry App.tsx <-- imported into index.tsx

i have debugged and am seeing that the index.tsx captureException is indeed firing before the App.tsx captureException, which indicates that the Sentry.init is made before App.tsx capturException is fired

AbhiPrasad commented 1 year ago

Are you putting the captureException call in a react component? Or outside of it. What might be happening is that the side effect of the module is being treeshaken away. If you put a console.log next to the captureException, does it work?

The best way to debug this is to provide a reproduction, otherwise there's not much more we can do to debug further! Thanks!

ci-vamp commented 1 year ago

i created a callback that wraps Sentry.captureException. i used this in react-error-boundary as on the onError callback.

we use this exact setup in our react-native (mobile) project. now trying to integrate sentry into the react (web) project and running into this issue.

it is hitting the captureException and i am able to step through it w the debugger. but what is happening is it is somehow losing its reference to the global Sentry object (or at least its configuration). thats how you can see that the singleton and client objects are not populated with DSN etc.

ci-vamp commented 1 year ago

i will try to create a reproduction but the only way i can think of is to cut out all of the code in our codebase but leave the config (webpack etc) the same so its reproducible. cant share all the project code as it is because its closed source

ci-vamp commented 1 year ago

@AbhiPrasad Figured it out!

Turns out that we load a ZenDesk widget which has its own sentry instance. The ZD widget loads in as a global on window and it overwrites our sentry instance / config. This is why everything after the root index was losing its config properties.

I can't find anything online about others having this issue. Do you have any guidance?

ci-vamp commented 1 year ago

image

image

The "after return" was after our loading spinner (waiting for user auth to load) is complete. It turns out that the zendesk widget only loads after the user is authed as well!

So at least the problem is defined. But I’m not sure how to proceed. We can't control the ZD widget and their sentry config but of course we need our own.

HazAT commented 1 year ago

@ci-vamp which ZD widget are you using?

ci-vamp commented 1 year ago

@HazAT i believe it is the "web widget (classic)" but i can confirm when my teammate gets online

ci-vamp commented 1 year ago

@HazAT yes i can confirm it is the "classic" web widget, here is the documentation

HazAT commented 1 year ago

Just to double-check and verify - you run a normal webpage and use the ZD widget in there.

ci-vamp commented 1 year ago

yes we have a react app. in the react app we load in the zendesk widget using their code snippet which we lazy load into the document body. all of this logic (including configuration / auth of the widget) is done through a hook.

from their docs we take the snippet from our admin page and that is what we lazy load in

image

here is the lazy loading logic in the hook

image
ci-vamp commented 1 year ago

lazyLoadScript just injects it into the doc body

image
ci-vamp commented 1 year ago

this is the sentry lib that the ZD widget is loading (note: minified)

https://static.zdassets.com/ekr/sentry-browser.min.js

AbhiPrasad commented 1 year ago

@ci-vamp we have some contacts at Zendesk, we're going to reach out to them!

ci-vamp commented 1 year ago

thank you. i am unable to find anyone else having this issue. for the time being i have tried configuring our own client / hub and using that to invoke the captureException

sentryConfig.ts

import * as React from 'react';
import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from 'react-router-dom';
import {
    Hub,
    BrowserClient,
    BrowserTracing,
    defaultStackParser,
    defaultIntegrations,
    makeFetchTransport,
    reactRouterV6Instrumentation
  } from '@sentry/react';

const sentryClient = new BrowserClient({
    debug: true,
    transport: makeFetchTransport,
    dsn: 'OUR_DSN',
    integrations: [
        ...defaultIntegrations,
        new BrowserTracing({
          routingInstrumentation: reactRouterV6Instrumentation(
            React.useEffect,
            useLocation,
            useNavigationType,
            createRoutesFromChildren,
            matchRoutes
          )
        })
      ],
      stackParser: defaultStackParser,
      tracesSampleRate: 1.0,
});

const sentryHub = new Hub(sentryClient);

export default sentryHub;

then in the utility (it accepts additional user / context data which we add to the scope)

// sentryClient is the export from sentryConfig.ts
sentryClient.withScope(scope => {
  const scopedUser = scope.getUser();
  const user = scopedUser ? { ...scopedUser, ...sentryContext.user } : sentryContext.user;

  scope.setUser(user || null);

  sentryClient.captureException(error);
})

this seems to work. however when an error is thrown to the error boundary (triggering the utility above as a callback) we get this new error:

ERROR
client.recordDroppedEvent is not a function
TypeError: client.recordDroppedEvent is not a function
    at IdleTransaction.finish (https://localhost:3000/static/js/bundle.js:155423:18)
    at IdleTransaction.finish (https://localhost:3000/static/js/bundle.js:154587:315)
    at https://localhost:3000/static/js/bundle.js:154682:18
    at sentryWrapped (https://localhost:3000/static/js/bundle.js:148904:17)

even with debug: true set on the sentry client no debug logs are appearing to describe why this is happening. the error from the error boundary never makes it to sentry. but directly calling captureException (from the sentryClient / hub) does work.

AbhiPrasad commented 1 year ago

The error boundary uses the global hub/client hence the issue: https://github.com/getsentry/sentry-javascript/blob/9ea2bca66a0ca45a83ea5e2369067745cc407e5d/packages/react/src/errorboundary.tsx#L138

We should maybe allow for you to pass a reference to hub in the error boundary.

ci-vamp commented 1 year ago

to be clear i am not using the sentry error boundary. i am using this lib

its usage looks like this

  <ErrorBoundary
    onReset={reset}
    onError={handleErrorBoundaryError}
    FallbackComponent={ErrorFallback}
  >

the ErrorFallback is just a component to render. the handleErrorBoundaryError is the callback that shapes / adds some context then calls Sentry.captureException (the utility i showed above).

even taking zendesk widget out completely i am running into an issue where it states "the error has already been captured" by the time it reaches the callback with Sentry.captureException.

i want to be in control of what gets sent to sentry so i can do some additional shaping. this works fine on mobile and we thought porting over the logic to web would be trivial but it has become quite a time sink.

ci-vamp commented 1 year ago

it looks like as soon as it touches the error boundary sentry ships it off. then when my callback is invoked it says the error has already been captured.

i tried adding new Sentry.GlobalHandlers({onerror: false, onunhandledrejection: false }), to the integrations thinking it may disable global capture but it had no effect

AbhiPrasad commented 1 year ago

@ci-vamp in development mode React maybe capturing the error globally. Could you try running in production mode?

HazAT commented 1 year ago

@ci-vamp can you try it again - it should work now (with Zendesk enabled)