diff --git a/raven/contrib/webpy/__init__.py b/raven/contrib/webpy/__init__.py new file mode 100644 index 000000000..a32338223 --- /dev/null +++ b/raven/contrib/webpy/__init__.py @@ -0,0 +1,77 @@ +""" +raven.contrib.webpy +~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2013 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" +from __future__ import absolute_import + +import sys + +import web + +from raven.conf import setup_logging +from raven.handlers.logging import SentryHandler +from raven.contrib.webpy.utils import get_data_from_request + + +class SentryApplication(web.application): + """ + Web.py application for Sentry. + + >>> sentry = Sentry(client, mapping=urls, fvars=globals()) + + Automatically configure logging:: + + >>> sentry = Sentry(client, logging=True, mapping=urls, fvars=globals()) + + Capture an exception:: + + >>> try: + >>> 1 / 0 + >>> except ZeroDivisionError: + >>> sentry.captureException() + + Capture a message:: + + >>> sentry.captureMessage('hello, world!') + """ + def __init__(self, client, logging=False, **kwargs): + self.client = client + self.logging = logging + if self.logging: + setup_logging(SentryHandler(self.client)) + web.application.__init__(self, **kwargs) + + def handle_exception(self, *args, **kwargs): + self.client.captureException( + exc_info=kwargs.get('exc_info'), + data=get_data_from_request(), + extra={ + 'app': self, + }, + ) + + def handle(self): + try: + return web.application.handle(self) + except: + self.handle_exception(exc_info=sys.exc_info()) + raise + + def captureException(self, *args, **kwargs): + assert self.client, 'captureException called before application configured' + data = kwargs.get('data') + if data is None: + kwargs['data'] = get_data_from_request() + + 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: + kwargs['data'] = get_data_from_request() + + return self.client.captureMessage(*args, **kwargs) diff --git a/raven/contrib/webpy/utils.py b/raven/contrib/webpy/utils.py new file mode 100644 index 000000000..781cb4b29 --- /dev/null +++ b/raven/contrib/webpy/utils.py @@ -0,0 +1,26 @@ +""" +raven.contrib.webpy.utils +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" +from __future__ import absolute_import + +import web + +from raven.utils.wsgi import get_headers, get_environ + + +def get_data_from_request(): + """Returns request data extracted from web.ctx.""" + return { + 'sentry.interfaces.Http': { + 'url': '%s://%s%s' % (web.ctx['protocol'], web.ctx['host'], web.ctx['path']), + 'query_string': web.ctx.query, + 'method': web.ctx.method, + 'data': web.data(), + 'headers': dict(get_headers(web.ctx.environ)), + 'env': dict(get_environ(web.ctx.environ)), + } + } diff --git a/setup.py b/setup.py index 600756203..a365fd97a 100755 --- a/setup.py +++ b/setup.py @@ -67,6 +67,8 @@ 'webob', 'webtest', 'anyjson', + 'paste', + 'web.py' ] + flask_requires + flask_tests_requires + unittest2_requires diff --git a/tests/contrib/webpy/__init__.py b/tests/contrib/webpy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/contrib/webpy/tests.py b/tests/contrib/webpy/tests.py new file mode 100644 index 000000000..f2603a4c3 --- /dev/null +++ b/tests/contrib/webpy/tests.py @@ -0,0 +1,85 @@ +from exam import fixture +from paste.fixture import TestApp + +from raven.base import Client +from raven.contrib.webpy import SentryApplication +from raven.utils.testutils import TestCase + + +class TempStoreClient(Client): + def __init__(self, servers=None, **kwargs): + self.events = [] + super(TempStoreClient, self).__init__(servers=servers, **kwargs) + + def is_enabled(self): + return True + + def send(self, **kwargs): + self.events.append(kwargs) + + +class TestEndpoint(object): + def GET(self): + raise ValueError('That\'s what she said') + + def POST(self): + raise TypeError('Potato') + + +urls = ( + '/test', TestEndpoint +) + + +def create_app(client): + return SentryApplication(client=client, mapping=urls) + + +class WebPyTest(TestCase): + @fixture + def app(self): + self.store = TempStoreClient() + return create_app(self.store) + + @fixture + def client(self): + return TestApp(self.app.wsgifunc()) + + def test_get(self): + resp = self.client.get('/test', expect_errors=True) + + 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') + 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) + self.assertEquals(response.status, 500) + self.assertEquals(len(self.store.events), 1) + + event = self.store.events.pop() + + self.assertTrue('sentry.interfaces.Http' in event) + http = event['sentry.interfaces.Http'] + self.assertEquals(http['url'], 'http://localhost/test') + self.assertEquals(http['query_string'], '?biz=baz') + self.assertEquals(http['method'], 'POST') + self.assertEquals(http['data'], 'foo=bar') + self.assertTrue('headers' in http) + headers = http['headers'] + self.assertTrue('Content-Length' in headers, headers.keys()) + self.assertEquals(headers['Content-Length'], '7') + self.assertTrue('Content-Type' in headers, headers.keys()) + self.assertEquals(headers['Content-Type'], 'application/x-www-form-urlencoded') + self.assertTrue('Host' in headers, headers.keys()) + self.assertEquals(headers['Host'], 'localhost') + env = http['env'] + self.assertTrue('SERVER_NAME' in env, env.keys()) + self.assertEquals(env['SERVER_NAME'], 'localhost')