Skip to content

Commit 7e4992a

Browse files
feat(aiohttp): Add failed_request_status_codes (#3551)
`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 8060a64 commit 7e4992a

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
@@ -48,6 +48,8 @@
4848
from aiohttp.web_request import Request
4949
from aiohttp.web_urldispatcher import UrlMappingMatchInfo
5050
from aiohttp import TraceRequestStartParams, TraceRequestEndParams
51+
52+
from collections.abc import Set
5153
from types import SimpleNamespace
5254
from typing import Any
5355
from typing import Optional
@@ -59,20 +61,27 @@
5961

6062

6163
TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
64+
DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600))
6265

6366

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

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

7786
@staticmethod
7887
def setup_once():
@@ -100,7 +109,8 @@ def setup_once():
100109

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

106116
weak_request = weakref.ref(request)
@@ -131,6 +141,13 @@ async def sentry_app_handle(self, request, *args, **kwargs):
131141
response = await old_handle(self, request)
132142
except HTTPException as e:
133143
transaction.set_http_status(e.status_code)
144+
145+
if (
146+
e.status_code
147+
in integration._failed_request_status_codes
148+
):
149+
_capture_exception()
150+
134151
raise
135152
except (asyncio.CancelledError, ConnectionResetError):
136153
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)