diff options
| author | Matt Robenolt <matt@ydekproductions.com> | 2014-03-08 10:44:35 -0800 |
|---|---|---|
| committer | Matt Robenolt <matt@ydekproductions.com> | 2014-03-08 10:44:35 -0800 |
| commit | 843f94708e430b9771ab3ee879aad24709edfcce (patch) | |
| tree | 23921bc2995d1781c49ec5c3dccb4b6cec1232b3 | |
| parent | 0cd35e5ba1246bb401674be6d8d5d6d37ab35ce9 (diff) | |
| download | raven-protocol6.tar.gz | |
Initial pass at v6 of the protocolprotocol6
29 files changed, 639 insertions, 386 deletions
diff --git a/raven/base.py b/raven/base.py index b06e8fd..0331fd6 100644 --- a/raven/base.py +++ b/raven/base.py @@ -14,18 +14,20 @@ import logging import os import sys import time -import uuid import warnings +from uuid import uuid4 from datetime import datetime +from threading import local import raven from raven.conf import defaults -from raven.context import Context +from raven.context import Context, Timeline +from raven.events import get_handler 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.stacks import get_culprit from raven.utils.urlparse import urlparse from raven.utils.compat import HTTPError from raven.transport.registry import TransportRegistry, default_transports @@ -33,6 +35,7 @@ from raven.transport.registry import TransportRegistry, default_transports __all__ = ('Client',) PLATFORM_NAME = 'python' +DEFAULT_ENVIRONMENT = 'production' # singleton for the client Raven = None @@ -109,7 +112,7 @@ class Client(object): >>> print "Exception caught; reference is %s" % ident """ logger = logging.getLogger('raven') - protocol_version = '4' + protocol_version = '6' _registry = TransportRegistry(transports=default_transports) @@ -126,6 +129,7 @@ class Client(object): self.logger = logging.getLogger( '%s.%s' % (cls.__module__, cls.__name__)) self.error_logger = logging.getLogger('sentry.errors') + self._local = local() if dsn is None and os.environ.get('SENTRY_DSN'): msg = "Configuring Raven from environment variable 'SENTRY_DSN'" @@ -192,6 +196,19 @@ class Client(object): Raven = self self._context = Context() + self._timeline = Timeline() + + @property + def transaction(self): + try: + return self._local.transaction + except AttributeError: + self.start_transaction() + return self._local.transaction + + def reset(self): + self._context.clear() + self._timeline.clear() @classmethod def register_scheme(cls, scheme, transport_class): @@ -224,7 +241,10 @@ class Client(object): return '$'.join(result) def get_handler(self, name): - return self.module_cache[name](self) + try: + return get_handler(name)(self) + except KeyError: + return None def _get_public_dsn(self): url = urlparse(self.servers[0]) @@ -251,7 +271,7 @@ class Client(object): return url return '%s:%s' % (scheme, url) - def build_msg(self, event_type, data=None, date=None, + def build_msg(self, data=None, date=None, time_spent=None, extra=None, stack=None, public_key=None, tags=None, **kwargs): """ @@ -260,76 +280,41 @@ class Client(object): The result of ``build_msg`` should be a standardized dict, with all default values available. """ - - # create ID client-side so that it can be passed to application - event_id = uuid.uuid4().hex - data = merge_dicts(self.context.data, data) + data.setdefault('events', list(self._timeline)) data.setdefault('tags', {}) data.setdefault('extra', {}) - data.setdefault('level', logging.ERROR) if stack is None: stack = self.auto_log_stacks - if '.' not in event_type: - # Assume it's a builtin - event_type = 'raven.events.%s' % event_type + events = [] + for event in data['events']: + event_type = event.get('type', 'message') + handler = self.get_handler(event_type) + log_stack = event.pop('stack', stack) + if handler is None: + events.append(event) + else: + events.append(handler.handle(stack=log_stack, **event)) + + data['events'] = events - handler = self.get_handler(event_type) - result = handler.capture(**kwargs) + # Last event is considered significant + if not len(events): + raise Exception('No events to be sent') + + significant_event = events[-1] # data (explicit) culprit takes over auto event detection - culprit = result.pop('culprit', None) + culprit = significant_event.pop('culprit', None) if data.get('culprit'): culprit = data['culprit'] - for k, v in six.iteritems(result): - if k not in data: - data[k] = v - - if stack and 'sentry.interfaces.Stacktrace' not in data: - if stack is True: - frames = iter_stack_frames() - - else: - frames = stack - - data.update({ - 'sentry.interfaces.Stacktrace': { - 'frames': get_stack_info(frames, - transformer=self.transform) - }, - }) - - if 'sentry.interfaces.Stacktrace' in data: - if self.include_paths: - for frame in data['sentry.interfaces.Stacktrace']['frames']: - if frame.get('in_app') is not None: - continue - - path = frame.get('module') - if not path: - continue - - if path.startswith('raven.'): - frame['in_app'] = False - else: - frame['in_app'] = ( - any(path.startswith(x) for x in self.include_paths) - and not - any(path.startswith(x) for x in self.exclude_paths) - ) - if not culprit: - if 'sentry.interfaces.Stacktrace' in data: - culprit = get_culprit(data['sentry.interfaces.Stacktrace']['frames']) - elif data.get('sentry.interfaces.Exception', {}).get('stacktrace'): - culprit = get_culprit(data['sentry.interfaces.Exception']['stacktrace']['frames']) - - if not data.get('level'): - data['level'] = kwargs.get('level') or logging.ERROR + if significant_event['type'] == 'exception' and 'stacktrace' in significant_event: + culprit = get_culprit(significant_event['stacktrace']['frames']) if not data.get('server_name'): data['server_name'] = self.name @@ -353,7 +338,7 @@ class Client(object): data.update(processor.process(data)) if 'message' not in data: - data['message'] = handler.to_string(data) + data['message'] = handler.to_string(significant_event) # tags should only be key=>u'value' for key, value in six.iteritems(data['tags']): @@ -363,12 +348,16 @@ class Client(object): for k, v in six.iteritems(data['extra']): data['extra'][k] = self.transform(v) + # create ID client-side so that it can be passed to application + data.setdefault('id', uuid4().hex) + data.setdefault('transaction', self.transaction) + # It's important date is added **after** we serialize 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) + data.setdefault('environment', DEFAULT_ENVIRONMENT) return data @@ -391,6 +380,61 @@ class Client(object): """ return self._context + def pre_add_action(self, action, **kwargs): + """ + Hook for subclasses to do something before adding an action to + the timeline. + """ + pass + + def add_action(self, action, **kwargs): + """ + Appends an arbitrary action to the timeline. + + >>> client.add_event({'message': 'sup sentry'}) + """ + # Pop off tags and extra so they don't get carried through + kwargs.pop('tags', None) + kwargs.pop('extra', None) + + action.setdefault('type', 'message') + action.setdefault('timestamp', datetime.utcnow()) + action.update(**kwargs) + self.pre_add_action(action) + self._timeline.append(action) + + def add_http(self, data, **kwargs): + """ + Appends an http action to the timeline. + + >>> client.add_http({'url': 'http://example.com'}) + """ + data['type'] = 'http_request' + data.setdefault('method', 'GET') + kwargs['stack'] = False # Don't capture stack for http + if len(self._timeline) and self._timeline[0]['type'] == 'http_request': + self._timeline[0].update(data) + else: + self.add_action(data, **kwargs) + + def add_message(self, message, **kwargs): + """ + Appends a message event to the timeline. + + >>> client.add_message('sup sentry') + """ + if isinstance(message, six.string_types): + data = {'message': message} + data['type'] = 'message' + self.add_action(data, **kwargs) + + def add_exception(self, exc_info=None, **kwargs): + data = { + 'type': 'exception', + 'exc_info': exc_info, + } + self.add_action(data, **kwargs) + def user_context(self, data): """ Update the user context for future events. @@ -398,7 +442,7 @@ class Client(object): >>> client.user_context({'email': 'foo@example.com'}) """ return self.context.merge({ - 'sentry.interfaces.User': data, + 'user': data, }) def http_context(self, data, **kwargs): @@ -407,9 +451,7 @@ class Client(object): >>> client.http_context({'url': 'http://example.com'}) """ - return self.context.merge({ - 'sentry.interfaces.Http': data, - }) + return self.add_http(data, **kwargs) def extra_context(self, data, **kwargs): """ @@ -431,7 +473,10 @@ class Client(object): 'tags': data, }) - def capture(self, event_type, data=None, date=None, time_spent=None, + def start_transaction(self, tx_id=None): + self._local.transaction = tx_id or uuid4().hex + + def capture(self, data=None, date=None, time_spent=None, extra=None, stack=None, tags=None, **kwargs): """ Captures and processes an event and pipes it off to SentryClient.send. @@ -468,9 +513,6 @@ class Client(object): >>> } >>> } - :param event_type: the module path to the Event class. Builtins can use - shorthand class notation and exclude the full module - path. :param data: the data base, useful for specifying structured data interfaces. Any key which contains a '.' will be assumed to be a data interface. @@ -488,12 +530,14 @@ class Client(object): return data = self.build_msg( - event_type, data, date, time_spent, extra, stack, tags=tags, + data, date, time_spent, extra, stack, tags=tags, **kwargs) self.send(**data) + # timeline starts back at 0 + self._timeline.clear() - return (data.get('event_id'),) + return (data.get('id'),) def _get_log_message(self, data): # decode message so we can show the actual event @@ -610,7 +654,8 @@ class Client(object): >>> client.captureMessage('My event just happened!') """ - return self.capture('raven.events.Message', message=message, **kwargs) + self.add_message(message, **kwargs) + return self.capture(**kwargs) def captureException(self, exc_info=None, **kwargs): """ @@ -628,8 +673,8 @@ class Client(object): ``kwargs`` are passed through to ``.capture``. """ - return self.capture( - 'raven.events.Exception', exc_info=exc_info, **kwargs) + self.add_exception(exc_info=exc_info) + return self.capture(**kwargs) def captureQuery(self, query, params=(), engine=None, **kwargs): """ diff --git a/raven/context.py b/raven/context.py index 5d93939..701e93b 100644 --- a/raven/context.py +++ b/raven/context.py @@ -12,6 +12,8 @@ from threading import local from raven.utils import six +TAGS_EXTRA = set(('tags', 'extra')) + class Context(local, Mapping, Iterable): """ @@ -42,8 +44,9 @@ class Context(local, Mapping, Iterable): def merge(self, data): d = self.data + for key, value in six.iteritems(data): - if key in ('tags', 'extra'): + if key in TAGS_EXTRA: d.setdefault(key, {}) for t_key, t_value in six.iteritems(value): d[key][t_key] = t_value @@ -58,3 +61,23 @@ class Context(local, Mapping, Iterable): def clear(self): self.data = {} + + +class Timeline(local): + def __init__(self): + self.clear() + + def __iter__(self): + return iter(self.timeline) + + def __len__(self): + return len(self.timeline) + + def __getitem__(self, index): + return self.timeline[index] + + def append(self, what): + self.timeline.append(what) + + def clear(self): + self.timeline = [] diff --git a/raven/contrib/bottle/__init__.py b/raven/contrib/bottle/__init__.py index 90e56f2..db47175 100644 --- a/raven/contrib/bottle/__init__.py +++ b/raven/contrib/bottle/__init__.py @@ -48,7 +48,6 @@ class Sentry(object): def handle_exception(self, *args, **kwargs): self.client.captureException( exc_info=kwargs.get('exc_info'), - data=get_data_from_request(request), extra={ 'app': self.app, }, @@ -60,6 +59,8 @@ class Sentry(object): self.handle_exception(exc_info=exc_info) return start_response(status, headers, exc_info) + self.client.add_http(get_data_from_request(request)) + try: return self.app(environ, session_start_response) # catch ANY exception that goes through... @@ -69,22 +70,8 @@ class Sentry(object): def captureException(self, *args, **kwargs): assert self.client, 'captureException called before application configured' - data = kwargs.get('data') - if data is None: - try: - kwargs['data'] = get_data_from_request(request) - except RuntimeError: - # app is probably not configured yet - pass return self.client.captureException(*args, **kwargs) def captureMessage(self, *args, **kwargs): assert self.client, 'captureMessage called before application configured' - data = kwargs.get('data') - if data is None: - try: - kwargs['data'] = get_data_from_request(request) - except RuntimeError: - # app is probably not configured yet - pass return self.client.captureMessage(*args, **kwargs) diff --git a/raven/contrib/bottle/utils.py b/raven/contrib/bottle/utils.py index 9c75577..2beff63 100644 --- a/raven/contrib/bottle/utils.py +++ b/raven/contrib/bottle/utils.py @@ -26,14 +26,12 @@ def get_data_from_request(request): formdata = {} data = { - 'sentry.interfaces.Http': { - 'url': '%s://%s%s' % (urlparts.scheme, urlparts.netloc, urlparts.path), - 'query_string': urlparts.query, - 'method': request.method, - 'data': formdata, - 'headers': dict(get_headers(request.environ)), - 'env': dict(get_environ(request.environ)), - } + 'url': '%s://%s%s' % (urlparts.scheme, urlparts.netloc, urlparts.path), + 'query_string': urlparts.query, + 'method': request.method, + 'data': formdata, + 'headers': dict(get_headers(request.environ)), + 'env': dict(get_environ(request.environ)), } return data diff --git a/raven/contrib/django/__init__.py b/raven/contrib/django/__init__.py index 7184f94..7dec9e8 100644 --- a/raven/contrib/django/__init__.py +++ b/raven/contrib/django/__init__.py @@ -7,4 +7,8 @@ raven.contrib.django """ from __future__ import absolute_import +# Importing just to register the Django specific Exception handler +from .events import Exception # NOQA +del Exception # NOQA + from .client import DjangoClient # NOQA diff --git a/raven/contrib/django/client.py b/raven/contrib/django/client.py index 8ae98c8..1cd2b01 100644 --- a/raven/contrib/django/client.py +++ b/raven/contrib/django/client.py @@ -1,6 +1,6 @@ """ raven.contrib.django.client -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. @@ -13,20 +13,27 @@ import logging from django.conf import settings from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest -from django.template import TemplateSyntaxError -from django.template.loader import LoaderOrigin from raven.base import Client -from raven.contrib.django.utils import get_data_from_template, get_host +from raven.contrib.django.utils import get_host from raven.contrib.django.middleware import SentryLogMiddleware from raven.utils.wsgi import get_headers, get_environ +try: + from django.contrib.auth.models import AbstractBaseUser as BaseUser +except ImportError: + from django.contrib.auth.models import User as BaseUser # NOQA + __all__ = ('DjangoClient',) class DjangoClient(Client): logger = logging.getLogger('sentry.errors.client.django') + def __init__(self, *args, **kwargs): + super(DjangoClient, self).__init__(*args, **kwargs) + self._local.request_added = False + def is_enabled(self): return bool(self.servers or 'sentry' in settings.INSTALLED_APPS) @@ -49,17 +56,14 @@ class DjangoClient(Client): return user_info - def get_data_from_request(self, request): - try: - from django.contrib.auth.models import AbstractBaseUser as BaseUser - except ImportError: - from django.contrib.auth.models import User as BaseUser # NOQA + def add_request(self, request): + if hasattr(request, 'user') and isinstance(request.user, BaseUser): + self.user_context(self.get_user_info(request.user)) + return self.add_http(self.get_data_from_request(request)) + def get_data_from_request(self, request): result = {} - if hasattr(request, 'user') and isinstance(request.user, BaseUser): - result['sentry.interfaces.User'] = self.get_user_info(request.user) - try: uri = request.build_absolute_uri() except SuspiciousOperation: @@ -75,13 +79,13 @@ class DjangoClient(Client): if request.method != 'GET': try: data = request.body - except: + except Exception: try: data = request.raw_post_data except Exception: # assume we had a partial read. try: - data = request.POST or '<unavailable>' + data = dict(request.POST) or '<unavailable>' except Exception: data = '<unavailable>' else: @@ -90,15 +94,13 @@ class DjangoClient(Client): environ = request.META result.update({ - 'sentry.interfaces.Http': { - 'method': request.method, - 'url': uri, - 'query_string': request.META.get('QUERY_STRING'), - 'data': data, - 'cookies': dict(request.COOKIES), - 'headers': dict(get_headers(environ)), - 'env': dict(get_environ(environ)), - } + 'method': request.method, + 'url': uri, + 'query_string': request.META.get('QUERY_STRING'), + 'data': data, + 'cookies': dict(request.COOKIES), + 'headers': dict(get_headers(environ)), + 'env': dict(get_environ(environ)), }) return result @@ -106,19 +108,16 @@ class DjangoClient(Client): def build_msg(self, *args, **kwargs): data = super(DjangoClient, self).build_msg(*args, **kwargs) - stacks = ( - data.get('sentry.interfaces.Stacktrace'), - data.get('sentry.interfaces.Exception', {}).get('stacktrace'), - ) - - for stacktrace in filter(bool, stacks): - for frame in stacktrace['frames']: - module = frame.get('module') - if not module: - continue + for action in data['events']: + if action.get('type', 'message') == 'exception' and 'stacktrace' in action: + stacktrace = action['stacktrace'] + for frame in stacktrace['frames']: + module = frame.get('module') + if not module: + continue - if module.startswith('django.'): - frame['in_app'] = False + if module[:7] == 'django.': + frame['in_app'] = False if not self.site and 'django.contrib.sites' in settings.INSTALLED_APPS: try: @@ -132,7 +131,15 @@ class DjangoClient(Client): return data - def capture(self, event_type, request=None, **kwargs): + def pre_add_action(self, action, **kwargs): + request = action.pop('request', None) + if request is None: + request = getattr(SentryLogMiddleware.thread, 'request', None) + + if isinstance(request, HttpRequest): + self.add_request(request) + + def capture(self, request=None, **kwargs): if 'data' not in kwargs: kwargs['data'] = data = {} else: @@ -141,24 +148,9 @@ class DjangoClient(Client): if request is None: request = getattr(SentryLogMiddleware.thread, 'request', None) - is_http_request = isinstance(request, HttpRequest) - if is_http_request: - data.update(self.get_data_from_request(request)) - - if kwargs.get('exc_info'): - exc_value = kwargs['exc_info'][1] - # As of r16833 (Django) all exceptions may contain a ``django_template_source`` attribute (rather than the - # legacy ``TemplateSyntaxError.source`` check) which describes template information. - if hasattr(exc_value, 'django_template_source') or ((isinstance(exc_value, TemplateSyntaxError) and - isinstance(getattr(exc_value, 'source', None), (tuple, list)) and isinstance(exc_value.source[0], LoaderOrigin))): - source = getattr(exc_value, 'django_template_source', getattr(exc_value, 'source', None)) - if source is None: - self.logger.info('Unable to get template source from exception') - data.update(get_data_from_template(source)) + result = super(DjangoClient, self).capture(**kwargs) - result = super(DjangoClient, self).capture(event_type, **kwargs) - - if is_http_request and result: + if isinstance(request, HttpRequest) and result: # attach the sentry object to the request request.sentry = { 'project_id': data.get('project', self.project), @@ -188,3 +180,7 @@ class DjangoClient(Client): def send_integrated(self, kwargs): from sentry.models import Group return Group.objects.from_kwargs(**kwargs) + + def reset(self): + self._local.request_added = False + super(DjangoClient, self).reset() diff --git a/raven/contrib/django/events.py b/raven/contrib/django/events.py new file mode 100644 index 0000000..55cd787 --- /dev/null +++ b/raven/contrib/django/events.py @@ -0,0 +1,42 @@ +""" +raven.contrib.django.events +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2010-2014 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" +from __future__ import absolute_import + +import sys + +from django.template import TemplateSyntaxError +from django.template.loader import LoaderOrigin + +from raven.events import register, Exception as BaseException +from raven.contrib.django.utils import get_data_from_template + +__all__ = ('Exception',) + + +@register +class Exception(BaseException): + def capture(self, exc_info=None, stack=None, **kwargs): + data = super(Exception, self).capture(exc_info, stack, **kwargs) + if not exc_info or exc_info is True: + exc_info = sys.exc_info() + + exc_value = exc_info[1] + # As of r16833 (Django) all exceptions may contain a ``django_template_source`` attribute (rather than the + # legacy ``TemplateSyntaxError.source`` check) which describes template information. + if ( + hasattr(exc_value, 'django_template_source') or ( + (isinstance(exc_value, TemplateSyntaxError) and + isinstance(getattr(exc_value, 'source', None), (tuple, list)) and + isinstance(exc_value.source[0], LoaderOrigin)) + ) + ): + source = getattr(exc_value, 'django_template_source', getattr(exc_value, 'source', None)) + if source is None: + self.logger.info('Unable to get template source from exception') + data.update(get_data_from_template(source)) + return data diff --git a/raven/contrib/django/handlers.py b/raven/contrib/django/handlers.py index 504fbc1..fcbd9a7 100644 --- a/raven/contrib/django/handlers.py +++ b/raven/contrib/django/handlers.py @@ -24,6 +24,7 @@ class SentryHandler(BaseSentryHandler): client = property(_get_client) def _emit(self, record): - request = getattr(record, 'request', None) - - return super(SentryHandler, self)._emit(record, request=request) + request = getattr(record, 'request', getattr(record, 'extra', {}).get('request')) + if request is not None: + self.client.add_request(request) + return super(SentryHandler, self)._emit(record) diff --git a/raven/contrib/django/middleware/__init__.py b/raven/contrib/django/middleware/__init__.py index 9be15f1..1326e07 100644 --- a/raven/contrib/django/middleware/__init__.py +++ b/raven/contrib/django/middleware/__init__.py @@ -9,9 +9,11 @@ raven.contrib.django.middleware from __future__ import absolute_import import threading -import logging from django.conf import settings +from raven.contrib.django.models import client + +IGNORABLE_404_URLS = getattr(settings, 'IGNORABLE_404_URLS', ()) def is_ignorable_404(uri): @@ -20,22 +22,22 @@ def is_ignorable_404(uri): """ return any( pattern.search(uri) - for pattern in getattr(settings, 'IGNORABLE_404_URLS', ()) + for pattern in IGNORABLE_404_URLS ) class Sentry404CatchMiddleware(object): - def process_response(self, request, response): - from raven.contrib.django.models import client + def process_request(self, request): + client.add_request(request) + def process_response(self, request, response): if response.status_code != 404 or is_ignorable_404(request.get_full_path()) or not client.is_enabled(): return response - data = client.get_data_from_request(request) - data.update({ - 'level': logging.INFO, + # data = client.get_data_from_request(request) + data = { 'logger': 'http404', - }) + } result = client.captureMessage(message='Page Not Found: %s' % request.build_absolute_uri(), data=data) request.sentry = { 'project_id': data.get('project', client.project), @@ -64,3 +66,4 @@ class SentryLogMiddleware(object): def process_request(self, request): self.thread.request = request + client.add_request(request) diff --git a/raven/contrib/django/models.py b/raven/contrib/django/models.py index a41ee57..cfc84f5 100644 --- a/raven/contrib/django/models.py +++ b/raven/contrib/django/models.py @@ -160,8 +160,10 @@ def sentry_exception_handler(request=None, **kwargs): exc_info=sys.exc_info()) return + if request is not None: + client.add_request(request) try: - client.captureException(exc_info=sys.exc_info(), request=request) + client.captureException(exc_info=sys.exc_info()) except Exception as exc: try: logger.exception('Unable to process log entry: %s' % (exc,)) diff --git a/raven/contrib/django/utils.py b/raven/contrib/django/utils.py index 39d7c8a..a619206 100644 --- a/raven/contrib/django/utils.py +++ b/raven/contrib/django/utils.py @@ -41,7 +41,7 @@ def get_data_from_template(source): context_line = source_lines[lineno] return { - 'sentry.interfaces.Template': { + 'template': { 'filename': origin.loadname, 'abs_path': origin.name, 'pre_context': pre_context, diff --git a/raven/contrib/flask.py b/raven/contrib/flask.py index 38c89b9..18b102c 100644 --- a/raven/contrib/flask.py +++ b/raven/contrib/flask.py @@ -17,7 +17,6 @@ else: import sys import os -import logging from flask import request, current_app from flask.signals import got_request_exception @@ -68,7 +67,7 @@ class Sentry(object): Automatically configure logging:: - >>> sentry = Sentry(app, logging=True, level=logging.ERROR) + >>> sentry = Sentry(app, logging=True) Capture an exception:: @@ -90,13 +89,11 @@ class Sentry(object): - Capture information from Flask-Login (if available). """ def __init__(self, app=None, client=None, client_cls=Client, dsn=None, - logging=False, level=logging.NOTSET, wrap_wsgi=True, - register_signal=True): + logging=False, wrap_wsgi=True, register_signal=True): self.dsn = dsn self.logging = logging self.client_cls = client_cls self.client = client - self.level = level self.wrap_wsgi = wrap_wsgi self.register_signal = register_signal @@ -165,13 +162,13 @@ class Sentry(object): 'url': '%s://%s%s' % (urlparts.scheme, urlparts.netloc, urlparts.path), 'query_string': urlparts.query, 'method': request.method, - 'data': formdata, + 'data': dict(formdata), 'headers': dict(get_headers(request.environ)), 'env': dict(get_environ(request.environ)), } def before_request(self, *args, **kwargs): - self.client.http_context(self.get_http_info(request)) + self.client.add_http(self.get_http_info(request)) self.client.user_context(self.get_user_info(request)) def init_app(self, app, dsn=None): @@ -182,7 +179,7 @@ class Sentry(object): self.client = make_client(self.client_cls, app, self.dsn) if self.logging: - setup_logging(SentryHandler(self.client, level=self.level)) + setup_logging(SentryHandler(self.client)) if self.wrap_wsgi: app.wsgi_app = SentryMiddleware(app.wsgi_app, self.client) diff --git a/raven/contrib/tornado/__init__.py b/raven/contrib/tornado/__init__.py index 0ca9355..0320606 100644 --- a/raven/contrib/tornado/__init__.py +++ b/raven/contrib/tornado/__init__.py @@ -39,7 +39,7 @@ class AsyncSentryClient(Client): self.send(callback=kwargs.get('callback', None), **data) - return (data['event_id'],) + return (data['id'],) def send(self, auth_header=None, callback=None, **data): """ @@ -202,7 +202,7 @@ class SentryMixin(object): Truth calue testing """ return { - 'sentry.interfaces.User': { + 'user': { 'is_authenticated': True if self.get_current_user() else False } } diff --git a/raven/events.py b/raven/events.py index b8c8421..e291f16 100644 --- a/raven/events.py +++ b/raven/events.py @@ -11,9 +11,23 @@ import logging import sys from raven.utils.encoding import to_unicode -from raven.utils.stacks import get_stack_info, iter_traceback_frames +from raven.utils.stacks import ( + get_stack_info, iter_traceback_frames, iter_stack_frames, +) -__all__ = ('BaseEvent', 'Exception', 'Message', 'Query') + +__all__ = ('BaseEvent', 'Exception', 'Message', 'Query', 'get_handler') + +handlers = {} + + +def register(cls): + handlers[cls.__name__.lower()] = cls + return cls + + +def get_handler(name): + return handlers[name] class BaseEvent(object): @@ -24,14 +38,58 @@ class BaseEvent(object): def to_string(self, data): raise NotImplementedError + def handle(self, stack=None, **kwargs): + data = self.capture(**kwargs) + data.setdefault('timestamp', kwargs['timestamp']), + if 'data' in kwargs: + data.update(**kwargs['data']) + + if stack and 'stacktrace' not in kwargs: + if stack is True: + frames = iter_stack_frames() + else: + frames = stack + + data.update({ + 'stacktrace': { + 'frames': get_stack_info( + frames, transformer=self.transform) + }, + }) + + if 'stacktrace' in kwargs: + if self.client.include_paths: + for frame in kwargs['stacktrace']['frames']: + if frame.get('in_app') is not None: + continue + + module = frame.get('module') + abs_path = frame.get('abs_path') + + if module and module[:6] == 'raven.': + frame['in_app'] = False + elif abs_path and '/site-packages/' in abs_path: + frame['in_app'] = False + elif module: + frame['in_app'] = ( + any(module.startswith(x) for x in self.client.include_paths) + and not + any(module.startswith(x) for x in self.client.exclude_paths) + ) + + data.update({ + 'stacktrace': kwargs['stacktrace'], + }) + return data + def capture(self, **kwargs): - return { - } + raise NotImplementedError def transform(self, value): return self.client.transform(value) +@register class Exception(BaseEvent): """ Exceptions store the following metadata: @@ -43,12 +101,11 @@ class Exception(BaseEvent): """ def to_string(self, data): - exc = data['sentry.interfaces.Exception'] - if exc['value']: - return '%s: %s' % (exc['type'], exc['value']) - return exc['type'] + if data['value']: + return '%s: %s' % (data['exc_type'], data['value']) + return data['exc_type'] - def capture(self, exc_info=None, **kwargs): + def capture(self, exc_info=None, stack=None, **kwargs): if not exc_info or exc_info is True: exc_info = sys.exc_info() @@ -68,14 +125,12 @@ class Exception(BaseEvent): exc_type = getattr(exc_type, '__name__', '<unknown>') return { - 'level': kwargs.get('level', logging.ERROR), - 'sentry.interfaces.Exception': { - 'value': to_unicode(exc_value), - 'type': str(exc_type), - 'module': to_unicode(exc_module), - 'stacktrace': { - 'frames': frames - } + 'type': 'exception', + 'value': to_unicode(exc_value), + 'exc_type': str(exc_type), + 'module': to_unicode(exc_module), + 'stacktrace': { + 'frames': frames }, } finally: @@ -85,6 +140,7 @@ class Exception(BaseEvent): self.logger.exception(e) +@register class Message(BaseEvent): """ Messages store the following metadata: @@ -92,19 +148,22 @@ class Message(BaseEvent): - message: 'My message from %s about %s' - params: ('foo', 'bar') """ + def to_string(self, data): + return data['message'] + def capture(self, message, params=(), formatted=None, **kwargs): message = to_unicode(message) data = { - 'sentry.interfaces.Message': { - 'message': message, - 'params': self.transform(params), - }, + 'type': 'message', + 'message': message, + 'params': self.transform(params), } if 'message' not in data: data['message'] = formatted or message return data +@register class Query(BaseEvent): """ Messages store the following metadata: diff --git a/raven/handlers/logbook.py b/raven/handlers/logbook.py index bb79f9a..eb2513b 100644 --- a/raven/handlers/logbook.py +++ b/raven/handlers/logbook.py @@ -59,11 +59,11 @@ class SentryHandler(logbook.Handler): def _emit(self, record): data = { - 'level': logbook.get_level_name(record.level).lower(), 'logger': record.channel, } - event_type = 'raven.events.Message' + if 'tags' in record.kwargs: + data['tags'] = record.kwargs['tags'] handler_kwargs = { 'message': record.msg, @@ -71,17 +71,14 @@ class SentryHandler(logbook.Handler): 'formatted': self.format(record), } - if 'tags' in record.kwargs: - handler_kwargs['tags'] = record.kwargs['tags'] + data['message'] = handler_kwargs['formatted'] + + self.client.add_message(**handler_kwargs) # If there's no exception being processed, exc_info may be a 3-tuple of None # http://docs.python.org/library/sys.html#sys.exc_info if record.exc_info is True or (record.exc_info and all(record.exc_info)): - handler = self.client.get_handler(event_type) - data.update(handler.capture(**handler_kwargs)) - - event_type = 'raven.events.Exception' - handler_kwargs['exc_info'] = record.exc_info + self.client.add_exception(exc_info=record.exc_info) extra = { 'lineno': record.lineno, @@ -92,8 +89,7 @@ class SentryHandler(logbook.Handler): } extra.update(record.extra) - return self.client.capture(event_type, + return self.client.capture( data=data, extra=extra, - **handler_kwargs ) diff --git a/raven/handlers/logging.py b/raven/handlers/logging.py index 827e2f7..4d84f91 100644 --- a/raven/handlers/logging.py +++ b/raven/handlers/logging.py @@ -130,7 +130,6 @@ class SentryHandler(logging.Handler, object): stack = self._get_targetted_stack(stack) date = datetime.datetime.utcfromtimestamp(record.created) - event_type = 'raven.events.Message' handler_kwargs = { 'params': record.args, } @@ -146,25 +145,21 @@ class SentryHandler(logging.Handler, object): # Handle binary strings where it should be unicode... handler_kwargs['formatted'] = repr(record.message)[1:-1] + data['message'] = handler_kwargs['formatted'] + + self.client.add_message(**handler_kwargs) + # If there's no exception being processed, exc_info may be a 3-tuple of None # http://docs.python.org/library/sys.html#sys.exc_info if record.exc_info and all(record.exc_info): - # capture the standard message first so that we ensure - # the event is recorded as an exception, in addition to having our - # message interface attached - handler = self.client.get_handler(event_type) - data.update(handler.capture(**handler_kwargs)) - - event_type = 'raven.events.Exception' - handler_kwargs = {'exc_info': record.exc_info} + self.client.add_exception(exc_info=record.exc_info) # HACK: discover a culprit when we normally couldn't - elif not (data.get('sentry.interfaces.Stacktrace') or data.get('culprit')) and (record.name or record.funcName): + elif not data.get('culprit') and (record.name or record.funcName): culprit = label_from_frame({'module': record.name, 'function': record.funcName}) if culprit: data['culprit'] = culprit - data['level'] = record.levelno data['logger'] = record.name if hasattr(record, 'tags'): @@ -173,5 +168,5 @@ class SentryHandler(logging.Handler, object): kwargs.update(handler_kwargs) return self.client.capture( - event_type, stack=stack, data=data, + stack=stack, data=data, extra=extra, date=date, **kwargs) diff --git a/raven/middleware.py b/raven/middleware.py index 588bd6f..e4ea1d0 100644 --- a/raven/middleware.py +++ b/raven/middleware.py @@ -29,7 +29,8 @@ class Sentry(object): def __call__(self, environ, start_response): # TODO(dcramer): ideally this is lazy, but the context helpers must # support callbacks first - self.client.http_context(self.get_http_context(environ)) + self.client.start_transaction() + self.client.add_http(self.get_http_context(environ)) try: iterable = self.application(environ, start_response) @@ -51,7 +52,7 @@ class Sentry(object): iterable.close() except Exception: self.handle_exception(environ) - self.client.context.clear() + self.client.reset() def get_http_context(self, environ): return { diff --git a/raven/utils/json.py b/raven/utils/json.py index 57f337b..f52f940 100644 --- a/raven/utils/json.py +++ b/raven/utils/json.py @@ -26,7 +26,7 @@ except AttributeError: class BetterJSONEncoder(json.JSONEncoder): ENCODER_BY_TYPE = { uuid.UUID: lambda o: o.hex, - datetime.datetime: lambda o: o.strftime('%Y-%m-%dT%H:%M:%SZ'), + datetime.datetime: lambda o: o.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), set: list, frozenset: list, bytes: lambda o: o.decode('utf-8', errors='replace'), diff --git a/tests/base/tests.py b/tests/base/tests.py index 0b3563e..e734538 100644 --- a/tests/base/tests.py +++ b/tests/base/tests.py @@ -149,7 +149,7 @@ class ClientTest(TestCase): 'Content-Type': 'application/octet-stream', 'X-Sentry-Auth': ( 'Sentry sentry_timestamp=1328055286.51, ' - 'sentry_client=raven-python/%s, sentry_version=4, ' + 'sentry_client=raven-python/%s, sentry_version=6, ' 'sentry_key=public, ' 'sentry_secret=secret' % (raven.VERSION,)) }, @@ -237,18 +237,20 @@ class ClientTest(TestCase): def test_exception_event(self): try: raise ValueError('foo') - except: + except Exception: self.client.captureException() self.assertEquals(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') + timeline = event['events'] + self.assertEquals(len(timeline), 1) + exc = timeline[0] + self.assertEquals(exc['type'], 'exception') + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], 'foo') self.assertEquals(exc['module'], ValueError.__module__) # this differs in some Python versions - assert 'sentry.interfaces.Stacktrace' not in event + assert 'stacktrace' not in event stacktrace = exc['stacktrace'] self.assertEquals(len(stacktrace['frames']), 1) frame = stacktrace['frames'][0] @@ -293,9 +295,10 @@ class ClientTest(TestCase): self.assertEquals(len(self.client.events), 1) event = self.client.events.pop(0) self.assertEquals(event['message'], 'test') - self.assertTrue('sentry.interfaces.Stacktrace' in event) - self.assertEquals(len(frames), len(event['sentry.interfaces.Stacktrace']['frames'])) - for frame, frame_i in zip(frames, event['sentry.interfaces.Stacktrace']['frames']): + exc = event['events'][0] + self.assertTrue('stacktrace' in exc) + self.assertEquals(len(frames), len(exc['stacktrace']['frames'])) + for frame, frame_i in zip(frames, exc['stacktrace']['frames']): self.assertEquals(frame[0].f_code.co_filename, frame_i['abs_path']) self.assertEquals(frame[0].f_code.co_name, frame_i['function']) @@ -304,8 +307,11 @@ class ClientTest(TestCase): self.assertEquals(len(self.client.events), 1) event = self.client.events.pop(0) + timeline = event['events'] + self.assertEquals(len(timeline), 1) + message = timeline[0] self.assertEquals(event['message'], 'test') - self.assertTrue('sentry.interfaces.Stacktrace' in event) + self.assertTrue('stacktrace' in message) self.assertTrue('timestamp' in event) def test_site(self): diff --git a/tests/contrib/bottle/tests.py b/tests/contrib/bottle/tests.py index 92cdbc0..baefcc1 100644 --- a/tests/contrib/bottle/tests.py +++ b/tests/contrib/bottle/tests.py @@ -1,5 +1,3 @@ -import logging - from exam import fixture from webtest import TestApp @@ -37,7 +35,7 @@ def create_app(raven): def capture_exception(): try: raise ValueError('Boom') - except: + except Exception: tapp.app.captureException() return 'Hello' @@ -66,12 +64,15 @@ class BottleTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) - self.assertTrue('sentry.interfaces.Exception' in event) - - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ValueError') + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], 'hello world') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'ValueError: hello world') self.assertEquals(event['culprit'], 'tests.contrib.bottle.tests in an_error') @@ -81,10 +82,15 @@ class BottleTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') - assert event['message'] == 'ValueError: Boom' - assert 'sentry.interfaces.Http' in event - assert 'sentry.interfaces.Exception' in event + self.assertEqual(event['message'], 'ValueError: Boom') def test_captureMessage_captures_http(self): response = self.client.get('/message/?foo=bar') @@ -92,6 +98,10 @@ class BottleTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) - self.assertTrue('sentry.interfaces.Message' in event) - self.assertTrue('sentry.interfaces.Http' in event) + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + self.assertEqual(exc['type'], 'message') diff --git a/tests/contrib/django/tests.py b/tests/contrib/django/tests.py index a573b30..29fabf2 100644 --- a/tests/contrib/django/tests.py +++ b/tests/contrib/django/tests.py @@ -126,32 +126,36 @@ class DjangoClientTest(TestCase): def setUp(self): self.raven = get_client() + def tearDown(self): + self.raven.reset() + def test_basic(self): self.raven.captureMessage(message='foo') self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) - self.assertTrue('sentry.interfaces.Message' in event) - message = event['sentry.interfaces.Message'] + timeline = event['events'] + self.assertEqual(len(timeline), 1) + message = timeline[0] + self.assertEquals(message['type'], 'message') self.assertEquals(message['message'], 'foo') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'foo') self.assertEquals(type(event['timestamp']), datetime.datetime) def test_signal_integration(self): try: int('hello') - except: + except Exception: got_request_exception.send(sender=self.__class__, request=None) else: self.fail('Expected an exception.') self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ValueError') + timeline = event['events'] + self.assertEqual(len(timeline), 1) + exc = timeline[0] + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], "invalid literal for int() with base 10: 'hello'") - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], "ValueError: invalid literal for int() with base 10: 'hello'") self.assertEquals(event['culprit'], 'tests.contrib.django.tests in test_signal_integration') @@ -160,11 +164,11 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'Exception') + timeline = event['events'] + self.assertEqual(len(timeline), 2) + exc = timeline[1] + self.assertEquals(exc['exc_type'], 'Exception') self.assertEquals(exc['value'], 'view exception') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'Exception: view exception') self.assertEquals(event['culprit'], 'tests.contrib.django.views in raise_exc') @@ -178,7 +182,7 @@ class DjangoClientTest(TestCase): assert len(self.raven.events) == 1 event = self.raven.events.pop(0) - assert 'sentry.interfaces.User' not in event + assert 'user' not in event assert self.client.login(username='admin', password='admin') @@ -186,8 +190,8 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) - assert 'sentry.interfaces.User' in event - user_info = event['sentry.interfaces.User'] + assert 'user' in event + user_info = event['user'] assert user_info == { 'is_authenticated': True, 'username': user.username, @@ -225,12 +229,14 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + exc = timeline[1] + self.assertEquals(exc['type'], 'exception') - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ImportError') + self.assertEquals(exc['exc_type'], 'ImportError') self.assertEquals(exc['value'], 'request') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'ImportError: request') self.assertEquals(event['culprit'], 'tests.contrib.django.middleware in process_request') @@ -242,12 +248,14 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + exc = timeline[1] + self.assertEquals(exc['type'], 'exception') - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ImportError') + self.assertEquals(exc['exc_type'], 'ImportError') self.assertEquals(exc['value'], 'response') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'ImportError: response') self.assertEquals(event['culprit'], 'tests.contrib.django.middleware in process_response') @@ -258,39 +266,48 @@ class DjangoClientTest(TestCase): self.assertRaises(Exception, client.get, reverse('sentry-raise-exc')) - assert len(self.raven.events) == 2 + self.assertEqual(len(self.raven.events), 2) event = self.raven.events.pop(0) - - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'Exception') + timeline = event['events'] + self.assertEqual(len(timeline), 2) + tx_id = event['transaction'] + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + self.assertEquals(exc['type'], 'exception') + self.assertEquals(exc['exc_type'], 'Exception') self.assertEquals(exc['value'], 'view exception') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'Exception: view exception') self.assertEquals(event['culprit'], 'tests.contrib.django.views in raise_exc') event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + + exc = timeline[0] + self.assertEqual(exc['type'], 'exception') - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ValueError') + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], 'handler500') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'ValueError: handler500') self.assertEquals(event['culprit'], 'tests.contrib.django.urls in handler500') + # Make sure that both messages got the same tx_id + self.assertEqual(tx_id, event['transaction']) + def test_view_middleware_exception(self): with Settings(MIDDLEWARE_CLASSES=['tests.contrib.django.middleware.BrokenViewMiddleware']): self.assertRaises(ImportError, self.client.get, reverse('sentry-raise-exc')) self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + exc = timeline[1] - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ImportError') + self.assertEquals(exc['exc_type'], 'ImportError') self.assertEquals(exc['value'], 'view') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'ImportError: view') self.assertEquals(event['culprit'], 'tests.contrib.django.middleware in process_view') @@ -364,12 +381,17 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + message = timeline[1] + self.assertEqual(message['type'], 'message') - self.assertEquals(event['level'], logging.INFO) + self.assertEquals(message['logger'], 'http404') self.assertEquals(event['logger'], 'http404') - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] self.assertEquals(http['url'], 'http://testserver/non-existant-page') self.assertEquals(http['method'], 'GET') self.assertEquals(http['query_string'], '') @@ -395,7 +417,7 @@ class DjangoClientTest(TestCase): self.assertTrue('X-Sentry-ID' in headers) self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) - assert event['event_id'] == headers['X-Sentry-ID'] + self.assertEqual(event['id'], headers['X-Sentry-ID']) def test_get_client(self): self.assertEquals(get_client(), get_client()) @@ -423,9 +445,14 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + message = timeline[1] + self.assertEquals(message['type'], 'message') - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] self.assertEquals(http['method'], 'POST') self.assertEquals(http['data'], '<unavailable>') @@ -441,11 +468,16 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + message = timeline[1] + self.assertEquals(message['type'], 'message') - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] self.assertEquals(http['method'], 'POST') - self.assertEquals(http['data'], {'foo': 'bar', 'ham': 'spam'}) + self.assertEquals(http['data'], {'foo': ['bar'], 'ham': ['spam']}) # This test only applies to Django 1.3+ def test_request_capture(self): @@ -458,9 +490,14 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + message = timeline[1] + self.assertEquals(message['type'], 'message') - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] self.assertEquals(http['method'], 'POST') self.assertEquals(http['data'], '<unavailable>') self.assertTrue('headers' in http) @@ -478,8 +515,12 @@ class DjangoClientTest(TestCase): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') - frames = event['sentry.interfaces.Exception']['stacktrace']['frames'] + frames = exc['stacktrace']['frames'] for frame in frames: if frame['module'].startswith('django.'): assert frame.get('in_app') is False @@ -523,8 +564,7 @@ class DjangoClientTest(TestCase): request.META['HTTP_HOST'] = 'example.com' result = self.raven.get_data_from_request(request) build_absolute_uri.assert_called_once_with() - assert 'sentry.interfaces.Http' in result - assert result['sentry.interfaces.Http']['url'] == 'http://example.com/' + assert result['url'] == 'http://example.com/' class DjangoTemplateTagTest(TestCase): @@ -552,20 +592,17 @@ class DjangoLoggingTest(TestCase): logger.addHandler(handler) logger.error('This is a test error', extra={ - 'request': WSGIRequest(environ={ - 'wsgi.input': StringIO(), - 'REQUEST_METHOD': 'POST', - 'SERVER_NAME': 'testserver', - 'SERVER_PORT': '80', - 'CONTENT_TYPE': 'application/octet-stream', - 'ACCEPT': 'application/json', - }) + 'request': make_request() }) self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] + timeline = event['events'] + self.assertEqual(len(timeline), 2) + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + msg = timeline[1] + self.assertEqual(msg['type'], 'message') self.assertEquals(http['method'], 'POST') @@ -757,7 +794,7 @@ class SentryExceptionHandlerTest(TestCase): exc_info.return_value = self.exc_info sentry_exception_handler(request=self.request) - captureException.assert_called_once_with(exc_info=self.exc_info, request=self.request) + captureException.assert_called_once_with(exc_info=self.exc_info) @mock.patch.object(TempStoreClient, 'captureException') @mock.patch('sys.exc_info') diff --git a/tests/contrib/flask/tests.py b/tests/contrib/flask/tests.py index 3453e8c..87179e1 100644 --- a/tests/contrib/flask/tests.py +++ b/tests/contrib/flask/tests.py @@ -1,5 +1,3 @@ -import logging - from exam import before, fixture from mock import patch @@ -46,7 +44,7 @@ def create_app(ignore_exceptions=None): def capture_exception(): try: raise ValueError('Boom') - except: + except Exception: current_app.extensions['sentry'].captureException() return 'Hello' @@ -105,12 +103,16 @@ class FlaskTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ValueError') + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], 'hello world') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'ValueError: hello world') self.assertEquals(event['culprit'], 'tests.contrib.flask.tests in an_error') @@ -120,9 +122,14 @@ class FlaskTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] self.assertEquals(http['url'], 'http://localhost/an-error/') self.assertEquals(http['query_string'], 'foo=bar') self.assertEquals(http['method'], 'GET') @@ -147,13 +154,15 @@ class FlaskTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] + http = timeline[0] + self.assertEqual(http['type'], 'http_request') self.assertEquals(http['url'], 'http://localhost/an-error/') self.assertEquals(http['query_string'], 'biz=baz') self.assertEquals(http['method'], 'POST') - self.assertEquals(http['data'], {'foo': 'bar'}) + self.assertEquals(http['data'], {'foo': ['bar']}) self.assertTrue('headers' in http) headers = http['headers'] self.assertTrue('Content-Length' in headers, headers.keys()) @@ -174,10 +183,14 @@ class FlaskTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') assert event['message'] == 'ValueError: Boom' - assert 'sentry.interfaces.Http' in event - assert 'sentry.interfaces.Exception' in event def test_captureMessage_captures_http(self): response = self.client.get('/message/?foo=bar') @@ -185,9 +198,13 @@ class FlaskTest(BaseTest): self.assertEquals(len(self.raven.events), 1) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) - self.assertTrue('sentry.interfaces.Message' in event) - self.assertTrue('sentry.interfaces.Http' in event) + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + msg = timeline[1] + self.assertEqual(msg['type'], 'message') @patch('flask.wrappers.RequestBase._load_form_data') def test_get_data_handles_disconnected_client(self, lfd): @@ -196,9 +213,12 @@ class FlaskTest(BaseTest): self.client.post('/capture/?foo=bar', data={'baz': 'foo'}) event = self.raven.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] self.assertEqual({}, http.get('data')) def test_error_handler_with_ignored_exception(self): @@ -231,6 +251,10 @@ class FlaskLoginTest(BaseTest): def test_user(self): self.client.get('/an-error-logged-in/') event = self.raven.events.pop(0) - assert event['message'] == 'ValueError: hello world' - assert 'sentry.interfaces.Http' in event - assert 'sentry.interfaces.User' in event + timeline = event['events'] + self.assertEqual(len(timeline), 2) + + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + self.assertEqual(event['message'], 'ValueError: hello world') + self.assertIn('user', event) diff --git a/tests/contrib/tornado/tests.py b/tests/contrib/tornado/tests.py index bd147f1..78dfe71 100644 --- a/tests/contrib/tornado/tests.py +++ b/tests/contrib/tornado/tests.py @@ -87,8 +87,18 @@ class TornadoAsyncClientTestCase(testing.AsyncHTTPTestCase): self.assertEqual(response.code, 200) self.assertEqual(send.call_count, 1) args, kwargs = send.call_args + import pprint + pprint.pprint(args) + pprint.pprint(kwargs) - self.assertTrue(('sentry.interfaces.User' in kwargs)) + # timeline = event['events'] + # self.assertEqual(len(timeline), 2) + + # http = timeline[0] + # self.assertEqual(http['type'], 'http_request') + # exc = timeline[1] + + self.assertIn('user', kwargs) self.assertTrue(('sentry.interfaces.Http' in kwargs)) self.assertTrue(('sentry.interfaces.Exception' in kwargs)) diff --git a/tests/contrib/webpy/tests.py b/tests/contrib/webpy/tests.py index f2603a4..e7ef3fd 100644 --- a/tests/contrib/webpy/tests.py +++ b/tests/contrib/webpy/tests.py @@ -51,10 +51,11 @@ class WebPyTest(TestCase): self.assertEquals(resp.status, 500) self.assertEquals(len(self.store.events), 1) - event = self.store.events.pop() - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ValueError') + event = self.store.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + exc = timeline[0] + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], 'That\'s what she said') self.assertEquals(event['message'], 'ValueError: That\'s what she said') self.assertEquals(event['culprit'], 'tests.contrib.webpy.tests in GET') diff --git a/tests/handlers/logbook/tests.py b/tests/handlers/logbook/tests.py index a58d3e1..4c11116 100644 --- a/tests/handlers/logbook/tests.py +++ b/tests/handlers/logbook/tests.py @@ -34,24 +34,25 @@ class LogbookHandlerTest(TestCase): self.assertEquals(len(client.events), 1) event = client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') + self.assertEquals(event['logger'], __name__) - self.assertEquals(event['level'], 'error') self.assertEquals(event['message'], 'This is a test error') - self.assertFalse('sentry.interfaces.Exception' in event) - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEquals(msg['message'], 'This is a test error') self.assertEquals(msg['params'], ()) logger.warning('This is a test warning') self.assertEquals(len(client.events), 1) event = client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') self.assertEquals(event['logger'], __name__) - self.assertEquals(event['level'], 'warning') self.assertEquals(event['message'], 'This is a test warning') - self.assertFalse('sentry.interfaces.Exception' in event) - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEquals(msg['message'], 'This is a test warning') self.assertEquals(msg['params'], ()) @@ -60,14 +61,14 @@ class LogbookHandlerTest(TestCase): )) self.assertEquals(len(client.events), 1) event = client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] if six.PY3: expected = "'http://example.com'" else: expected = "u'http://example.com'" self.assertEquals(event['extra']['url'], expected) - self.assertFalse('sentry.interfaces.Exception' in event) - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEquals(msg['message'], 'This is a test info with a url') self.assertEquals(msg['params'], ()) @@ -78,14 +79,16 @@ class LogbookHandlerTest(TestCase): self.assertEquals(len(client.events), 1) event = client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') self.assertEquals(event['message'], 'This is a test info with an exception') - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ValueError') + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], 'This is a test ValueError') - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEquals(msg['message'], 'This is a test info with an exception') self.assertEquals(msg['params'], ()) @@ -93,10 +96,11 @@ class LogbookHandlerTest(TestCase): logger.info('This is a test of {0}', 'args') self.assertEquals(len(client.events), 1) event = client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') self.assertEquals(event['message'], 'This is a test of args') - self.assertFalse('sentry.interfaces.Exception' in event) - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEquals(msg['message'], 'This is a test of {0}') expected = ("'args'",) if six.PY3 else ("u'args'",) self.assertEquals(msg['params'], expected) diff --git a/tests/handlers/logging/tests.py b/tests/handlers/logging/tests.py index 905d069..d3e21ba 100644 --- a/tests/handlers/logging/tests.py +++ b/tests/handlers/logging/tests.py @@ -39,12 +39,12 @@ class LoggingIntegrationTest(TestCase): self.assertEqual(len(self.client.events), 1) event = self.client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') self.assertEqual(event['logger'], 'root') - self.assertEqual(event['level'], logging.INFO) self.assertEqual(event['message'], 'This is a test error') - self.assertFalse('sentry.interfaces.Exception' in event) - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEqual(msg['message'], 'This is a test error') self.assertEqual(msg['params'], ()) @@ -87,14 +87,16 @@ class LoggingIntegrationTest(TestCase): self.assertEqual(len(self.client.events), 1) event = self.client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') + exc = timeline[1] + self.assertEqual(exc['type'], 'exception') self.assertEqual(event['message'], 'This is a test info with an exception') - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEqual(exc['type'], 'ValueError') + self.assertEqual(exc['exc_type'], 'ValueError') self.assertEqual(exc['value'], 'This is a test ValueError') - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEqual(msg['message'], 'This is a test info with an exception') self.assertEqual(msg['params'], ()) @@ -104,8 +106,11 @@ class LoggingIntegrationTest(TestCase): self.assertEqual(len(self.client.events), 1) event = self.client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') self.assertEqual(event['message'], 'This is a test of args') - msg = event['sentry.interfaces.Message'] self.assertEqual(msg['message'], 'This is a test of %s') expected = ("'args'",) if six.PY3 else ("u'args'",) self.assertEqual(msg['params'], expected) @@ -116,13 +121,14 @@ class LoggingIntegrationTest(TestCase): self.assertEqual(len(self.client.events), 1) event = self.client.events.pop(0) - self.assertTrue('sentry.interfaces.Stacktrace' in event) - frames = event['sentry.interfaces.Stacktrace']['frames'] + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') + frames = msg['stacktrace']['frames'] self.assertNotEquals(len(frames), 1) frame = frames[0] self.assertEqual(frame['module'], 'raven.handlers.logging') - self.assertFalse('sentry.interfaces.Exception' in event) - self.assertTrue('sentry.interfaces.Message' in event) self.assertEqual(event['culprit'], 'root in make_record') self.assertEqual(event['message'], 'This is a test of stacks') @@ -141,14 +147,15 @@ class LoggingIntegrationTest(TestCase): self.assertEqual(len(self.client.events), 1) event = self.client.events.pop(0) - assert 'sentry.interfaces.Stacktrace' in event - assert 'culprit' in event - assert event['culprit'] == 'root in make_record' - self.assertTrue('message' in event, event) + timeline = event['events'] + self.assertEqual(len(timeline), 1) + msg = timeline[0] + self.assertEqual(msg['type'], 'message') + self.assertIn('stacktrace', msg) + self.assertIn('culprit', event) + self.assertEqual(event['culprit'], 'root in make_record') + self.assertIn('message', event) self.assertEqual(event['message'], 'This is a test of stacks') - self.assertFalse('sentry.interfaces.Exception' in event) - self.assertTrue('sentry.interfaces.Message' in event) - msg = event['sentry.interfaces.Message'] self.assertEqual(msg['message'], 'This is a test of stacks') self.assertEqual(msg['params'], ()) diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index 50f90f5..e18d2c7 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -1,6 +1,5 @@ from __future__ import with_statement -import logging import webob from exam import fixture from raven.utils.testutils import TestCase @@ -64,16 +63,17 @@ class MiddlewareTestCase(TestCase): self.assertEquals(len(self.client.events), 1) event = self.client.events.pop(0) + timeline = event['events'] + self.assertEqual(len(timeline), 2) - self.assertTrue('sentry.interfaces.Exception' in event) - exc = event['sentry.interfaces.Exception'] - self.assertEquals(exc['type'], 'ValueError') + http = timeline[0] + self.assertEqual(http['type'], 'http_request') + exc = timeline[1] + + self.assertEquals(exc['exc_type'], 'ValueError') self.assertEquals(exc['value'], 'hello world') - self.assertEquals(event['level'], logging.ERROR) self.assertEquals(event['message'], 'ValueError: hello world') - self.assertTrue('sentry.interfaces.Http' in event) - http = event['sentry.interfaces.Http'] self.assertEquals(http['url'], 'http://localhost/an-error') self.assertEquals(http['query_string'], 'foo=bar') self.assertEquals(http['method'], 'GET') diff --git a/tests/transport/tests.py b/tests/transport/tests.py index 9385095..ba43f24 100644 --- a/tests/transport/tests.py +++ b/tests/transport/tests.py @@ -63,12 +63,14 @@ class TransportTest(TestCase): mydate = datetime.datetime(2012, 5, 4, tzinfo=pytz.utc) d = calendar.timegm(mydate.timetuple()) - msg = c.build_msg('raven.events.Message', message='foo', date=d) + c.add_message('foo') + msg = c.build_msg(date=d) expected = { 'project': '1', - 'sentry.interfaces.Message': {'message': 'foo', 'params': ()}, + 'events': [ + {'type': 'message', 'message': 'foo', 'params': ()}, + ], 'server_name': 'test_server', - 'level': 40, 'modules': {}, 'tags': {}, 'time_spent': None, @@ -77,6 +79,9 @@ class TransportTest(TestCase): } # The event_id is always overridden - del msg['event_id'] + del msg['id'] + + # Timestamps are unique + del msg['events'][0]['timestamp'] self.assertDictContainsSubset(expected, msg) diff --git a/tests/utils/json/tests.py b/tests/utils/json/tests.py index 9715033..0bcdb74 100644 --- a/tests/utils/json/tests.py +++ b/tests/utils/json/tests.py @@ -14,7 +14,7 @@ class JSONTest(TestCase): def test_datetime(self): res = datetime.datetime(day=1, month=1, year=2011, hour=1, minute=1, second=1) - assert json.dumps(res) == '"2011-01-01T01:01:01Z"' + assert json.dumps(res) == '"2011-01-01T01:01:01.000000Z"' def test_set(self): res = set(['foo', 'bar']) |
