diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38d960885e..7ece9440b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: 3.12 @@ -39,7 +39,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: 3.12 @@ -54,7 +54,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: 3.12 @@ -82,7 +82,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: 3.12 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c3a36dc124..6cd6a8d8b7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47bc4de03d..05fdb344aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest name: "Release a new version" steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 with: token: ${{ secrets.GH_RELEASE_PAT }} fetch-depth: 0 diff --git a/.github/workflows/test-integrations-aws-lambda.yml b/.github/workflows/test-integrations-aws-lambda.yml index 773f41247b..43765b9a11 100644 --- a/.github/workflows/test-integrations-aws-lambda.yml +++ b/.github/workflows/test-integrations-aws-lambda.yml @@ -65,7 +65,7 @@ jobs: os: [ubuntu-20.04] needs: check-permissions steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - uses: actions/setup-python@v5 diff --git a/.github/workflows/test-integrations-cloud-computing.yml b/.github/workflows/test-integrations-cloud-computing.yml index 049b37d211..957b2b23b4 100644 --- a/.github/workflows/test-integrations-cloud-computing.yml +++ b/.github/workflows/test-integrations-cloud-computing.yml @@ -32,7 +32,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -80,7 +80,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index c046190e1e..28c23edb8a 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -32,7 +32,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-data-processing.yml b/.github/workflows/test-integrations-data-processing.yml index 25a1f7d709..c40261938b 100644 --- a/.github/workflows/test-integrations-data-processing.yml +++ b/.github/workflows/test-integrations-data-processing.yml @@ -32,7 +32,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -104,7 +104,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-databases.yml b/.github/workflows/test-integrations-databases.yml index 5683bfbd95..7e4c24dc20 100644 --- a/.github/workflows/test-integrations-databases.yml +++ b/.github/workflows/test-integrations-databases.yml @@ -50,7 +50,7 @@ jobs: SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -125,7 +125,7 @@ jobs: SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 2a00071382..ae148bc21d 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -32,7 +32,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -80,7 +80,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-miscellaneous.yml b/.github/workflows/test-integrations-miscellaneous.yml index b8c8e0a3a0..f56e5004a5 100644 --- a/.github/workflows/test-integrations-miscellaneous.yml +++ b/.github/workflows/test-integrations-miscellaneous.yml @@ -32,7 +32,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -80,7 +80,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-networking.yml b/.github/workflows/test-integrations-networking.yml index 18dfd72c34..1c63222ca9 100644 --- a/.github/workflows/test-integrations-networking.yml +++ b/.github/workflows/test-integrations-networking.yml @@ -32,7 +32,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -80,7 +80,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-web-frameworks-1.yml b/.github/workflows/test-integrations-web-frameworks-1.yml index 861c36b485..757ebf5fb5 100644 --- a/.github/workflows/test-integrations-web-frameworks-1.yml +++ b/.github/workflows/test-integrations-web-frameworks-1.yml @@ -50,7 +50,7 @@ jobs: SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -116,7 +116,7 @@ jobs: SENTRY_PYTHON_TEST_POSTGRES_USER: postgres SENTRY_PYTHON_TEST_POSTGRES_PASSWORD: sentry steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/test-integrations-web-frameworks-2.yml b/.github/workflows/test-integrations-web-frameworks-2.yml index 0d86487900..fa383e97cd 100644 --- a/.github/workflows/test-integrations-web-frameworks-2.yml +++ b/.github/workflows/test-integrations-web-frameworks-2.yml @@ -32,7 +32,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -100,7 +100,7 @@ jobs: # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 os: [ubuntu-20.04] steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 89818e2c1d..458421865b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 2.5.0 + +### Various fixes & improvements + +- Allow to configure status codes to report to Sentry in Starlette and FastAPI (#3008) by @sentrivana + + By passing a new option to the FastAPI and Starlette integrations, you're now able to configure what + status codes should be sent as events to Sentry. Here's how it works: + + ```python + from sentry_sdk.integrations.starlette import StarletteIntegration + from sentry_sdk.integrations.fastapi import FastApiIntegration + + sentry_sdk.init( + # ... + integrations=[ + StarletteIntegration( + failed_request_status_codes=[403, range(500, 599)], + ), + FastApiIntegration( + failed_request_status_codes=[403, range(500, 599)], + ), + ] + ) + ``` + + `failed_request_status_codes` expects a list of integers or containers (objects that allow membership checks via `in`) + of integers. Examples of valid `failed_request_status_codes`: + + - `[500]` will only send events on HTTP 500. + - `[400, range(500, 599)]` will send events on HTTP 400 as well as the 500-599 range. + - `[500, 503]` will send events on HTTP 500 and 503. + + The default is `[range(500, 599)]`. + + See the [FastAPI](https://docs.sentry.io/platforms/python/integrations/fastapi/) and [Starlette](https://docs.sentry.io/platforms/python/integrations/starlette/) integration docs for more details. + +- Support multiple keys with `cache_prefixes` (#3136) by @sentrivana +- Support integer Redis keys (#3132) by @sentrivana +- Update SDK version in CONTRIBUTING.md (#3129) by @sentrivana +- Bump actions/checkout from 4.1.4 to 4.1.5 (#3067) by @dependabot + ## 2.4.0 ### Various fixes & improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05b642c502..f8cae4d549 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -163,12 +163,12 @@ This project follows [semver](https://semver.org/), with three additions: - Certain features (e.g. integrations) may be explicitly called out as "experimental" or "unstable" in the documentation. They come with their own versioning policy described in the documentation. -We recommend to pin your version requirements against `1.x.*` or `1.x.y`. +We recommend to pin your version requirements against `2.x.*` or `2.x.y`. Either one of the following is fine: ``` -sentry-sdk>=1.0.0,<2.0.0 -sentry-sdk==1.5.0 +sentry-sdk>=2.0.0,<3.0.0 +sentry-sdk==2.4.0 ``` A major release `N` implies the previous release `N-1` will no longer receive updates. We generally do not backport bugfixes to older versions unless they are security relevant. However, feel free to ask for backports of specific commits on the bugtracker. diff --git a/docs/conf.py b/docs/conf.py index d3fb1e90e4..c4937b7f18 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.4.0" +release = "2.5.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/scripts/split-tox-gh-actions/templates/test_group.jinja b/scripts/split-tox-gh-actions/templates/test_group.jinja index be06276e9f..66081a6bd1 100644 --- a/scripts/split-tox-gh-actions/templates/test_group.jinja +++ b/scripts/split-tox-gh-actions/templates/test_group.jinja @@ -39,7 +39,7 @@ {% endif %} steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 {% if needs_github_secrets %} {% raw %} with: diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 9f7546e81b..2aa9588a3d 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 Container, MutableMapping from datetime import datetime @@ -220,3 +220,5 @@ }, total=False, ) + + HttpStatusCodeRange = Union[int, Container[int]] diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index d03ccaac80..0ad05a7615 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -508,4 +508,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.4.0" +VERSION = "2.5.0" diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 6e6705a7d3..b94b721622 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,22 @@ def _filter_headers(headers): ) for k, v in headers.items() } + + +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 + continue + + try: + if code in target: + return True + except TypeError: + logger.warning( + "failed_request_status_codes has to be a list of integers or containers" + ) + + return False diff --git a/sentry_sdk/integrations/redis/modules/caches.py b/sentry_sdk/integrations/redis/modules/caches.py index 754b2118b8..8d3469d141 100644 --- a/sentry_sdk/integrations/redis/modules/caches.py +++ b/sentry_sdk/integrations/redis/modules/caches.py @@ -31,11 +31,15 @@ def _compile_cache_span_properties(redis_command, args, kwargs, integration): # type: (str, tuple[Any, ...], dict[str, Any], RedisIntegration) -> dict[str, Any] key = _get_safe_key(redis_command, args, kwargs) key_as_string = _key_as_string(key) + keys_as_string = key_as_string.split(", ") is_cache_key = False for prefix in integration.cache_prefixes: - if key_as_string.startswith(prefix): - is_cache_key = True + for kee in keys_as_string: + if kee.startswith(prefix): + is_cache_key = True + break + if is_cache_key: break value = None diff --git a/sentry_sdk/integrations/redis/utils.py b/sentry_sdk/integrations/redis/utils.py index 64b12395b6..43ea5b1572 100644 --- a/sentry_sdk/integrations/redis/utils.py +++ b/sentry_sdk/integrations/redis/utils.py @@ -52,7 +52,7 @@ def _safe_decode(key): except UnicodeDecodeError: return "" - return key + return str(key) def _key_as_string(key): diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index cb0f977d99..ac55f8058f 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, ) @@ -30,7 +31,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,14 +72,17 @@ class StarletteIntegration(Integration): transaction_style = "" - def __init__(self, transaction_style="url"): - # type: (str) -> None + def __init__(self, transaction_style="url", failed_request_status_codes=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)" % (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(): @@ -198,12 +202,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) diff --git a/setup.py b/setup.py index 21a1c60c72..56db3ca94c 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.4.0", + version="2.5.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", 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/redis/test_redis_cache_module.py b/tests/integrations/redis/test_redis_cache_module.py index ef25983abe..f118aa53f5 100644 --- a/tests/integrations/redis/test_redis_cache_module.py +++ b/tests/integrations/redis/test_redis_cache_module.py @@ -198,6 +198,43 @@ def test_cache_data(sentry_init, capture_events): assert spans[5]["op"] == "db.redis" # we ignore db spans in this test. +def test_cache_prefixes(sentry_init, capture_events): + sentry_init( + integrations=[ + RedisIntegration( + cache_prefixes=["yes"], + ), + ], + traces_sample_rate=1.0, + ) + events = capture_events() + + connection = FakeStrictRedis() + with sentry_sdk.start_transaction(): + connection.mget("yes", "no") + connection.mget("no", 1, "yes") + connection.mget("no", "yes.1", "yes.2") + connection.mget("no.1", "no.2", "no.3") + connection.mget("no.1", "no.2", "no.actually.yes") + connection.mget(b"no.3", b"yes.5") + connection.mget(uuid.uuid4().bytes) + connection.mget(uuid.uuid4().bytes, "yes") + + (event,) = events + + spans = event["spans"] + assert len(spans) == 13 # 8 db spans + 5 cache spans + + cache_spans = [span for span in spans if span["op"] == "cache.get"] + assert len(cache_spans) == 5 + + assert cache_spans[0]["description"] == "yes, no" + assert cache_spans[1]["description"] == "no, 1, yes" + assert cache_spans[2]["description"] == "no, yes.1, yes.2" + assert cache_spans[3]["description"] == "no.3, yes.5" + assert cache_spans[4]["description"] == ", yes" + + @pytest.mark.parametrize( "method_name,args,kwargs,expected_key", [ @@ -244,6 +281,12 @@ def test_cache_data(sentry_init, capture_events): None, (b"\x00c\x0f\xeaC\xe1L\x1c\xbff\xcb\xcc\xc1\xed\xc6\t",), ), + ( + "get", + [123], + None, + (123,), + ), ], ) def test_get_safe_key(method_name, args, kwargs, expected_key): @@ -266,6 +309,9 @@ def test_get_safe_key(method_name, args, kwargs, expected_key): ), (["bla", "blub", "foo"], "bla, blub, foo"), ([uuid.uuid4().bytes], ""), + ({"key1": 1, "key2": 2}, "key1, key2"), + (1, "1"), + ([1, 2, 3, b"hello"], "1, 2, 3, hello"), ], ) def test_key_as_string(key, expected_key): 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