summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--conftest.py1
-rw-r--r--raven/base.py11
-rw-r--r--raven/contrib/celery/__init__.py67
-rw-r--r--raven/contrib/django/middleware/__init__.py49
-rw-r--r--raven/contrib/django/models.py121
-rw-r--r--raven/contrib/django/resolver.py69
-rw-r--r--raven/contrib/flask.py4
-rw-r--r--raven/handlers/logging.py12
-rw-r--r--raven/middleware.py1
-rw-r--r--raven/utils/stacks.py36
-rw-r--r--raven/utils/testutils.py14
-rw-r--r--raven/utils/transaction.py51
-rw-r--r--tests/contrib/django/test_resolver.py21
-rw-r--r--tests/contrib/django/tests.py69
-rw-r--r--tests/contrib/flask/tests.py22
-rw-r--r--tests/contrib/test_celery.py40
-rw-r--r--tests/contrib/webpy/tests.py1
-rw-r--r--tests/handlers/logging/tests.py3
-rw-r--r--tests/utils/stacks/tests.py31
-rw-r--r--tests/utils/test_transaction.py41
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