diff options
-rw-r--r-- | conftest.py | 1 | ||||
-rw-r--r-- | raven/base.py | 11 | ||||
-rw-r--r-- | raven/contrib/celery/__init__.py | 67 | ||||
-rw-r--r-- | raven/contrib/django/middleware/__init__.py | 49 | ||||
-rw-r--r-- | raven/contrib/django/models.py | 121 | ||||
-rw-r--r-- | raven/contrib/django/resolver.py | 69 | ||||
-rw-r--r-- | raven/contrib/flask.py | 4 | ||||
-rw-r--r-- | raven/handlers/logging.py | 12 | ||||
-rw-r--r-- | raven/middleware.py | 1 | ||||
-rw-r--r-- | raven/utils/stacks.py | 36 | ||||
-rw-r--r-- | raven/utils/testutils.py | 14 | ||||
-rw-r--r-- | raven/utils/transaction.py | 51 | ||||
-rw-r--r-- | tests/contrib/django/test_resolver.py | 21 | ||||
-rw-r--r-- | tests/contrib/django/tests.py | 69 | ||||
-rw-r--r-- | tests/contrib/flask/tests.py | 22 | ||||
-rw-r--r-- | tests/contrib/test_celery.py | 40 | ||||
-rw-r--r-- | tests/contrib/webpy/tests.py | 1 | ||||
-rw-r--r-- | tests/handlers/logging/tests.py | 3 | ||||
-rw-r--r-- | tests/utils/stacks/tests.py | 31 | ||||
-rw-r--r-- | tests/utils/test_transaction.py | 41 |
20 files changed, 447 insertions, 217 deletions
diff --git a/conftest.py b/conftest.py index 55c641c..5c3b03f 100644 --- a/conftest.py +++ b/conftest.py @@ -81,4 +81,5 @@ def pytest_configure(config): ], }], ALLOWED_HOSTS=['*'], + DISABLE_SENTRY_INSTRUMENTATION=True, ) diff --git a/raven/base.py b/raven/base.py index 3b4c891..6ff424c 100644 --- a/raven/base.py +++ b/raven/base.py @@ -37,7 +37,8 @@ from raven.utils import json, get_versions, get_auth_header, merge_dicts from raven._compat import text_type, iteritems 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_stack_info, iter_stack_frames +from raven.utils.transaction import TransactionStack from raven.transport.registry import TransportRegistry, default_transports # enforce imports to avoid obscure stacktraces with MemoryError @@ -186,6 +187,7 @@ class Client(object): self.tags = o.get('tags') or {} self.environment = o.get('environment') or None self.release = o.get('release') or os.environ.get('HEROKU_SLUG_COMMIT') + self.transaction = TransactionStack() self.ignore_exceptions = set(o.get('ignore_exceptions') or ()) @@ -410,12 +412,7 @@ class Client(object): ) if not culprit: - if 'stacktrace' in data: - culprit = get_culprit(data['stacktrace']['frames']) - elif 'exception' in data: - stacktrace = data['exception']['values'][0].get('stacktrace') - if stacktrace: - culprit = get_culprit(stacktrace['frames']) + culprit = self.transaction.peek() if not data.get('level'): data['level'] = kwargs.get('level') or logging.ERROR diff --git a/raven/contrib/celery/__init__.py b/raven/contrib/celery/__init__.py index 3fec34c..cfc34cb 100644 --- a/raven/contrib/celery/__init__.py +++ b/raven/contrib/celery/__init__.py @@ -10,7 +10,9 @@ from __future__ import absolute_import import logging from celery.exceptions import SoftTimeLimitExceeded -from celery.signals import after_setup_logger, task_failure +from celery.signals import ( + after_setup_logger, task_failure, task_prerun, task_postrun +) from raven.handlers.logging import SentryHandler @@ -25,26 +27,7 @@ class CeleryFilter(logging.Filter): def register_signal(client, ignore_expected=False): - def process_failure_signal(sender, task_id, args, kwargs, einfo, **kw): - if ignore_expected and isinstance(einfo.exception, sender.throws): - return - - # This signal is fired inside the stack so let raven do its magic - if isinstance(einfo.exception, SoftTimeLimitExceeded): - fingerprint = ['celery', 'SoftTimeLimitExceeded', sender] - else: - fingerprint = None - client.captureException( - extra={ - 'task_id': task_id, - 'task': sender, - 'args': args, - 'kwargs': kwargs, - }, - fingerprint=fingerprint, - ) - - task_failure.connect(process_failure_signal, weak=False) + SentryCeleryHandler(client, ignore_expected=ignore_expected).install() def register_logger_signal(client, logger=None, loglevel=logging.ERROR): @@ -67,3 +50,45 @@ def register_logger_signal(client, logger=None, loglevel=logging.ERROR): logger.addHandler(handler) after_setup_logger.connect(process_logger_event, weak=False) + + +class SentryCeleryHandler(object): + def __init__(self, client, ignore_expected=False): + self.client = client + self.ignore_expected = ignore_expected + + def install(self): + task_prerun.connect(self.handle_task_prerun, weak=False) + task_postrun.connect(self.handle_task_postrun, weak=False) + task_failure.connect(self.process_failure_signal, weak=False) + + def uninstall(self): + task_prerun.disconnect(self.handle_task_prerun) + task_postrun.disconnect(self.handle_task_postrun) + task_failure.disconnect(self.process_failure_signal) + + def process_failure_signal(self, sender, task_id, args, kwargs, einfo, **kw): + if self.ignore_expected and isinstance(einfo.exception, sender.throws): + return + + # This signal is fired inside the stack so let raven do its magic + if isinstance(einfo.exception, SoftTimeLimitExceeded): + fingerprint = ['celery', 'SoftTimeLimitExceeded', sender] + else: + fingerprint = None + + self.client.captureException( + extra={ + 'task_id': task_id, + 'task': sender, + 'args': args, + 'kwargs': kwargs, + }, + fingerprint=fingerprint, + ) + + def handle_task_prerun(self, sender, task_id, task, **kw): + self.client.transaction.push(task.name) + + def handle_task_postrun(self, sender, task_id, task, **kw): + self.client.transaction.pop(task.name) diff --git a/raven/contrib/django/middleware/__init__.py b/raven/contrib/django/middleware/__init__.py index 275aef7..650c949 100644 --- a/raven/contrib/django/middleware/__init__.py +++ b/raven/contrib/django/middleware/__init__.py @@ -8,11 +8,13 @@ raven.contrib.django.middleware from __future__ import absolute_import -import threading import logging +import threading from django.conf import settings +from raven.contrib.django.resolver import RouteResolver + def is_ignorable_404(uri): """ @@ -61,9 +63,48 @@ class SentryResponseErrorIdMiddleware(object): return response -class SentryLogMiddleware(object): - # Create a threadlocal variable to store the session in for logging - thread = threading.local() +class SentryMiddleware(threading.local): + resolver = RouteResolver() + + # backwards compat + @property + def thread(self): + return self + + def _get_transaction_from_request(self, request): + # TODO(dcramer): it'd be nice to pull out parameters + # and make this a normalized path + return self.resolver.resolve(request.path) def process_request(self, request): + self._txid = None self.thread.request = request + + def process_view(self, request, func, args, kwargs): + from raven.contrib.django.models import client + + try: + self._txid = client.transaction.push( + self._get_transaction_from_request(request) + ) + except Exception as exc: + raise + client.error_logger.exception(repr(exc)) + return None + + def process_response(self, request, response): + from raven.contrib.django.models import client + + if self._txid: + client.transaction.pop(self._txid) + self._txid = None + return response + + # def process_exception(self, request, exception): + # from raven.contrib.django.models import client + + # if self._txid: + # client.transaction.pop(self._txid) + # self._txid = None + +SentryLogMiddleware = SentryMiddleware diff --git a/raven/contrib/django/models.py b/raven/contrib/django/models.py index 8411ddd..7aeb563 100644 --- a/raven/contrib/django/models.py +++ b/raven/contrib/django/models.py @@ -16,13 +16,17 @@ import sys import warnings from django.conf import settings +from django.core.signals import got_request_exception, request_started +from threading import Lock -from raven._compat import PY2, binary_type, text_type, string_types +from raven._compat import PY2, binary_type, text_type from raven.utils.conf import convert_options from raven.utils.imports import import_string logger = logging.getLogger('sentry.errors.client') +settings_lock = Lock() + def get_installed_apps(): """ @@ -151,58 +155,64 @@ def sentry_exception_handler(request=None, **kwargs): warnings.warn('Unable to process log entry: %s' % (exc,)) -def register_handlers(): - from django.core.signals import got_request_exception, request_started - - def before_request(*args, **kwargs): - client.context.activate() - request_started.connect(before_request, weak=False) +class SentryDjangoHandler(object): + def __init__(self, client=client): + self.client = client - # HACK: support Sentry's internal communication - if 'sentry' in settings.INSTALLED_APPS: - from django.db import transaction - # Django 1.6 - if hasattr(transaction, 'atomic'): - commit_on_success = transaction.atomic + try: + import celery + except ImportError: + self.has_celery = False else: - commit_on_success = transaction.commit_on_success + self.has_celery = celery.VERSION >= (2, 5) - @commit_on_success - def wrap_sentry(request, **kwargs): - if transaction.is_dirty(): - transaction.rollback() - return sentry_exception_handler(request, **kwargs) + self.celery_handler = None - exception_handler = wrap_sentry - else: - exception_handler = sentry_exception_handler + def install_celery(self): + from raven.contrib.celery import ( + SentryCeleryHandler, register_logger_signal + ) - # Connect to Django's internal signal handler - got_request_exception.connect(exception_handler, weak=False) + self.celery_handler = SentryCeleryHandler(client).install() - # If Celery is installed, register a signal handler - if 'djcelery' in settings.INSTALLED_APPS: - try: - # Celery < 2.5? is not supported - from raven.contrib.celery import ( - register_signal, register_logger_signal) - except ImportError: - logger.exception('Failed to install Celery error handler') - else: + # try: + # ga = lambda x, d=None: getattr(settings, 'SENTRY_%s' % x, d) + # options = getattr(settings, 'RAVEN_CONFIG', {}) + # loglevel = options.get('celery_loglevel', + # ga('CELERY_LOGLEVEL', logging.ERROR)) + + # register_logger_signal(client, loglevel=loglevel) + # except Exception: + # logger.exception('Failed to install Celery error handler') + + def install(self): + request_started.connect(self.before_request, weak=False) + got_request_exception.connect(self.exception_handler, weak=False) + + if self.has_celery: try: - register_signal(client) + self.install_celery() except Exception: logger.exception('Failed to install Celery error handler') + def uninstall(self): + request_started.disconnect(self.before_request) + got_request_exception.disconnect(self.exception_handler) + + if self.celery_handler: + self.celery_handler.uninstall() + + def exception_handler(self, request=None, **kwargs): + try: + self.client.captureException(exc_info=sys.exc_info(), request=request) + except Exception as exc: try: - ga = lambda x, d=None: getattr(settings, 'SENTRY_%s' % x, d) - options = getattr(settings, 'RAVEN_CONFIG', {}) - loglevel = options.get('celery_loglevel', - ga('CELERY_LOGLEVEL', logging.ERROR)) + logger.exception('Unable to process log entry: %s' % (exc,)) + except Exception as exc: + warnings.warn('Unable to process log entry: %s' % (exc,)) - register_logger_signal(client, loglevel=loglevel) - except Exception: - logger.exception('Failed to install Celery error handler') + def before_request(self, *args, **kwargs): + self.client.context.activate() def register_serializers(): @@ -210,7 +220,30 @@ def register_serializers(): import raven.contrib.django.serializers # NOQA -if ('raven.contrib.django' in settings.INSTALLED_APPS - or 'raven.contrib.django.raven_compat' in settings.INSTALLED_APPS): - register_handlers() +def install_middleware(): + """ + Force installation of SentryMiddlware if it's not explicitly present. + + This ensures things like request context and transaction names are made + available. + """ + name = 'raven.contrib.django.middleware.SentryMiddleware' + all_names = (name, 'raven.contrib.django.middleware.SentryLogMiddleware') + with settings_lock: + middleware_list = set(settings.MIDDLEWARE_CLASSES) + if not any(n in middleware_list for n in all_names): + settings.MIDDLEWARE_CLASSES = ( + name, + ) + tuple(settings.MIDDLEWARE_CLASSES) + + +if ( + 'raven.contrib.django' in settings.INSTALLED_APPS or + 'raven.contrib.django.raven_compat' in settings.INSTALLED_APPS +): register_serializers() + install_middleware() + + if not getattr(settings, 'DISABLE_SENTRY_INSTRUMENTATION', False): + handler = SentryDjangoHandler() + handler.install() diff --git a/raven/contrib/django/resolver.py b/raven/contrib/django/resolver.py new file mode 100644 index 0000000..9103822 --- /dev/null +++ b/raven/contrib/django/resolver.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import + +import re + +from django.urls import get_resolver, Resolver404 + + +class RouteResolver(object): + _optional_group_matcher = re.compile(r'\(\?\:([^\)]+)\)') + _named_group_matcher = re.compile(r'\(\?P<(\w+)>[^\)]+\)') + _non_named_group_matcher = re.compile(r'\([^\)]+\)') + # [foo|bar|baz] + _either_option_matcher = re.compile(r'\[([^\]]+)\|([^\]]+)\]') + _camel_re = re.compile(r'([A-Z]+)([a-z])') + + _cache = {} + + def _simplify(self, pattern): + """ + Clean up urlpattern regexes into something readable by humans: + + From: + > "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$" + + To: + > "{sport_slug}/athletes/{athlete_slug}/" + """ + # remove optional params + pattern = self._optional_group_matcher.sub(lambda m: '[%s]' % m.group(1), pattern) + + # handle named groups first + pattern = self._named_group_matcher.sub(lambda m: '{%s}' % m.group(1), pattern) + + # handle non-named groups + pattern = self._non_named_group_matcher.sub("{var}", pattern) + + # handle optional params + pattern = self._either_option_matcher.sub(lambda m: m.group(1), pattern) + + # clean up any outstanding regex-y characters. + pattern = pattern.replace('^', '').replace('$', '') \ + .replace('?', '').replace('//', '/').replace('\\', '') + if not pattern.startswith('/'): + pattern = '/' + pattern + return pattern + + def resolve(self, path, urlconf=None): + # TODO(dcramer): it'd be nice to pull out parameters + # and make this a normalized path + resolver = get_resolver(urlconf) + match = resolver.regex.search(path) + if match: + new_path = path[match.end():] + for pattern in resolver.url_patterns: + try: + sub_match = pattern.resolve(new_path) + except Resolver404: + continue + if sub_match: + pattern = pattern.regex.pattern + try: + return self._cache[pattern] + except KeyError: + pass + + pattern_name = self._simplify(pattern) + self._cache[pattern] = pattern + return pattern_name + return path diff --git a/raven/contrib/flask.py b/raven/contrib/flask.py index a973344..670e2ec 100644 --- a/raven/contrib/flask.py +++ b/raven/contrib/flask.py @@ -216,6 +216,9 @@ class Sentry(object): def before_request(self, *args, **kwargs): self.last_event_id = None + + self.client.transaction.push(request.url_rule.rule) + try: self.client.http_context(self.get_http_info(request)) except Exception as e: @@ -229,6 +232,7 @@ class Sentry(object): if self.last_event_id: response.headers['X-Sentry-ID'] = self.last_event_id self.client.context.clear() + self.client.transaction.pop(request.url_rule.rule) return response def init_app(self, app, dsn=None, logging=None, level=None, diff --git a/raven/handlers/logging.py b/raven/handlers/logging.py index cc82550..180e54b 100644 --- a/raven/handlers/logging.py +++ b/raven/handlers/logging.py @@ -17,7 +17,7 @@ import traceback from raven._compat import string_types, iteritems, text_type from raven.base import Client from raven.utils.encoding import to_string -from raven.utils.stacks import iter_stack_frames, label_from_frame +from raven.utils.stacks import iter_stack_frames RESERVED = frozenset(( 'stack', 'name', 'module', 'funcName', 'args', 'msg', 'levelno', @@ -159,16 +159,6 @@ class SentryHandler(logging.Handler, object): event_type = 'raven.events.Exception' handler_kwargs = {'exc_info': record.exc_info} - # HACK: discover a culprit when we normally couldn't - elif not (data.get('stacktrace') or 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 diff --git a/raven/middleware.py b/raven/middleware.py index c9c5566..cc2b824 100644 --- a/raven/middleware.py +++ b/raven/middleware.py @@ -70,6 +70,7 @@ class ClosingIterator(Iterator): self.iterable.close() finally: self.sentry.client.context.clear() + self.sentry.transaction.clear() self.closed = True diff --git a/raven/utils/stacks.py b/raven/utils/stacks.py index 7933313..ee95d7a 100644 --- a/raven/utils/stacks.py +++ b/raven/utils/stacks.py @@ -11,7 +11,6 @@ import inspect import linecache import re import sys -import warnings from raven.utils.serializer import transform from raven._compat import iteritems @@ -82,41 +81,6 @@ def get_lines_from_file(filename, lineno, context_lines, ) -def label_from_frame(frame): - module = frame.get('module') or '?' - function = frame.get('function') or '?' - if module == function == '?': - return '' - return '%s in %s' % (module, function) - - -def get_culprit(frames, *args, **kwargs): - # We iterate through each frame looking for a deterministic culprit - # When one is found, we mark it as last "best guess" (best_guess) and then - # check it against ``exclude_paths``. If it isn't listed, then we - # use this option. If nothing is found, we use the "best guess". - if args or kwargs: - warnings.warn('get_culprit no longer does application detection') - - best_guess = None - culprit = None - for frame in reversed(frames): - culprit = label_from_frame(frame) - if not culprit: - culprit = None - continue - - if frame.get('in_app'): - return culprit - elif not best_guess: - best_guess = culprit - elif best_guess: - break - - # Return either the best guess or the last frames call - return best_guess or culprit - - def _getitem_from_frame(f_locals, key, default=None): """ f_locals is not guaranteed to have .get(), but it will always diff --git a/raven/utils/testutils.py b/raven/utils/testutils.py index 52df1b1..4859f43 100644 --- a/raven/utils/testutils.py +++ b/raven/utils/testutils.py @@ -7,6 +7,8 @@ raven.utils.testutils """ from __future__ import absolute_import +import raven + from exam import Exam try: @@ -17,3 +19,15 @@ except ImportError: class TestCase(Exam, BaseTestCase): pass + + +class InMemoryClient(raven.Client): + def __init__(self, **kwargs): + self.events = [] + super(InMemoryClient, self).__init__(**kwargs) + + def is_enabled(self): + return True + + def send(self, **kwargs): + self.events.append(kwargs) diff --git a/raven/utils/transaction.py b/raven/utils/transaction.py new file mode 100644 index 0000000..8dc6e06 --- /dev/null +++ b/raven/utils/transaction.py @@ -0,0 +1,51 @@ +from __future__ import absolute_import + +from threading import local + + +class TransactionContext(object): + def __init__(self, stack, context): + self.stack = stack + self.context = context + + def __enter__(self): + self.stack.push(self.context) + return self + + def __exit__(self, *exc_info): + self.stack.pop(self.context) + + +class TransactionStack(local): + def __init__(self): + self.stack = [] + + def __len__(self): + return len(self.stack) + + def __iter__(self): + return iter(self.stack) + + def __call__(self, context): + return TransactionContext(self, context) + + def clear(self): + self.stack = [] + + def peek(self): + try: + return self.stack[-1] + except IndexError: + return None + + def push(self, context): + self.stack.append(context) + return context + + def pop(self, context=None): + if context is None: + return self.stack.pop() + + while self.stack: + if self.stack.pop() is context: + return context diff --git a/tests/contrib/django/test_resolver.py b/tests/contrib/django/test_resolver.py new file mode 100644 index 0000000..930d287 --- /dev/null +++ b/tests/contrib/django/test_resolver.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import + +from raven.contrib.django.resolver import RouteResolver + + +def test_no_match(): + resolver = RouteResolver() + result = resolver.resolve('/foo/bar', 'raven.contrib.django.urls') + assert result == '/foo/bar' + + +def test_simple_match(): + resolver = RouteResolver() + result = resolver.resolve('/report/', 'raven.contrib.django.urls') + assert result == '/report/' + + +def test_complex_match(): + resolver = RouteResolver() + result = resolver.resolve('/api/1234/store/', 'raven.contrib.django.urls') + assert result == '/api/{project_id}/store/' diff --git a/tests/contrib/django/tests.py b/tests/contrib/django/tests.py index 6fca81c..d369cc4 100644 --- a/tests/contrib/django/tests.py +++ b/tests/contrib/django/tests.py @@ -28,7 +28,9 @@ from raven.base import Client from raven.contrib.django.client import DjangoClient from raven.contrib.django.celery import CeleryClient from raven.contrib.django.handlers import SentryHandler -from raven.contrib.django.models import client, get_client, sentry_exception_handler +from raven.contrib.django.models import ( + SentryDjangoHandler, client, get_client +) from raven.contrib.django.middleware.wsgi import Sentry from raven.contrib.django.templatetags.raven import sentry_public_dsn from raven.contrib.django.views import is_valid_origin @@ -36,6 +38,7 @@ from raven.transport import HTTPTransport from raven.utils.serializer import transform from django.test.client import Client as TestClient, ClientHandler as TestClientHandler + from .models import TestModel settings.SENTRY_CLIENT = 'tests.contrib.django.tests.TempStoreClient' @@ -128,6 +131,9 @@ class DjangoClientTest(TestCase): def setUp(self): self.raven = get_client() + self.handler = SentryDjangoHandler(self.raven) + self.handler.install() + self.addCleanup(self.handler.uninstall) def test_basic(self): self.raven.captureMessage(message='foo') @@ -156,7 +162,6 @@ class DjangoClientTest(TestCase): assert exc['value'], "int() argument must be a string or a number == not 'NoneType'" assert event['level'] == logging.ERROR assert event['message'], "TypeError: int() argument must be a string or a number == not 'NoneType'" - assert event['culprit'] == 'tests.contrib.django.tests in test_signal_integration' @pytest.mark.skipif(sys.version_info[:2] == (2, 6), reason='Python 2.6') def test_view_exception(self): @@ -170,7 +175,6 @@ class DjangoClientTest(TestCase): assert exc['value'] == 'view exception' assert event['level'] == logging.ERROR assert event['message'] == 'Exception: view exception' - assert event['culprit'] == 'tests.contrib.django.views in raise_exc' def test_user_info(self): with Settings(MIDDLEWARE_CLASSES=[ @@ -262,7 +266,6 @@ class DjangoClientTest(TestCase): assert exc['value'] == 'request' assert event['level'] == logging.ERROR assert event['message'] == 'ImportError: request' - assert event['culprit'] == 'tests.contrib.django.middleware in process_request' def test_response_middlware_exception(self): if django.VERSION[:2] < (1, 3): @@ -279,7 +282,6 @@ class DjangoClientTest(TestCase): assert exc['value'] == 'response' assert event['level'] == logging.ERROR assert event['message'] == 'ImportError: response' - assert event['culprit'] == 'tests.contrib.django.middleware in process_response' def test_broken_500_handler_with_middleware(self): with Settings(BREAK_THAT_500=True, INSTALLED_APPS=['raven.contrib.django']): @@ -297,7 +299,6 @@ class DjangoClientTest(TestCase): assert exc['value'] == 'view exception' assert event['level'] == logging.ERROR assert event['message'] == 'Exception: view exception' - assert event['culprit'] == 'tests.contrib.django.views in raise_exc' event = self.raven.events.pop(0) @@ -307,7 +308,6 @@ class DjangoClientTest(TestCase): assert exc['value'] == 'handler500' assert event['level'] == logging.ERROR assert event['message'] == 'ValueError: handler500' - assert event['culprit'] == 'tests.contrib.django.urls in handler500' def test_view_middleware_exception(self): with Settings(MIDDLEWARE_CLASSES=['tests.contrib.django.middleware.BrokenViewMiddleware']): @@ -322,30 +322,6 @@ class DjangoClientTest(TestCase): assert exc['value'] == 'view' assert event['level'] == logging.ERROR assert event['message'] == 'ImportError: view' - assert event['culprit'] == 'tests.contrib.django.middleware in process_view' - - def test_exclude_modules_view(self): - exclude_paths = self.raven.exclude_paths - self.raven.exclude_paths = ['tests.views'] - self.assertRaises(Exception, self.client.get, reverse('sentry-raise-exc-decor')) - - assert len(self.raven.events) == 1 - event = self.raven.events.pop(0) - - assert event['culprit'] == 'tests.contrib.django.views in raise_exc' - self.raven.exclude_paths = exclude_paths - - def test_include_modules(self): - include_paths = self.raven.include_paths - self.raven.include_paths = ['django.shortcuts'] - - self.assertRaises(Exception, self.client.get, reverse('sentry-django-exc')) - - assert len(self.raven.events) == 1 - event = self.raven.events.pop(0) - - assert event['culprit'].startswith('django.shortcuts in ') - self.raven.include_paths = include_paths @pytest.mark.skipif(DJANGO_18, reason='Django 1.8+ not supported') def test_template_name_as_view(self): @@ -393,7 +369,7 @@ class DjangoClientTest(TestCase): resp = self.client.get('/non-existent-page') assert resp.status_code == 404 - assert len(self.raven.events) == 1 + assert len(self.raven.events) == 1, [e['message'] for e in self.raven.events] event = self.raven.events.pop(0) assert event['level'] == logging.INFO @@ -786,11 +762,16 @@ class SentryExceptionHandlerTest(TestCase): def exc_info(self): return (ValueError, ValueError('lol world'), None) + def setUp(self): + super(SentryExceptionHandlerTest, self).setUp() + self.client = get_client() + self.handler = SentryDjangoHandler(self.client) + @mock.patch.object(TempStoreClient, 'captureException') @mock.patch('sys.exc_info') def test_does_capture_exception(self, exc_info, captureException): exc_info.return_value = self.exc_info - sentry_exception_handler(request=self.request) + self.handler.exception_handler(request=self.request) captureException.assert_called_once_with(exc_info=self.exc_info, request=self.request) @@ -799,11 +780,11 @@ class SentryExceptionHandlerTest(TestCase): def test_does_exclude_filtered_types(self, exc_info, mock_send): exc_info.return_value = self.exc_info try: - get_client().ignore_exceptions = set(['ValueError']) + self.client.ignore_exceptions = set(['ValueError']) - sentry_exception_handler(request=self.request) + self.handler.exception_handler(request=self.request) finally: - get_client().ignore_exceptions.clear() + self.client.ignore_exceptions.clear() assert not mock_send.called @@ -814,12 +795,12 @@ class SentryExceptionHandlerTest(TestCase): try: if six.PY3: - get_client().ignore_exceptions = set(['builtins.*']) + self.client.ignore_exceptions = set(['builtins.*']) else: - get_client().ignore_exceptions = set(['exceptions.*']) - sentry_exception_handler(request=self.request) + self.client.ignore_exceptions = set(['exceptions.*']) + self.handler.exception_handler(request=self.request) finally: - get_client().ignore_exceptions.clear() + self.client.ignore_exceptions.clear() assert not mock_send.called @@ -830,11 +811,11 @@ class SentryExceptionHandlerTest(TestCase): try: if six.PY3: - get_client().ignore_exceptions = set(['builtins.ValueError']) + self.client.ignore_exceptions = set(['builtins.ValueError']) else: - get_client().ignore_exceptions = set(['exceptions.ValueError']) - sentry_exception_handler(request=self.request) + self.client.ignore_exceptions = set(['exceptions.ValueError']) + self.handler.exception_handler(request=self.request) finally: - get_client().ignore_exceptions.clear() + self.client.ignore_exceptions.clear() assert not mock_send.called diff --git a/tests/contrib/flask/tests.py b/tests/contrib/flask/tests.py index cf87329..73dff6a 100644 --- a/tests/contrib/flask/tests.py +++ b/tests/contrib/flask/tests.py @@ -6,24 +6,11 @@ from mock import patch from flask import Flask, current_app, g from flask.ext.login import LoginManager, AnonymousUserMixin, login_user -from raven.base import Client from raven.contrib.flask import Sentry -from raven.utils.testutils import TestCase +from raven.utils.testutils import InMemoryClient, TestCase from raven.handlers.logging import SentryHandler -class TempStoreClient(Client): - def __init__(self, **kwargs): - self.events = [] - super(TempStoreClient, self).__init__(**kwargs) - - def is_enabled(self): - return True - - def send(self, **kwargs): - self.events.append(kwargs) - - class User(AnonymousUserMixin): is_active = lambda x: True is_authenticated = lambda x: True @@ -96,12 +83,12 @@ class BaseTest(TestCase): @before def bind_sentry(self): - self.raven = TempStoreClient() + self.raven = InMemoryClient() self.middleware = Sentry(self.app, client=self.raven) def make_client_and_raven(self, *args, **kwargs): app = create_app(*args, **kwargs) - raven = TempStoreClient() + raven = InMemoryClient() Sentry(app, client=raven) return app.test_client(), raven, app @@ -124,7 +111,6 @@ class FlaskTest(BaseTest): 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') def test_capture_plus_logging(self): client, raven, app = self.make_client_and_raven(debug=False) @@ -251,7 +237,7 @@ class FlaskTest(BaseTest): def test_logging_setup_with_exclusion_list(self): app = Flask(__name__) - raven = TempStoreClient() + raven = InMemoryClient() Sentry(app, client=raven, logging=True, logging_exclusions=("excluded_logger",)) diff --git a/tests/contrib/test_celery.py b/tests/contrib/test_celery.py new file mode 100644 index 0000000..bc3de0f --- /dev/null +++ b/tests/contrib/test_celery.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import + +import celery + +from raven.contrib.celery import SentryCeleryHandler +from raven.utils.testutils import InMemoryClient, TestCase + + +class CeleryTestCase(TestCase): + def setUp(self): + super(CeleryTestCase, self).setUp() + self.celery = celery.Celery(__name__) + self.celery.conf.CELERY_ALWAYS_EAGER = True + + self.client = InMemoryClient() + self.handler = SentryCeleryHandler(self.client, ignore_expected=True) + self.handler.install() + self.addCleanup(self.handler.uninstall) + + def test_simple(self): + @self.celery.task(name='dummy_task') + def dummy_task(x, y): + return x / y + + dummy_task.delay(1, 2) + dummy_task.delay(1, 0) + assert len(self.client.events) == 1 + event = self.client.events[0] + exception = event['exception']['values'][0] + assert event['culprit'] == 'dummy_task' + assert exception['type'] == 'ZeroDivisionError' + + def test_ignore_expected(self): + @self.celery.task(name='dummy_task', throws=(ZeroDivisionError,)) + def dummy_task(x, y): + return x / y + + dummy_task.delay(1, 2) + dummy_task.delay(1, 0) + assert len(self.client.events) == 0 diff --git a/tests/contrib/webpy/tests.py b/tests/contrib/webpy/tests.py index d96cb9c..7d63e50 100644 --- a/tests/contrib/webpy/tests.py +++ b/tests/contrib/webpy/tests.py @@ -57,7 +57,6 @@ class WebPyTest(TestCase): self.assertEquals(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') def test_post(self): response = self.client.post('/test?biz=baz', params={'foo': 'bar'}, expect_errors=True) diff --git a/tests/handlers/logging/tests.py b/tests/handlers/logging/tests.py index 01feb09..bf8063a 100644 --- a/tests/handlers/logging/tests.py +++ b/tests/handlers/logging/tests.py @@ -167,7 +167,6 @@ class LoggingIntegrationTest(TestCase): self.assertEqual(frame['module'], 'raven.handlers.logging') assert 'exception' not 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') def test_no_record_stack(self): @@ -186,8 +185,6 @@ class LoggingIntegrationTest(TestCase): self.assertEqual(len(self.client.events), 1) event = self.client.events.pop(0) assert 'stacktrace' in event - assert 'culprit' in event - assert event['culprit'] == 'root in make_record' self.assertTrue('message' in event, event) self.assertEqual(event['message'], 'This is a test of stacks') assert 'exception' not in event diff --git a/tests/utils/stacks/tests.py b/tests/utils/stacks/tests.py index a0cc5be..bfd9215 100644 --- a/tests/utils/stacks/tests.py +++ b/tests/utils/stacks/tests.py @@ -6,7 +6,7 @@ import os.path from mock import Mock from raven.utils.testutils import TestCase -from raven.utils.stacks import get_culprit, get_stack_info, get_lines_from_file +from raven.utils.stacks import get_stack_info, get_lines_from_file class Context(object): @@ -18,33 +18,6 @@ class Context(object): iterkeys = lambda s, *a: six.iterkeys(s.dict, *a) -class GetCulpritTest(TestCase): - def test_empty_module(self): - culprit = get_culprit([{ - 'module': None, - 'function': 'foo', - }]) - assert culprit == '? in foo' - - def test_empty_function(self): - culprit = get_culprit([{ - 'module': 'foo', - 'function': None, - }]) - assert culprit == 'foo in ?' - - def test_no_module_or_function(self): - culprit = get_culprit([{}]) - assert culprit is None - - def test_all_params(self): - culprit = get_culprit([{ - 'module': 'package.name', - 'function': 'foo', - }]) - assert culprit == 'package.name in foo' - - class GetStackInfoTest(TestCase): def test_bad_locals_in_frame(self): frame = Mock() @@ -95,6 +68,7 @@ class GetStackInfoTest(TestCase): assert results['frames'][8]['filename'] == '8' assert results['frames'][9]['filename'] == '9' + class FailLoader(): ''' Recreating the built-in loaders from a fake stack trace was brittle. @@ -109,6 +83,7 @@ class FailLoader(): else: raise ValueError('Invalid file extension') + class GetLineFromFileTest(TestCase): def setUp(self): self.loader = FailLoader() diff --git a/tests/utils/test_transaction.py b/tests/utils/test_transaction.py new file mode 100644 index 0000000..3ffe0cd --- /dev/null +++ b/tests/utils/test_transaction.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import + +from raven.utils.transaction import TransactionStack + + +def test_simple(): + stack = TransactionStack() + + stack.push('foo') + + assert len(stack) == 1 + assert stack.peek() == 'foo' + + bar = stack.push(['bar']) + + assert len(stack) == 2 + assert stack.peek() == ['bar'] + + stack.push({'baz': True}) + + assert len(stack) == 3 + assert stack.peek() == {'baz': True} + + stack.pop(bar) + + assert len(stack) == 1 + assert stack.peek() == 'foo' + + stack.pop() + + assert len(stack) == 0 + assert stack.peek() == None + + +def test_context_manager(): + stack = TransactionStack() + + with stack('foo'): + assert stack.peek() == 'foo' + + assert stack.peek() is None |