From c8b1c1c0219b150131c29ff1553615be6f23f18d Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 3 Dec 2018 21:38:06 +0100 Subject: [PATCH 1/5] feat: aiohttp-server integration --- sentry_sdk/integrations/aiohttp.py | 99 ++++++++++++++++++++++ tests/integrations/aiohttp/__init__.py | 3 + tests/integrations/aiohttp/conftest.py | 1 + tests/integrations/aiohttp/test_aiohttp.py | 37 ++++++++ tox.ini | 12 +-- 5 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 sentry_sdk/integrations/aiohttp.py create mode 100644 tests/integrations/aiohttp/__init__.py create mode 100644 tests/integrations/aiohttp/conftest.py create mode 100644 tests/integrations/aiohttp/test_aiohttp.py diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py new file mode 100644 index 0000000000..e3e360b7f2 --- /dev/null +++ b/sentry_sdk/integrations/aiohttp.py @@ -0,0 +1,99 @@ +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 + + +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 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..6439e65c52 --- /dev/null +++ b/tests/integrations/aiohttp/__init__.py @@ -0,0 +1,3 @@ +import pytest + +sanic = pytest.importorskip("aiohttp") diff --git a/tests/integrations/aiohttp/conftest.py b/tests/integrations/aiohttp/conftest.py new file mode 100644 index 0000000000..81588ebb6e --- /dev/null +++ b/tests/integrations/aiohttp/conftest.py @@ -0,0 +1 @@ +pytest_plugins = "aiohttp.pytest_plugin" diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py new file mode 100644 index 0000000000..642cabfcf2 --- /dev/null +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -0,0 +1,37 @@ +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": "Python/3.7 aiohttp/3.4.4", + } diff --git a/tox.ini b/tox.ini index 83e6e6e27e..4f67ab3f1f 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,8 @@ 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 + linters: black linters: flake8 setenv = @@ -102,6 +101,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 From 6260f3f39105c00c1a2c91fb788dfff5675d8c1f Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 3 Dec 2018 21:56:42 +0100 Subject: [PATCH 2/5] fix: Ignore 403 --- sentry_sdk/integrations/aiohttp.py | 4 +++- tests/integrations/aiohttp/test_aiohttp.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index e3e360b7f2..937c1a7352 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -9,7 +9,7 @@ from sentry_sdk.utils import capture_internal_exceptions, event_from_exception import asyncio -from aiohttp.web import Application +from aiohttp.web import Application, HTTPError class AioHttpIntegration(Integration): @@ -42,6 +42,8 @@ async def inner(): try: response = await old_handle(self, request) + except HTTPError: + raise except Exception: reraise(*_capture_exception(hub)) diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 642cabfcf2..b7237fe7b1 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -35,3 +35,21 @@ async def hello(request): "Host": host, "User-Agent": "Python/3.7 aiohttp/3.4.4", } + + +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 From c561924fb80ec456ad120799820030463e4c03ec Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 3 Dec 2018 22:24:02 +0100 Subject: [PATCH 3/5] fix: Copypaste error --- tests/integrations/aiohttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/aiohttp/__init__.py b/tests/integrations/aiohttp/__init__.py index 6439e65c52..b4711aadba 100644 --- a/tests/integrations/aiohttp/__init__.py +++ b/tests/integrations/aiohttp/__init__.py @@ -1,3 +1,3 @@ import pytest -sanic = pytest.importorskip("aiohttp") +aiohttp = pytest.importorskip("aiohttp") From 9b3ec82028921ab41000ba310d83f046ff1a33c0 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 3 Dec 2018 22:33:02 +0100 Subject: [PATCH 4/5] fix: aiohttp plugin --- tests/integrations/aiohttp/conftest.py | 1 - tox.ini | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 tests/integrations/aiohttp/conftest.py diff --git a/tests/integrations/aiohttp/conftest.py b/tests/integrations/aiohttp/conftest.py deleted file mode 100644 index 81588ebb6e..0000000000 --- a/tests/integrations/aiohttp/conftest.py +++ /dev/null @@ -1 +0,0 @@ -pytest_plugins = "aiohttp.pytest_plugin" diff --git a/tox.ini b/tox.ini index 4f67ab3f1f..869e2a88c8 100644 --- a/tox.ini +++ b/tox.ini @@ -87,6 +87,7 @@ deps = rq-0.12: rq>=0.12,<0.13 aiohttp: aiohttp>=3.4.0,<3.5.0 + aiohttp: pytest-aiohttp linters: black linters: flake8 From 0eed14711ca14474fd8aa09047bb078406f24663 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Mon, 3 Dec 2018 22:45:50 +0100 Subject: [PATCH 5/5] fix: Dont assert version numbers --- tests/integrations/aiohttp/test_aiohttp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index b7237fe7b1..6abfff9227 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -33,7 +33,7 @@ async def hello(request): "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Host": host, - "User-Agent": "Python/3.7 aiohttp/3.4.4", + "User-Agent": request["headers"]["User-Agent"], }