Skip to content

Commit 87e4a7e

Browse files
feat(aiohttp): Add failed_request_status_codes
`failed_request_status_codes` allows users to specify the status codes, whose corresponding `HTTPException` types, should be reported to Sentry. By default, these include 5xx statuses, which is a change from the previous default behavior, where no `HTTPException`s would be reported to Sentry. Closes #3535
1 parent 26b86a5 commit 87e4a7e

File tree

2 files changed

+142
-3
lines changed

2 files changed

+142
-3
lines changed

sentry_sdk/integrations/aiohttp.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
from aiohttp.web_request import Request
4848
from aiohttp.web_urldispatcher import UrlMappingMatchInfo
4949
from aiohttp import TraceRequestStartParams, TraceRequestEndParams
50+
51+
from collections.abc import Set
5052
from types import SimpleNamespace
5153
from typing import Any
5254
from typing import Optional
@@ -58,20 +60,27 @@
5860

5961

6062
TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
63+
DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600))
6164

6265

6366
class AioHttpIntegration(Integration):
6467
identifier = "aiohttp"
6568
origin = f"auto.http.{identifier}"
6669

67-
def __init__(self, transaction_style="handler_name"):
68-
# type: (str) -> None
70+
def __init__(
71+
self,
72+
transaction_style="handler_name", # type: str
73+
*,
74+
failed_request_status_codes=DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int]
75+
):
76+
# type: (...) -> None
6977
if transaction_style not in TRANSACTION_STYLE_VALUES:
7078
raise ValueError(
7179
"Invalid value for transaction_style: %s (must be in %s)"
7280
% (transaction_style, TRANSACTION_STYLE_VALUES)
7381
)
7482
self.transaction_style = transaction_style
83+
self._failed_request_status_codes = failed_request_status_codes
7584

7685
@staticmethod
7786
def setup_once():
@@ -99,7 +108,8 @@ def setup_once():
99108

100109
async def sentry_app_handle(self, request, *args, **kwargs):
101110
# type: (Any, Request, *Any, **Any) -> Any
102-
if sentry_sdk.get_client().get_integration(AioHttpIntegration) is None:
111+
integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
112+
if integration is None:
103113
return await old_handle(self, request, *args, **kwargs)
104114

105115
weak_request = weakref.ref(request)
@@ -130,6 +140,13 @@ async def sentry_app_handle(self, request, *args, **kwargs):
130140
response = await old_handle(self, request)
131141
except HTTPException as e:
132142
transaction.set_http_status(e.status_code)
143+
144+
if (
145+
e.status_code
146+
in integration._failed_request_status_codes
147+
):
148+
_capture_exception()
149+
133150
raise
134151
except (asyncio.CancelledError, ConnectionResetError):
135152
transaction.set_status(SPANSTATUS.CANCELLED)

tests/integrations/aiohttp/test_aiohttp.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
from aiohttp import web, ClientSession
88
from aiohttp.client import ServerDisconnectedError
99
from aiohttp.web_request import Request
10+
from aiohttp.web_exceptions import (
11+
HTTPInternalServerError,
12+
HTTPNetworkAuthenticationRequired,
13+
HTTPBadRequest,
14+
HTTPNotFound,
15+
HTTPUnavailableForLegalReasons,
16+
)
1017

1118
from sentry_sdk import capture_message, start_transaction
1219
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
@@ -617,3 +624,118 @@ async def handler(_):
617624
# Important to note that the ServerDisconnectedError indicates we have no error server-side.
618625
with pytest.raises(ServerDisconnectedError):
619626
await client.get("/")
627+
628+
629+
@pytest.mark.parametrize(
630+
("integration_kwargs", "exception_to_raise", "should_capture"),
631+
(
632+
({}, None, False),
633+
({}, HTTPBadRequest, False),
634+
(
635+
{},
636+
HTTPUnavailableForLegalReasons(None),
637+
False,
638+
), # Highest 4xx status code (451)
639+
({}, HTTPInternalServerError, True),
640+
({}, HTTPNetworkAuthenticationRequired, True), # Highest 5xx status code (511)
641+
({"failed_request_status_codes": set()}, HTTPInternalServerError, False),
642+
(
643+
{"failed_request_status_codes": set()},
644+
HTTPNetworkAuthenticationRequired,
645+
False,
646+
),
647+
({"failed_request_status_codes": {404, *range(500, 600)}}, HTTPNotFound, True),
648+
(
649+
{"failed_request_status_codes": {404, *range(500, 600)}},
650+
HTTPInternalServerError,
651+
True,
652+
),
653+
(
654+
{"failed_request_status_codes": {404, *range(500, 600)}},
655+
HTTPBadRequest,
656+
False,
657+
),
658+
),
659+
)
660+
@pytest.mark.asyncio
661+
async def test_failed_request_status_codes(
662+
sentry_init,
663+
aiohttp_client,
664+
capture_events,
665+
integration_kwargs,
666+
exception_to_raise,
667+
should_capture,
668+
):
669+
sentry_init(integrations=[AioHttpIntegration(**integration_kwargs)])
670+
events = capture_events()
671+
672+
async def handle(_):
673+
if exception_to_raise is not None:
674+
raise exception_to_raise
675+
else:
676+
return web.Response(status=200)
677+
678+
app = web.Application()
679+
app.router.add_get("/", handle)
680+
681+
client = await aiohttp_client(app)
682+
resp = await client.get("/")
683+
684+
expected_status = (
685+
200 if exception_to_raise is None else exception_to_raise.status_code
686+
)
687+
assert resp.status == expected_status
688+
689+
if should_capture:
690+
(event,) = events
691+
assert event["exception"]["values"][0]["type"] == exception_to_raise.__name__
692+
else:
693+
assert not events
694+
695+
696+
@pytest.mark.asyncio
697+
async def test_failed_request_status_codes_with_returned_status(
698+
sentry_init, aiohttp_client, capture_events
699+
):
700+
"""
701+
Returning a web.Response with a failed_request_status_code should not be reported to Sentry.
702+
"""
703+
sentry_init(integrations=[AioHttpIntegration(failed_request_status_codes={500})])
704+
events = capture_events()
705+
706+
async def handle(_):
707+
return web.Response(status=500)
708+
709+
app = web.Application()
710+
app.router.add_get("/", handle)
711+
712+
client = await aiohttp_client(app)
713+
resp = await client.get("/")
714+
715+
assert resp.status == 500
716+
assert not events
717+
718+
719+
@pytest.mark.asyncio
720+
async def test_failed_request_status_codes_non_http_exception(
721+
sentry_init, aiohttp_client, capture_events
722+
):
723+
"""
724+
If an exception, which is not an instance of HTTPException, is raised, it should be captured, even if
725+
failed_request_status_codes is empty.
726+
"""
727+
sentry_init(integrations=[AioHttpIntegration(failed_request_status_codes=set())])
728+
events = capture_events()
729+
730+
async def handle(_):
731+
1 / 0
732+
733+
app = web.Application()
734+
app.router.add_get("/", handle)
735+
736+
client = await aiohttp_client(app)
737+
resp = await client.get("/")
738+
assert resp.status == 500
739+
740+
(event,) = events
741+
assert event["exception"]["values"][0]["type"] == "ZeroDivisionError"

0 commit comments

Comments
 (0)