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.