From c7f763ab7dcf646abc603055cd8451aa74789b53 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 23 Apr 2024 14:55:22 +0200 Subject: [PATCH 1/7] feat(integrations): Allow to configure status codes --- sentry_sdk/_types.py | 4 +++- sentry_sdk/integrations/_wsgi_common.py | 19 +++++++++++++++++-- sentry_sdk/integrations/starlette.py | 19 +++++++++++++++---- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 1577dbde4f..6a51cc0cb9 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: - from collections.abc import MutableMapping + from collections.abc import MutableMapping, Sequence from datetime import datetime @@ -211,3 +211,5 @@ }, total=False, ) + + HttpStatusCodeRange = Union[int, Sequence[int]] diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 6e6705a7d3..9e40763091 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -3,7 +3,7 @@ import sentry_sdk from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.utils import AnnotatedValue +from sentry_sdk.utils import AnnotatedValue, logger from sentry_sdk._types import TYPE_CHECKING try: @@ -18,7 +18,7 @@ from typing import Mapping from typing import Optional from typing import Union - from sentry_sdk._types import Event + from sentry_sdk._types import Event, HttpStatusCodeRange SENSITIVE_ENV_KEYS = ( @@ -200,3 +200,18 @@ def _filter_headers(headers): ) for k, v in headers.items() } + + +def _in_http_status_code_range(code, code_range): + # type: (int, HttpStatusCodeRange) -> bool + for target in code_range: + if isinstance(target, int): + if code == target: + return True + continue + + try: + if code in target: + return True + except TypeError: + logger.warning("TODO") diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index eafc82f6ed..8783a84934 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -7,6 +7,7 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations._wsgi_common import ( + _in_http_status_code_range, _is_json_content_type, request_body_within_bounds, ) @@ -31,7 +32,7 @@ if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Dict, Optional, Tuple - from sentry_sdk._types import Event + from sentry_sdk._types import Event, HttpStatusCodeRange try: import starlette # type: ignore @@ -71,15 +72,19 @@ class StarletteIntegration(Integration): identifier = "starlette" transaction_style = "" + failed_request_status_codes = [range(500, 599)] - def __init__(self, transaction_style="url"): - # type: (str) -> None + def __init__(self, transaction_style="url", failed_request_status_codes=None): + # type: (str, list[HttpStatusCodeRange]) -> None if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( "Invalid value for transaction_style: %s (must be in %s)" % (transaction_style, TRANSACTION_STYLE_VALUES) ) self.transaction_style = transaction_style + self.failed_request_status_codes = failed_request_status_codes or [ + range(500, 599) + ] @staticmethod def setup_once(): @@ -199,12 +204,18 @@ def _sentry_middleware_init(self, *args, **kwargs): async def _sentry_patched_exception_handler(self, *args, **kwargs): # type: (Any, Any, Any) -> None + integration = sentry_sdk.get_client().get_integration( + StarletteIntegration + ) + exp = args[0] is_http_server_error = ( hasattr(exp, "status_code") and isinstance(exp.status_code, int) - and exp.status_code >= 500 + and _in_http_status_code_range( + exp.status_code, integration.failed_request_status_codes + ) ) if is_http_server_error: _capture_exception(exp, handled=True) From 6d2d84475930dfd1d0b4d2ac879fe24794094826 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 23 Apr 2024 17:45:22 +0200 Subject: [PATCH 2/7] more stuff --- sentry_sdk/integrations/_wsgi_common.py | 6 +- tests/integrations/fastapi/test_fastapi.py | 54 +++++++++++++- .../integrations/starlette/test_starlette.py | 71 ++++++++++++++++--- 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 9e40763091..5cc35a9c93 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -202,9 +202,9 @@ def _filter_headers(headers): } -def _in_http_status_code_range(code, code_range): - # type: (int, HttpStatusCodeRange) -> bool - for target in code_range: +def _in_http_status_code_range(code, code_ranges): + # type: (int, list[HttpStatusCodeRange]) -> bool + for target in code_ranges: if isinstance(target, int): if code == target: return True diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 00f693fd8c..428ee77654 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -4,7 +4,7 @@ from unittest import mock import pytest -from fastapi import FastAPI, Request +from fastapi import FastAPI, HTTPException, Request from fastapi.testclient import TestClient from fastapi.middleware.trustedhost import TrustedHostMiddleware @@ -501,3 +501,55 @@ def test_transaction_name_in_middleware( assert ( transaction_event["transaction_info"]["source"] == expected_transaction_source ) + + +@pytest.mark.parametrize( + "failed_request_status_codes,status_code,expected_error", + [ + (None, 500, True), + (None, 400, False), + ([500, 501], 500, True), + ([500, 501], 401, False), + ([range(400, 499)], 401, True), + ([range(400, 499)], 500, False), + ([range(400, 499), range(500, 599)], 300, False), + ([range(400, 499), range(500, 599)], 403, True), + ([range(400, 499), range(500, 599)], 503, True), + ([range(400, 403), 500, 501], 401, True), + ([range(400, 403), 500, 501], 405, False), + ([range(400, 403), 500, 501], 501, True), + ([range(400, 403), 500, 501], 503, False), + ([None], 500, False), + ], +) +def test_configurable_status_codes( + sentry_init, + capture_events, + failed_request_status_codes, + status_code, + expected_error, +): + sentry_init( + integrations=[ + StarletteIntegration( + failed_request_status_codes=failed_request_status_codes + ), + FastApiIntegration(failed_request_status_codes=failed_request_status_codes), + ] + ) + + events = capture_events() + + app = FastAPI() + + @app.get("/error") + async def _error(): + raise HTTPException(status_code) + + client = TestClient(app) + client.get("/error") + + if expected_error: + assert len(events) == 1 + else: + assert not events diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index e1f3c1a482..9e58daf567 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -25,6 +25,7 @@ AuthenticationError, SimpleUser, ) +from starlette.exceptions import HTTPException from starlette.middleware import Middleware from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.trustedhost import TrustedHostMiddleware @@ -258,7 +259,7 @@ async def my_send(*args, **kwargs): @pytest.mark.asyncio -async def test_starlettrequestextractor_content_length(sentry_init): +async def test_starletterequestextractor_content_length(sentry_init): scope = SCOPE.copy() scope["headers"] = [ [b"content-length", str(len(json.dumps(BODY_JSON))).encode()], @@ -270,7 +271,7 @@ async def test_starlettrequestextractor_content_length(sentry_init): @pytest.mark.asyncio -async def test_starlettrequestextractor_cookies(sentry_init): +async def test_starletterequestextractor_cookies(sentry_init): starlette_request = starlette.requests.Request(SCOPE) extractor = StarletteRequestExtractor(starlette_request) @@ -281,7 +282,7 @@ async def test_starlettrequestextractor_cookies(sentry_init): @pytest.mark.asyncio -async def test_starlettrequestextractor_json(sentry_init): +async def test_starletterequestextractor_json(sentry_init): starlette_request = starlette.requests.Request(SCOPE) # Mocking async `_receive()` that works in Python 3.7+ @@ -295,7 +296,7 @@ async def test_starlettrequestextractor_json(sentry_init): @pytest.mark.asyncio -async def test_starlettrequestextractor_form(sentry_init): +async def test_starletterequestextractor_form(sentry_init): scope = SCOPE.copy() scope["headers"] = [ [b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"], @@ -323,7 +324,7 @@ async def test_starlettrequestextractor_form(sentry_init): @pytest.mark.asyncio -async def test_starlettrequestextractor_body_consumed_twice( +async def test_starletterequestextractor_body_consumed_twice( sentry_init, capture_events ): """ @@ -361,7 +362,7 @@ async def test_starlettrequestextractor_body_consumed_twice( @pytest.mark.asyncio -async def test_starlettrequestextractor_extract_request_info_too_big(sentry_init): +async def test_starletterequestextractor_extract_request_info_too_big(sentry_init): sentry_init( send_default_pii=True, integrations=[StarletteIntegration()], @@ -392,7 +393,7 @@ async def test_starlettrequestextractor_extract_request_info_too_big(sentry_init @pytest.mark.asyncio -async def test_starlettrequestextractor_extract_request_info(sentry_init): +async def test_starletterequestextractor_extract_request_info(sentry_init): sentry_init( send_default_pii=True, integrations=[StarletteIntegration()], @@ -423,7 +424,7 @@ async def test_starlettrequestextractor_extract_request_info(sentry_init): @pytest.mark.asyncio -async def test_starlettrequestextractor_extract_request_info_no_pii(sentry_init): +async def test_starletterequestextractor_extract_request_info_no_pii(sentry_init): sentry_init( send_default_pii=False, integrations=[StarletteIntegration()], @@ -1078,3 +1079,57 @@ def test_transaction_name_in_middleware( assert ( transaction_event["transaction_info"]["source"] == expected_transaction_source ) + + +@pytest.mark.parametrize( + "failed_request_status_codes,status_code,expected_error", + [ + (None, 500, True), + (None, 400, False), + ([500, 501], 500, True), + ([500, 501], 401, False), + ([range(400, 499)], 401, True), + ([range(400, 499)], 500, False), + ([range(400, 499), range(500, 599)], 300, False), + ([range(400, 499), range(500, 599)], 403, True), + ([range(400, 499), range(500, 599)], 503, True), + ([range(400, 403), 500, 501], 401, True), + ([range(400, 403), 500, 501], 405, False), + ([range(400, 403), 500, 501], 501, True), + ([range(400, 403), 500, 501], 503, False), + ([None], 500, False), + ], +) +def test_configurable_status_codes( + sentry_init, + capture_events, + failed_request_status_codes, + status_code, + expected_error, +): + sentry_init( + integrations=[ + StarletteIntegration( + failed_request_status_codes=failed_request_status_codes + ) + ] + ) + + events = capture_events() + + async def _error(request): + raise HTTPException(status_code) + + app = starlette.applications.Starlette( + routes=[ + starlette.routing.Route("/error", _error, methods=["GET"]), + ], + ) + + client = TestClient(app) + client.get("/error") + + if expected_error: + assert len(events) == 1 + else: + assert not events From 3211ff7fd010caa952066ddeb74344713d74d1e5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Thu, 16 May 2024 15:16:11 +0200 Subject: [PATCH 3/7] fix type --- sentry_sdk/_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index e4cd991795..2aa9588a3d 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: - from collections.abc import MutableMapping, Sequence + from collections.abc import Container, MutableMapping from datetime import datetime @@ -221,4 +221,4 @@ total=False, ) - HttpStatusCodeRange = Union[int, Sequence[int]] + HttpStatusCodeRange = Union[int, Container[int]] From b28b89ad4abfaf5c6ef77c220b23b6180c921954 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Thu, 16 May 2024 15:19:03 +0200 Subject: [PATCH 4/7] add missing return --- sentry_sdk/integrations/_wsgi_common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 5cc35a9c93..30c7eeec7a 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -215,3 +215,5 @@ def _in_http_status_code_range(code, code_ranges): return True except TypeError: logger.warning("TODO") + + return False From 50ba6a382ba767db56ac9d587dd5eab80a1991e9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 4 Jun 2024 16:04:03 +0200 Subject: [PATCH 5/7] fix type --- sentry_sdk/integrations/starlette.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 062a49f4fa..66d1fa2421 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -74,7 +74,7 @@ class StarletteIntegration(Integration): failed_request_status_codes = [range(500, 599)] def __init__(self, transaction_style="url", failed_request_status_codes=None): - # type: (str, list[HttpStatusCodeRange]) -> None + # type: (str, Optional[list[HttpStatusCodeRange]]) -> None if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( "Invalid value for transaction_style: %s (must be in %s)" From 6ccfae222b179d151fa2c7226193fcdbe76ca716 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 4 Jun 2024 16:32:10 +0200 Subject: [PATCH 6/7] remove duplication --- sentry_sdk/integrations/starlette.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 66d1fa2421..ac55f8058f 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -71,7 +71,6 @@ class StarletteIntegration(Integration): identifier = "starlette" transaction_style = "" - failed_request_status_codes = [range(500, 599)] def __init__(self, transaction_style="url", failed_request_status_codes=None): # type: (str, Optional[list[HttpStatusCodeRange]]) -> None From 1cfadb7bcf56beb9c96c83552ed7555157c68b38 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 5 Jun 2024 11:27:03 +0200 Subject: [PATCH 7/7] error message --- sentry_sdk/integrations/_wsgi_common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 30c7eeec7a..b94b721622 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -214,6 +214,8 @@ def _in_http_status_code_range(code, code_ranges): if code in target: return True except TypeError: - logger.warning("TODO") + logger.warning( + "failed_request_status_codes has to be a list of integers or containers" + ) return False