bugsnag / bugsnag-python

Official BugSnag error monitoring and error reporting for django, flask, tornado and other python apps.
https://docs.bugsnag.com/platforms/python/
MIT License
84 stars 42 forks source link

Bugsnag clients do not inherit django setting configuration from default client #331

Closed jrjurman-jellyfish closed 1 year ago

jrjurman-jellyfish commented 1 year ago

Describe the bug

I'm trying to setup decorators so that we can wrap different functions to route to different bugsnag projects. It seems (based on the documentation) the only way to do this is with created bugsnag clients (not the default client). However, when looking at the new inbox, we're not seeing the configuration that we expect (the release environment is wrong, we don't have a REQUEST tab, etc).

I believe these are all things that are wired by using the .configure() method on the from bugsnag import django object, but there's no similar method for clients.

Steps to reproduce

Environment

Example code snippet

In our setup, we have the following: manage.py has

if __name__ == "__main__":
    os.environ.setdefault(
        "IPYTHONDIR", os.path.join(os.path.dirname(os.path.realpath(__file__)), '.ipython')
    )
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jellyfish.settings.dev")

    import bugsnag
    from django.core.management import execute_from_command_line
    from bugsnag import django as bugsnag_django

    bugsnag_django.configure()

In a bugsnag_clients.py we have

import bugsnag
from django.conf import settings

nessie_bugsnag = bugsnag.Client(api_key=settings.BUGSNAG_NESSIE_API_KEY)

In a our code file we have

from core.bugsnag_clients import nessie_bugsnag

@nessie_bugsnag.capture
def get_meeting_breakdown_percentages():
    # logic stuff
    # ...
    nessie_bugsnag.notify('One of the meeting breakdown categories is >= 99%. This warrants an investigation to see if meeting data is being correctly categorized. Route to Nessie.')

Below is a picture of the different results, the left side using the default bugsnag client, the right using the code above image

imjoehaines commented 1 year ago

Hey @jrjurman-jellyfish, there are a couple of ways you can accomplish this

You can use the capture decorator with the default client, but have to import it from bugsnag.legacy first:

from bugsnag.legacy import default_client

@default_client.capture(api_key='YOUR_API_KEY')
def a_function():
    pass

@default_client.capture(api_key='YOUR_OTHER_API_KEY')
def another_function():
    pass

Another way to do this is to use the configuration object returned by bugsnag_django.configure() when creating new clients:

import bugsnag
from bugsnag import django as bugsnag_django

configuration = bugsnag_django.configure()

# make sure only one client installs the exception hook!
# see the note here: http://docs.bugsnag.com/platforms/python/other/reporting-handled-errors/#creating-clients
client1 = bugsnag.Client(configuration, install_sys_hook=True)
client2 = bugsnag.Client(configuration, install_sys_hook=False)

@client1.capture(api_key='YOUR_API_KEY')
def a_function():
    pass

@client2.capture(api_key='YOUR_OTHER_API_KEY')
def another_function():
    pass

This will make both clients share the same configuration object so any changes to one will affect the other, which may not be desirable. For example, you'll have to pass the api_key to capture every time it's called as otherwise they'll overwrite each other. You could clone the configuration object to get a copy for each client instead, if that's easier

Hope that helps! I'll also raise a task to look at if we can improve the docs around this

jrjurman-jellyfish commented 1 year ago

@imjoehaines thanks for these details, this looks exactly like what we need! I'll try one of these options this week and respond back on what works (or if I'm still running into issues). Documentation around this stuff would be awesome, but I'm sure this answer alone (in github issues) will do a lot of good for anyone else struggling!

luke-belton commented 1 year ago

Hi @jrjurman-jellyfish - I'm going to close this issue for now as I believe Joe has answered your question, but let us know if this doesn't work and we can reopen. Alternatively please feel free to write into support@bugsnag.com!

jrjurman-jellyfish commented 1 year ago

@imjoehaines @luke-belton

I tried the first method, using @default_client.capture(api_key='YOUR_API_KEY'), and I'm seeing the snag go to our dedicated project (jellyfish-nessie), but I'm also seeing it show up for the original project that the default client is configured with (jellyfish-webapp):

image

Is this expected, and is there a way to configure the decorator such that we don't snag to both of these places?

imjoehaines commented 1 year ago

What's happening there is that both the capture decorator and our Django middleware are reporting the same exception

A simple way to fix it is to add a callback that sets skip_bugsnag on the exception (this was added in v4.3.0 so make sure your bugsnag-python version is up to date!):

def skip_bugsnag(event):
    event.original_error.skip_bugsnag = True

bugsnag.before_notify(skip_bugsnag)

This will prevent any event created from the same exception object from being sent more than once, so would stop the Django middleware from reporting the same error

Taking a step back though, I wonder if capture is actually the wrong tool for the job. If you only want to route events to different projects based on where they were raised, it probably makes more sense to use a callback to do this because then you can route both unhandled and handled exceptions. Using capture would only affect unhandled exceptions, so any manual calls to notify would still go to the default project unless you also passed the api_key every time

A callback to do this would look something like this:

def before_notify(event):
    # if this event originates in 'get_meeting_breakdown_percentages', route it
    # to a different project
    if event.errors[0].stacktrace[-1]['method'] == 'get_meeting_breakdown_percentages':
        event.api_key = '...'

bugsnag.before_notify(before_notify)