Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/integrations/aiohttp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest

aiohttp = pytest.importorskip("aiohttp")
55 changes: 55 additions & 0 deletions tests/integrations/aiohttp/test_aiohttp.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 7 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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
Expand Down