New context() API
This makes the API much easier to use, and resembles the raven-ruby implementation. Users can continually merge context (generally within their middleware), and simply need to call context.clear() to refresh it. The following APIs are added as part of this changeset: ``` Client.context -- the accessible context instance >>> Client.context.get() Client.context.clear() -- flush the current context >>> Client.context.clear() Client.user_context() -- a shortcut to set the user interface >>> Client.user_context({'email': 'foo@example.com'}) Client.tags_context() -- add additional tags to context (or overwrite an existing tag) >>> Client.tags_context({'key': 'value'}) Client.extra_context() -- add additional data to context (or overwrite an existing key in the data) >>> Client.extra_context({'key': 'value'}) ```
This commit is contained in:
parent
0ace37b210
commit
6f03dee429
105
raven/base.py
105
raven/base.py
|
@ -10,7 +10,6 @@ from __future__ import absolute_import
|
|||
|
||||
import base64
|
||||
import zlib
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
@ -18,16 +17,17 @@ import time
|
|||
import uuid
|
||||
import warnings
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import raven
|
||||
from raven.conf import defaults
|
||||
from raven.context import Context
|
||||
from raven.utils import json, get_versions, get_auth_header
|
||||
from raven.utils import six, json, get_versions, get_auth_header, merge_dicts
|
||||
from raven.utils.encoding import to_unicode
|
||||
from raven.utils.serializer import transform
|
||||
from raven.utils.stacks import get_stack_info, iter_stack_frames, get_culprit
|
||||
from raven.utils.urlparse import urlparse
|
||||
from raven.utils.compat import HTTPError
|
||||
from raven.utils import six
|
||||
from raven.transport.registry import TransportRegistry, default_transports
|
||||
|
||||
__all__ = ('Client',)
|
||||
|
@ -42,8 +42,8 @@ class ModuleProxyCache(dict):
|
|||
def __missing__(self, key):
|
||||
module, class_name = key.rsplit('.', 1)
|
||||
|
||||
handler = getattr(__import__(module, {},
|
||||
{}, [class_name]), class_name)
|
||||
handler = getattr(__import__(
|
||||
module, {}, {}, [class_name]), class_name)
|
||||
|
||||
self[key] = handler
|
||||
|
||||
|
@ -195,6 +195,8 @@ class Client(object):
|
|||
if Raven is None:
|
||||
Raven = self
|
||||
|
||||
self._context = Context()
|
||||
|
||||
@classmethod
|
||||
def register_scheme(cls, scheme, transport_class):
|
||||
cls._registry.register_scheme(scheme, transport_class)
|
||||
|
@ -266,12 +268,12 @@ class Client(object):
|
|||
# create ID client-side so that it can be passed to application
|
||||
event_id = uuid.uuid4().hex
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
if extra is None:
|
||||
extra = {}
|
||||
if not date:
|
||||
date = datetime.datetime.utcnow()
|
||||
data = merge_dicts(self.context.data, data)
|
||||
|
||||
data.setdefault('tags', {})
|
||||
data.setdefault('extra', {})
|
||||
data.setdefault('level', logging.ERROR)
|
||||
|
||||
if stack is None:
|
||||
stack = self.auto_log_stacks
|
||||
|
||||
|
@ -336,22 +338,13 @@ class Client(object):
|
|||
if not data.get('modules'):
|
||||
data['modules'] = self.get_module_versions()
|
||||
|
||||
data['tags'] = tags or {}
|
||||
data.setdefault('extra', {})
|
||||
data.setdefault('level', logging.ERROR)
|
||||
data['tags'] = merge_dicts(self.tags, data['tags'], tags)
|
||||
data['extra'] = merge_dicts(self.extra, data['extra'], extra)
|
||||
|
||||
# Add default extra context
|
||||
if self.extra:
|
||||
for k, v in six.iteritems(self.extra):
|
||||
data['extra'].setdefault(k, v)
|
||||
|
||||
# Add default tag context
|
||||
if self.tags:
|
||||
for k, v in six.iteritems(self.tags):
|
||||
data['tags'].setdefault(k, v)
|
||||
|
||||
for k, v in six.iteritems(extra):
|
||||
data['extra'][k] = v
|
||||
# Legacy support for site attribute
|
||||
site = data.pop('site', None) or self.site
|
||||
if site:
|
||||
data['tags'].setdefault('site', site)
|
||||
|
||||
if culprit:
|
||||
data['culprit'] = culprit
|
||||
|
@ -363,27 +356,20 @@ class Client(object):
|
|||
if 'message' not in data:
|
||||
data['message'] = handler.to_string(data)
|
||||
|
||||
data.setdefault('project', self.project)
|
||||
|
||||
# Legacy support for site attribute
|
||||
site = data.pop('site', None) or self.site
|
||||
if site:
|
||||
data['tags'].setdefault('site', site)
|
||||
|
||||
# tags should only be key=>u'value'
|
||||
for key, value in six.iteritems(data['tags']):
|
||||
data['tags'][key] = to_unicode(value)
|
||||
|
||||
# Make sure custom data is coerced
|
||||
# extra data can be any arbitrary value
|
||||
for k, v in six.iteritems(data['extra']):
|
||||
data['extra'][k] = self.transform(v)
|
||||
|
||||
# It's important date is added **after** we serialize
|
||||
data.update({
|
||||
'timestamp': date,
|
||||
'time_spent': time_spent,
|
||||
'event_id': event_id,
|
||||
'platform': PLATFORM_NAME,
|
||||
})
|
||||
data.setdefault('project', self.project)
|
||||
data.setdefault('timestamp', date or datetime.utcnow())
|
||||
data.setdefault('time_spent', time_spent)
|
||||
data.setdefault('event_id', event_id)
|
||||
data.setdefault('platform', PLATFORM_NAME)
|
||||
|
||||
return data
|
||||
|
||||
|
@ -392,18 +378,39 @@ class Client(object):
|
|||
data, list_max_length=self.list_max_length,
|
||||
string_max_length=self.string_max_length)
|
||||
|
||||
def context(self, **kwargs):
|
||||
@property
|
||||
def context(self):
|
||||
"""
|
||||
Create default context around a block of code for exception management.
|
||||
Updates this clients thread-local context for future events.
|
||||
|
||||
>>> with client.context(tags={'key': 'value'}) as raven:
|
||||
>>> # use the context manager's client reference
|
||||
>>> raven.captureMessage('hello!')
|
||||
>>>
|
||||
>>> # uncaught exceptions also contain the context
|
||||
>>> 1 / 0
|
||||
>>> def view_handler(view_func, *args, **kwargs):
|
||||
>>> client.context.merge(tags={'key': 'value'})
|
||||
>>> try:
|
||||
>>> return view_func(*args, **kwargs)
|
||||
>>> finally:
|
||||
>>> client.context.clear()
|
||||
"""
|
||||
return Context(self, **kwargs)
|
||||
return self._context
|
||||
|
||||
def user_context(self, data):
|
||||
return self.context.merge({
|
||||
'sentry.interfaces.User': data,
|
||||
})
|
||||
|
||||
def http_context(self, data, **kwargs):
|
||||
return self.context.merge({
|
||||
'sentry.interfaces.Http': data,
|
||||
})
|
||||
|
||||
def extra_context(self, data, **kwargs):
|
||||
return self.context.merge({
|
||||
'extra': data,
|
||||
})
|
||||
|
||||
def tags_context(self, data, **kwargs):
|
||||
return self.context.merge({
|
||||
'tags': data,
|
||||
})
|
||||
|
||||
def capture(self, event_type, data=None, date=None, time_spent=None,
|
||||
extra=None, stack=None, tags=None, **kwargs):
|
||||
|
|
|
@ -7,41 +7,51 @@ raven.context
|
|||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from collections import Mapping, Iterable
|
||||
from threading import local
|
||||
|
||||
from raven.utils import six
|
||||
|
||||
|
||||
class Context(object):
|
||||
class Context(local, Mapping, Iterable):
|
||||
"""
|
||||
Create default context around a block of code for exception management.
|
||||
Stores context until cleared.
|
||||
|
||||
>>> with Context(client, tags={'key': 'value'}) as raven:
|
||||
>>> # use the context manager's client reference
|
||||
>>> raven.captureMessage('hello!')
|
||||
>>>
|
||||
>>> # uncaught exceptions also contain the context
|
||||
>>> 1 / 0
|
||||
>>> def view_handler(view_func, *args, **kwargs):
|
||||
>>> context = Context()
|
||||
>>> context.merge(tags={'key': 'value'})
|
||||
>>> try:
|
||||
>>> return view_func(*args, **kwargs)
|
||||
>>> finally:
|
||||
>>> context.clear()
|
||||
"""
|
||||
def __init__(self, client, **defaults):
|
||||
self.client = client
|
||||
self.defaults = defaults
|
||||
self.result = None
|
||||
def __init__(self):
|
||||
self.data = {}
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __getitem__(self, key):
|
||||
return self.data[key]
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
if all(exc_info):
|
||||
self.result = self.captureException(exc_info)
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
|
||||
def __call(self, function, *args, **kwargs):
|
||||
for key, value in six.iteritems(self.defaults):
|
||||
if key not in kwargs:
|
||||
kwargs[key] = value
|
||||
def __repr__(self):
|
||||
return '<%s: %s>' % (type(self).__name__, self.data)
|
||||
|
||||
return function(*args, **kwargs)
|
||||
def merge(self, data):
|
||||
d = self.data
|
||||
for key, value in six.iteritems(data):
|
||||
if key in ('tags', 'extra'):
|
||||
d.setdefault(key, {})
|
||||
for t_key, t_value in six.iteritems(value):
|
||||
d[key][t_key] = t_value
|
||||
else:
|
||||
d[key] = value
|
||||
|
||||
def captureException(self, *args, **kwargs):
|
||||
return self.__call(self.client.captureException, *args, **kwargs)
|
||||
def set(self, data):
|
||||
self.data = data
|
||||
|
||||
def captureMessage(self, *args, **kwargs):
|
||||
return self.__call(self.client.captureMessage, *args, **kwargs)
|
||||
def get(self):
|
||||
return self.data
|
||||
|
||||
def clear(self):
|
||||
self.data = {}
|
||||
|
|
|
@ -48,6 +48,9 @@ class Sentry(object):
|
|||
except Exception:
|
||||
self.handle_exception(environ)
|
||||
|
||||
def process_response(self, request, response):
|
||||
self.client.context.clear()
|
||||
|
||||
def handle_exception(self, environ):
|
||||
event_id = self.client.captureException(
|
||||
data={
|
||||
|
|
|
@ -18,6 +18,17 @@ import sys
|
|||
logger = logging.getLogger('raven.errors')
|
||||
|
||||
|
||||
def merge_dicts(*dicts):
|
||||
out = {}
|
||||
for d in dicts:
|
||||
if not d:
|
||||
continue
|
||||
|
||||
for k, v in six.iteritems(d):
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def varmap(func, var, context=None, name=None):
|
||||
"""
|
||||
Executes ``func(key_name, value)`` on all values
|
||||
|
|
|
@ -270,35 +270,20 @@ class ClientTest(TestCase):
|
|||
self.assertFalse('sentry.interfaces.Stacktrace' in event)
|
||||
self.assertTrue('timestamp' in event)
|
||||
|
||||
def test_exception_context_manager(self):
|
||||
cm = self.client.context(tags={'foo': 'bar'})
|
||||
def test_context(self):
|
||||
self.client.context.merge({
|
||||
'tags': {'foo': 'bar'},
|
||||
})
|
||||
try:
|
||||
with cm:
|
||||
raise ValueError('foo')
|
||||
raise ValueError('foo')
|
||||
except:
|
||||
pass
|
||||
self.client.captureException()
|
||||
else:
|
||||
self.fail('Exception should have been raised')
|
||||
|
||||
self.assertNotEquals(cm.result, None)
|
||||
|
||||
self.assertEquals(len(self.client.events), 1)
|
||||
assert len(self.client.events) == 1
|
||||
event = self.client.events.pop(0)
|
||||
self.assertEquals(event['message'], 'ValueError: foo')
|
||||
self.assertTrue('sentry.interfaces.Exception' in event)
|
||||
exc = event['sentry.interfaces.Exception']
|
||||
self.assertEquals(exc['type'], 'ValueError')
|
||||
self.assertEquals(exc['value'], 'foo')
|
||||
self.assertEquals(exc['module'], ValueError.__module__) # this differs in some Python versions
|
||||
self.assertTrue('sentry.interfaces.Stacktrace' in event)
|
||||
frames = event['sentry.interfaces.Stacktrace']
|
||||
self.assertEquals(len(frames['frames']), 1)
|
||||
frame = frames['frames'][0]
|
||||
self.assertEquals(frame['abs_path'], __file__.replace('.pyc', '.py'))
|
||||
self.assertEquals(frame['filename'], 'tests/base/tests.py')
|
||||
self.assertEquals(frame['module'], __name__)
|
||||
self.assertEquals(frame['function'], 'test_exception_context_manager')
|
||||
self.assertTrue('timestamp' in event)
|
||||
assert event['tags'] == {'foo': 'bar'}
|
||||
|
||||
def test_stack_explicit_frames(self):
|
||||
def bar():
|
||||
|
|
|
@ -1,46 +1,37 @@
|
|||
import mock
|
||||
import sys
|
||||
from exam import fixture
|
||||
from raven.utils.testutils import TestCase
|
||||
|
||||
from raven.context import Context
|
||||
|
||||
|
||||
class ContextTest(TestCase):
|
||||
@fixture
|
||||
def client(self):
|
||||
return mock.Mock()
|
||||
|
||||
def context(self, **kwargs):
|
||||
return Context(self.client, **kwargs)
|
||||
|
||||
def test_capture_exception(self):
|
||||
with self.context(tags={'foo': 'bar'}) as client:
|
||||
result = client.captureException('exception')
|
||||
self.assertEquals(result, self.client.captureException.return_value)
|
||||
self.client.captureException.assert_called_once_with('exception', tags={
|
||||
'foo': 'bar',
|
||||
})
|
||||
|
||||
def test_capture_message(self):
|
||||
with self.context(tags={'foo': 'bar'}) as client:
|
||||
result = client.captureMessage('exception')
|
||||
self.assertEquals(result, self.client.captureMessage.return_value)
|
||||
self.client.captureMessage.assert_called_once_with('exception', tags={
|
||||
'foo': 'bar',
|
||||
})
|
||||
|
||||
def test_implicit_exception_handling(self):
|
||||
try:
|
||||
with self.context(tags={'foo': 'bar'}):
|
||||
try:
|
||||
1 / 0
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.client.captureException.assert_called_once_with(exc_info, tags={
|
||||
def test_simple(self):
|
||||
context = Context()
|
||||
context.merge({'foo': 'bar'})
|
||||
context.merge({'biz': 'baz'})
|
||||
context.merge({'biz': 'boz'})
|
||||
assert context.get() == {
|
||||
'foo': 'bar',
|
||||
})
|
||||
'biz': 'boz',
|
||||
}
|
||||
|
||||
def test_tags(self):
|
||||
context = Context()
|
||||
context.merge({'tags': {'foo': 'bar'}})
|
||||
context.merge({'tags': {'biz': 'baz'}})
|
||||
assert context.get() == {
|
||||
'tags': {
|
||||
'foo': 'bar',
|
||||
'biz': 'baz',
|
||||
}
|
||||
}
|
||||
|
||||
def test_extra(self):
|
||||
context = Context()
|
||||
context.merge({'extra': {'foo': 'bar'}})
|
||||
context.merge({'extra': {'biz': 'baz'}})
|
||||
assert context.get() == {
|
||||
'extra': {
|
||||
'foo': 'bar',
|
||||
'biz': 'baz',
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue