Question

I'm using Sentry (Raven 3.4.1) on a user-facing Python/Pyramid webapp. Sentry seems to have the ability to track which and how many users experienced a certain exception. (See for example the Sentry 6.2.0 changelog, which mentions: "Streams which have recorded user data will now show the number of unique users an event has happened to.") How do I supply this information to Raven, so that it shows up in Sentry?

Can I only do this if I pass the exception to Raven manually? Right now, I'm using a SentryHandler logging handler, attached to the root logger, and an egg:raven#raven filter in the PasteDeploy pipeline. (Following the official Raven configuration docs for Pyramid closely.)

Is there a good trick to generally pass this information to Raven? Can I maybe set a local variable with a certain name somewhere at the bottom of my stack, as soon as I have loaded the user's session, and Raven will pick it up automatically? What's the best practice here?

I suspect this has to do with what I'm trying to do, but I can't find anything about it in the Raven docs.

Was it helpful?

Solution

The only way I found to do this was to overwrite some internal methods of Raven.

The basic idea is that we want to modify the handle_exception() method on the Sentry class of Raven. There, I can inject the sentry.interfaces.User interface I already mentioned in the question. To do this, I need my own filter factory, which I can then use instead of the default Paste filter that comes with Raven and which uses my own subclass of Sentry.

In my project, I have a file sentry.py:

from raven.middleware import Sentry
from raven.utils.wsgi import get_current_url, get_headers, get_environ
from raven.base import Client

def sentry_filter_factory(app, global_conf, **kwargs):
    """ Overwritten just to override the 'Sentry' class being used. """ 
    client = Client(**kwargs)
    return UserEnhancedSentry(app, client)

class UserEnhancedSentry(Sentry):
    """ Overriding raven.middleware.Sentry's handle_exception() method to inject
        the sentry.interfaces.User interface. This relies on the data previously
        being injected into the environ by our custom tween. """
    def handle_exception(self, environ):
        data={}
        data['sentry.interfaces.Http'] = {
            'method': environ.get('REQUEST_METHOD'),
            'url': get_current_url(environ, strip_querystring=True),
            'query_string': environ.get('QUERY_STRING'),
            # TODO
            # 'data': environ.get('wsgi.input'),
            'headers': dict(get_headers(environ)),
            'env': dict(get_environ(environ)),
        }
        if environ.has_key('myapp.user'):
            user_id, username, email = environ.get('myapp.user')
            ip_address = environ.get('HTTP_X_FORWARDED_FOR', environ.get('REMOTE_ADDR'))
            data['sentry.interfaces.User'] = {
                'id': user_id,
                'username': username,
                'email': email,
                'ip_address': ip_address,
            }
        event_id = self.client.captureException(data=data)
        return event_id

In my setup.py, I declare the filter factory as an entry point:

entry_points = """\
[paste.app_factory]
main = myapp:main
[paste.filter_app_factory]
raven = myapp.sentry:sentry_filter_factory
""",

Now I can use the entry point to declare a filter in my configuration .ini file and integrate it into the Paste pipeline, just as described in the Paste docs:

[pipeline:main]
pipeline =
    sentry
    myapp

[filter:sentry]
use = egg:myapp#raven
dsn = https://abc:def@app.getsentry.com/123

So now, whenever the Sentry filter catches an exception, it will not only store data about the HTTP request, but also about the user—assuming it can find information about that in the environ. All I have to do now is inject the user information from the session there, which I'm doing with a custom tween:

def sentry_user_tween_factory(handler, registry):
    """ Returns a tween that does nothing but enhance the environ by information about the currently
        logged in user, which will be useful for the Sentry report in case there's an exception """ 
    def sentry_user_tween(request):
        user = request.session.get('user')
        if user:
            request.environ['myapp.user'] = (user.token, user.name, user.email)
        else:
            request.environ['myapp.user'] = (0, 'anonymous', None)
        # proceed with the next tween/handler
        response = handler(request)
        return response
    return sentry_user_tween

config.add_tween('myapp.sentry_user_tween_factory')

That all seems unreasonably complicated, but I'm happy that I found a way to make it work, at last.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top