Skip to content

Commit 6489fa0

Browse files
feat(starlette): Support new failed_request_status_codes (getsentry#3563)
Add support for passing `failed_request_status_codes` to the `StarletteIntegration` and `FastApiIntegration` constructors as a `Set[int]`, while maintaining backwards-compatibility with the old format.
1 parent 3995132 commit 6489fa0

File tree

6 files changed

+189
-35
lines changed

6 files changed

+189
-35
lines changed

sentry_sdk/integrations/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from typing import Type
1717

1818

19+
_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600))
20+
21+
1922
_installer_lock = Lock()
2023

2124
# Set of all integration identifiers we have attempted to install

sentry_sdk/integrations/_wsgi_common.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def _filter_headers(headers):
210210

211211

212212
def _in_http_status_code_range(code, code_ranges):
213-
# type: (int, list[HttpStatusCodeRange]) -> bool
213+
# type: (object, list[HttpStatusCodeRange]) -> bool
214214
for target in code_ranges:
215215
if isinstance(target, int):
216216
if code == target:
@@ -226,3 +226,18 @@ def _in_http_status_code_range(code, code_ranges):
226226
)
227227

228228
return False
229+
230+
231+
class HttpCodeRangeContainer:
232+
"""
233+
Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int].
234+
Used for backwards compatibility with the old `failed_request_status_codes` option.
235+
"""
236+
237+
def __init__(self, code_ranges):
238+
# type: (list[HttpStatusCodeRange]) -> None
239+
self._code_ranges = code_ranges
240+
241+
def __contains__(self, item):
242+
# type: (object) -> bool
243+
return _in_http_status_code_range(item, self._code_ranges)

sentry_sdk/integrations/aiohttp.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
import sentry_sdk
66
from sentry_sdk.api import continue_trace
77
from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA
8-
from sentry_sdk.integrations import Integration, DidNotEnable
8+
from sentry_sdk.integrations import (
9+
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
10+
Integration,
11+
DidNotEnable,
12+
)
913
from sentry_sdk.integrations.logging import ignore_logger
1014
from sentry_sdk.sessions import track_session
1115
from sentry_sdk.integrations._wsgi_common import (
@@ -61,7 +65,6 @@
6165

6266

6367
TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
64-
_DEFAULT_FAILED_REQUEST_STATUS_CODES = frozenset(range(500, 600))
6568

6669

6770
class AioHttpIntegration(Integration):

sentry_sdk/integrations/starlette.py

+33-15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import asyncio
22
import functools
3+
import warnings
4+
from collections.abc import Set
35
from copy import deepcopy
46

57
import sentry_sdk
68
from sentry_sdk.consts import OP
7-
from sentry_sdk.integrations import DidNotEnable, Integration
9+
from sentry_sdk.integrations import (
10+
DidNotEnable,
11+
Integration,
12+
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
13+
)
814
from sentry_sdk.integrations._wsgi_common import (
9-
_in_http_status_code_range,
15+
HttpCodeRangeContainer,
1016
_is_json_content_type,
1117
request_body_within_bounds,
1218
)
@@ -30,7 +36,7 @@
3036
from typing import TYPE_CHECKING
3137

3238
if TYPE_CHECKING:
33-
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple
39+
from typing import Any, Awaitable, Callable, Container, Dict, Optional, Tuple, Union
3440

3541
from sentry_sdk._types import Event, HttpStatusCodeRange
3642

@@ -76,23 +82,37 @@ class StarletteIntegration(Integration):
7682

7783
def __init__(
7884
self,
79-
transaction_style="url",
80-
failed_request_status_codes=None,
81-
middleware_spans=True,
85+
transaction_style="url", # type: str
86+
failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Union[Set[int], list[HttpStatusCodeRange], None]
87+
middleware_spans=True, # type: bool
8288
):
83-
# type: (str, Optional[list[HttpStatusCodeRange]], bool) -> None
89+
# type: (...) -> None
8490
if transaction_style not in TRANSACTION_STYLE_VALUES:
8591
raise ValueError(
8692
"Invalid value for transaction_style: %s (must be in %s)"
8793
% (transaction_style, TRANSACTION_STYLE_VALUES)
8894
)
8995
self.transaction_style = transaction_style
9096
self.middleware_spans = middleware_spans
91-
self.failed_request_status_codes = (
92-
[range(500, 599)]
93-
if failed_request_status_codes is None
94-
else failed_request_status_codes
95-
) # type: list[HttpStatusCodeRange]
97+
98+
if isinstance(failed_request_status_codes, Set):
99+
self.failed_request_status_codes = (
100+
failed_request_status_codes
101+
) # type: Container[int]
102+
else:
103+
warnings.warn(
104+
"Passing a list or None for failed_request_status_codes is deprecated. "
105+
"Please pass a set of int instead.",
106+
DeprecationWarning,
107+
stacklevel=2,
108+
)
109+
110+
if failed_request_status_codes is None:
111+
self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES
112+
else:
113+
self.failed_request_status_codes = HttpCodeRangeContainer(
114+
failed_request_status_codes
115+
)
96116

97117
@staticmethod
98118
def setup_once():
@@ -226,9 +246,7 @@ async def _sentry_patched_exception_handler(self, *args, **kwargs):
226246
is_http_server_error = (
227247
hasattr(exp, "status_code")
228248
and isinstance(exp.status_code, int)
229-
and _in_http_status_code_range(
230-
exp.status_code, integration.failed_request_status_codes
231-
)
249+
and exp.status_code in integration.failed_request_status_codes
232250
)
233251
if is_http_server_error:
234252
_capture_exception(exp, handled=True)

tests/integrations/fastapi/test_fastapi.py

+48-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
import threading
4+
import warnings
45
from unittest import mock
56

67
import pytest
@@ -505,20 +506,28 @@ def test_transaction_name_in_middleware(
505506
)
506507

507508

508-
@test_starlette.parametrize_test_configurable_status_codes
509-
def test_configurable_status_codes(
509+
@test_starlette.parametrize_test_configurable_status_codes_deprecated
510+
def test_configurable_status_codes_deprecated(
510511
sentry_init,
511512
capture_events,
512513
failed_request_status_codes,
513514
status_code,
514515
expected_error,
515516
):
517+
with pytest.warns(DeprecationWarning):
518+
starlette_integration = StarletteIntegration(
519+
failed_request_status_codes=failed_request_status_codes
520+
)
521+
522+
with pytest.warns(DeprecationWarning):
523+
fast_api_integration = FastApiIntegration(
524+
failed_request_status_codes=failed_request_status_codes
525+
)
526+
516527
sentry_init(
517528
integrations=[
518-
StarletteIntegration(
519-
failed_request_status_codes=failed_request_status_codes
520-
),
521-
FastApiIntegration(failed_request_status_codes=failed_request_status_codes),
529+
starlette_integration,
530+
fast_api_integration,
522531
]
523532
)
524533

@@ -537,3 +546,36 @@ async def _error():
537546
assert len(events) == 1
538547
else:
539548
assert not events
549+
550+
551+
@test_starlette.parametrize_test_configurable_status_codes
552+
def test_configurable_status_codes(
553+
sentry_init,
554+
capture_events,
555+
failed_request_status_codes,
556+
status_code,
557+
expected_error,
558+
):
559+
integration_kwargs = {}
560+
if failed_request_status_codes is not None:
561+
integration_kwargs["failed_request_status_codes"] = failed_request_status_codes
562+
563+
with warnings.catch_warnings():
564+
warnings.simplefilter("error", DeprecationWarning)
565+
starlette_integration = StarletteIntegration(**integration_kwargs)
566+
fastapi_integration = FastApiIntegration(**integration_kwargs)
567+
568+
sentry_init(integrations=[starlette_integration, fastapi_integration])
569+
570+
events = capture_events()
571+
572+
app = FastAPI()
573+
574+
@app.get("/error")
575+
async def _error():
576+
raise HTTPException(status_code)
577+
578+
client = TestClient(app)
579+
client.get("/error")
580+
581+
assert len(events) == int(expected_error)

tests/integrations/starlette/test_starlette.py

+84-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import re
88
import threading
9+
import warnings
910
from unittest import mock
1011

1112
import pytest
@@ -1133,7 +1134,22 @@ def test_span_origin(sentry_init, capture_events):
11331134
assert span["origin"] == "auto.http.starlette"
11341135

11351136

1136-
parametrize_test_configurable_status_codes = pytest.mark.parametrize(
1137+
class NonIterableContainer:
1138+
"""Wraps any container and makes it non-iterable.
1139+
1140+
Used to test backwards compatibility with our old way of defining failed_request_status_codes, which allowed
1141+
passing in a list of (possibly non-iterable) containers. The Python standard library does not provide any built-in
1142+
non-iterable containers, so we have to define our own.
1143+
"""
1144+
1145+
def __init__(self, inner):
1146+
self.inner = inner
1147+
1148+
def __contains__(self, item):
1149+
return item in self.inner
1150+
1151+
1152+
parametrize_test_configurable_status_codes_deprecated = pytest.mark.parametrize(
11371153
"failed_request_status_codes,status_code,expected_error",
11381154
[
11391155
(None, 500, True),
@@ -1150,28 +1166,29 @@ def test_span_origin(sentry_init, capture_events):
11501166
([range(400, 403), 500, 501], 501, True),
11511167
([range(400, 403), 500, 501], 503, False),
11521168
([], 500, False),
1169+
([NonIterableContainer(range(500, 600))], 500, True),
1170+
([NonIterableContainer(range(500, 600))], 404, False),
11531171
],
11541172
)
1155-
"""Test cases for configurable status codes.
1173+
"""Test cases for configurable status codes (deprecated API).
11561174
Also used by the FastAPI tests.
11571175
"""
11581176

11591177

1160-
@parametrize_test_configurable_status_codes
1161-
def test_configurable_status_codes(
1178+
@parametrize_test_configurable_status_codes_deprecated
1179+
def test_configurable_status_codes_deprecated(
11621180
sentry_init,
11631181
capture_events,
11641182
failed_request_status_codes,
11651183
status_code,
11661184
expected_error,
11671185
):
1168-
sentry_init(
1169-
integrations=[
1170-
StarletteIntegration(
1171-
failed_request_status_codes=failed_request_status_codes
1172-
)
1173-
]
1174-
)
1186+
with pytest.warns(DeprecationWarning):
1187+
starlette_integration = StarletteIntegration(
1188+
failed_request_status_codes=failed_request_status_codes
1189+
)
1190+
1191+
sentry_init(integrations=[starlette_integration])
11751192

11761193
events = capture_events()
11771194

@@ -1191,3 +1208,59 @@ async def _error(request):
11911208
assert len(events) == 1
11921209
else:
11931210
assert not events
1211+
1212+
1213+
parametrize_test_configurable_status_codes = pytest.mark.parametrize(
1214+
("failed_request_status_codes", "status_code", "expected_error"),
1215+
(
1216+
(None, 500, True),
1217+
(None, 400, False),
1218+
({500, 501}, 500, True),
1219+
({500, 501}, 401, False),
1220+
({*range(400, 500)}, 401, True),
1221+
({*range(400, 500)}, 500, False),
1222+
({*range(400, 600)}, 300, False),
1223+
({*range(400, 600)}, 403, True),
1224+
({*range(400, 600)}, 503, True),
1225+
({*range(400, 403), 500, 501}, 401, True),
1226+
({*range(400, 403), 500, 501}, 405, False),
1227+
({*range(400, 403), 500, 501}, 501, True),
1228+
({*range(400, 403), 500, 501}, 503, False),
1229+
(set(), 500, False),
1230+
),
1231+
)
1232+
1233+
1234+
@parametrize_test_configurable_status_codes
1235+
def test_configurable_status_codes(
1236+
sentry_init,
1237+
capture_events,
1238+
failed_request_status_codes,
1239+
status_code,
1240+
expected_error,
1241+
):
1242+
integration_kwargs = {}
1243+
if failed_request_status_codes is not None:
1244+
integration_kwargs["failed_request_status_codes"] = failed_request_status_codes
1245+
1246+
with warnings.catch_warnings():
1247+
warnings.simplefilter("error", DeprecationWarning)
1248+
starlette_integration = StarletteIntegration(**integration_kwargs)
1249+
1250+
sentry_init(integrations=[starlette_integration])
1251+
1252+
events = capture_events()
1253+
1254+
async def _error(_):
1255+
raise HTTPException(status_code)
1256+
1257+
app = starlette.applications.Starlette(
1258+
routes=[
1259+
starlette.routing.Route("/error", _error, methods=["GET"]),
1260+
],
1261+
)
1262+
1263+
client = TestClient(app)
1264+
client.get("/error")
1265+
1266+
assert len(events) == int(expected_error)

0 commit comments

Comments
 (0)