diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 0000000000..937c1a7352 --- /dev/null +++ b/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,101 @@ +import sys +import weakref + +from sentry_sdk._compat import reraise +from sentry_sdk.hub import Hub +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations._wsgi_common import _filter_headers +from sentry_sdk.utils import capture_internal_exceptions, event_from_exception + +import asyncio +from aiohttp.web import Application, HTTPError + + +class AioHttpIntegration(Integration): + identifier = "aiohttp" + + @staticmethod + def setup_once(): + if sys.version_info < (3, 7): + # We better have contextvars or we're going to leak state between + # requests. + raise RuntimeError( + "The aiohttp integration for Sentry requires Python 3.7+" + ) + + ignore_logger("aiohttp.server") + + old_handle = Application._handle + + async def sentry_app_handle(self, request, *args, **kwargs): + async def inner(): + hub = Hub.current + if hub.get_integration(AioHttpIntegration) is None: + return old_handle(self, request, *args, **kwargs) + + weak_request = weakref.ref(request) + + with Hub(Hub.current) as hub: + with hub.configure_scope() as scope: + scope.add_event_processor(_make_request_processor(weak_request)) + + try: + response = await old_handle(self, request) + except HTTPError: + raise + except Exception: + reraise(*_capture_exception(hub)) + + return response + + return await asyncio.create_task(inner()) + + Application._handle = sentry_app_handle + + +def _make_request_processor(weak_request): + def aiohttp_processor(event, hint): + request = weak_request() + if request is None: + return event + + with capture_internal_exceptions(): + # TODO: Figure out what to do with request body. Methods on request + # are async, but event processors are not. + + request_info = event.setdefault("request", {}) + + if "url" not in request_info: + request_info["url"] = "%s://%s%s" % ( + request.scheme, + request.host, + request.path, + ) + + if "query_string" not in request_info: + request_info["query_string"] = request.query_string + + if "method" not in request_info: + request_info["method"] = request.method + + if "env" not in request_info: + request_info["env"] = {"REMOTE_ADDR": request.remote} + + if "headers" not in request_info: + request_info["headers"] = _filter_headers(dict(request.headers)) + + return event + + return aiohttp_processor + + +def _capture_exception(hub): + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=hub.client.options, + mechanism={"type": "aiohttp", "handled": False}, + ) + hub.capture_event(event, hint=hint) + return exc_info diff --git a/tests/integrations/aiohttp/__init__.py b/tests/integrations/aiohttp/__init__.py new file mode 100644 index 0000000000..b4711aadba --- /dev/null +++ b/tests/integrations/aiohttp/__init__.py @@ -0,0 +1,3 @@ +import pytest + +aiohttp = pytest.importorskip("aiohttp") diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py new file mode 100644 index 0000000000..6abfff9227 --- /dev/null +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -0,0 +1,55 @@ +from aiohttp import web + +from sentry_sdk.integrations.aiohttp import AioHttpIntegration + + +async def test_basic(sentry_init, aiohttp_client, loop, capture_events): + sentry_init(integrations=[AioHttpIntegration()]) + + async def hello(request): + 1 / 0 + + app = web.Application() + app.router.add_get("/", hello) + + events = capture_events() + + client = await aiohttp_client(app) + resp = await client.get("/") + assert resp.status == 500 + + event, = events + + exception, = event["exception"]["values"] + assert exception["type"] == "ZeroDivisionError" + request = event["request"] + host = request["headers"]["Host"] + + assert request["env"] == {"REMOTE_ADDR": "127.0.0.1"} + assert request["method"] == "GET" + assert request["query_string"] == "" + assert request["url"] == f"http://{host}/" + assert request["headers"] == { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Host": host, + "User-Agent": request["headers"]["User-Agent"], + } + + +async def test_403_not_captured(sentry_init, aiohttp_client, loop, capture_events): + sentry_init(integrations=[AioHttpIntegration()]) + + async def hello(request): + raise web.HTTPForbidden() + + app = web.Application() + app.router.add_get("/", hello) + + events = capture_events() + + client = await aiohttp_client(app) + resp = await client.get("/") + assert resp.status == 403 + + assert not events diff --git a/tox.ini b/tox.ini index 83e6e6e27e..869e2a88c8 100644 --- a/tox.ini +++ b/tox.ini @@ -30,14 +30,11 @@ envlist = {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-pyramid-{1.3,1.4,1.5,1.6,1.7,1.8,1.9} - {pypy,py2.7,py3.5,py3.6}-rq-0.6 - {pypy,py2.7,py3.5,py3.6}-rq-0.7 - {pypy,py2.7,py3.5,py3.6}-rq-0.8 - {pypy,py2.7,py3.5,py3.6}-rq-0.9 - {pypy,py2.7,py3.5,py3.6}-rq-0.10 - {pypy,py2.7,py3.5,py3.6}-rq-0.11 + {pypy,py2.7,py3.5,py3.6}-rq-{0.6,0.7,0.8,0.9,0.10,0.11} {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-rq-0.12 + {py3.7,py3.8}-aiohttp + [testenv] deps = -r test-requirements.txt @@ -89,6 +86,9 @@ deps = rq-0.11: rq>=0.11,<0.12 rq-0.12: rq>=0.12,<0.13 + aiohttp: aiohttp>=3.4.0,<3.5.0 + aiohttp: pytest-aiohttp + linters: black linters: flake8 setenv = @@ -102,6 +102,7 @@ setenv = sanic: TESTPATH=tests/integrations/sanic pyramid: TESTPATH=tests/integrations/pyramid rq: TESTPATH=tests/integrations/rq + aiohttp: TESTPATH=tests/integrations/aiohttp passenv = AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY