diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 9e84dc3dd2..5bcc487037 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -40,6 +40,7 @@ class CompressionAlgo(Enum): from typing import Any from typing import Sequence from typing import Tuple + from typing import AbstractSet from typing_extensions import Literal from typing_extensions import TypedDict @@ -919,6 +920,7 @@ def __init__( max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int] enable_logs=False, # type: bool before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]] + trace_ignore_status_codes=frozenset(), # type: AbstractSet[int] ): # type: (...) -> None """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`. @@ -1307,6 +1309,14 @@ def __init__( function will be retained. If the function returns None, the log will not be sent to Sentry. + :param trace_ignore_status_codes: An optional property that disables tracing for + HTTP requests with certain status codes. + + Requests are not traced if the status code is contained in the provided set. + + If `trace_ignore_status_codes` is not provided, requests with any status code + may be traced. + :param _experiments: """ pass diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index a82b99ff4d..1697df1f22 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -30,6 +30,7 @@ from typing import Tuple from typing import Union from typing import TypeVar + from typing import Set from typing_extensions import TypedDict, Unpack @@ -970,6 +971,12 @@ def _get_scope_from_finish_args( return scope_or_hub + def _get_log_representation(self): + # type: () -> str + return "{op}transaction <{name}>".format( + op=("<" + self.op + "> " if self.op else ""), name=self.name + ) + def finish( self, scope=None, # type: Optional[sentry_sdk.Scope] @@ -1039,6 +1046,32 @@ def finish( super().finish(scope, end_timestamp) + status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) + if ( + status_code is not None + and status_code in client.options["trace_ignore_status_codes"] + ): + logger.debug( + "[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format( + transaction_description=self._get_log_representation(), + status_code=self._data[SPANDATA.HTTP_STATUS_CODE], + trace_ignore_status_codes=client.options[ + "trace_ignore_status_codes" + ], + ) + ) + if client.transport: + client.transport.record_lost_event( + "event_processor", data_category="transaction" + ) + + num_spans = len(self._span_recorder.spans) + 1 + client.transport.record_lost_event( + "event_processor", data_category="span", quantity=num_spans + ) + + self.sampled = False + if not self.sampled: # At this point a `sampled = None` should have already been resolved # to a concrete decision. @@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context): """ client = sentry_sdk.get_client() - transaction_description = "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) + transaction_description = self._get_log_representation() # nothing to do if tracing is disabled if not has_tracing_enabled(client.options): diff --git a/tests/tracing/test_ignore_status_codes.py b/tests/tracing/test_ignore_status_codes.py new file mode 100644 index 0000000000..b2899e0ad9 --- /dev/null +++ b/tests/tracing/test_ignore_status_codes.py @@ -0,0 +1,139 @@ +import sentry_sdk +from sentry_sdk import start_transaction, start_span + +import pytest + +from collections import Counter + + +def test_no_ignored_codes(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(op="http", name="GET /"): + span_or_tx = sentry_sdk.get_current_span() + span_or_tx.set_data("http.response.status_code", 404) + + assert len(events) == 1 + + +@pytest.mark.parametrize("status_code", [200, 404]) +def test_single_code_ignored(sentry_init, capture_events, status_code): + sentry_init( + traces_sample_rate=1.0, + trace_ignore_status_codes={ + 404, + }, + ) + events = capture_events() + + with start_transaction(op="http", name="GET /"): + span_or_tx = sentry_sdk.get_current_span() + span_or_tx.set_data("http.response.status_code", status_code) + + if status_code == 404: + assert not events + else: + assert len(events) == 1 + + +@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404]) +def test_range_ignored(sentry_init, capture_events, status_code): + sentry_init( + traces_sample_rate=1.0, + trace_ignore_status_codes=set( + range( + 305, + 400, + ), + ), + ) + events = capture_events() + + with start_transaction(op="http", name="GET /"): + span_or_tx = sentry_sdk.get_current_span() + span_or_tx.set_data("http.response.status_code", status_code) + + if 305 <= status_code <= 399: + assert not events + else: + assert len(events) == 1 + + +@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404]) +def test_variety_ignored(sentry_init, capture_events, status_code): + sentry_init( + traces_sample_rate=1.0, + trace_ignore_status_codes={ + 301, + 302, + 303, + *range( + 305, + 400, + ), + *range( + 401, + 405, + ), + }, + ) + events = capture_events() + + with start_transaction(op="http", name="GET /"): + span_or_tx = sentry_sdk.get_current_span() + span_or_tx.set_data("http.response.status_code", status_code) + + if ( + 301 <= status_code <= 303 + or 305 <= status_code <= 399 + or 401 <= status_code <= 404 + ): + assert not events + else: + assert len(events) == 1 + + +def test_transaction_not_ignored_when_status_code_has_invalid_type( + sentry_init, capture_events +): + sentry_init( + traces_sample_rate=1.0, + trace_ignore_status_codes=set( + range(401, 404), + ), + ) + events = capture_events() + + with start_transaction(op="http", name="GET /"): + span_or_tx = sentry_sdk.get_current_span() + span_or_tx.set_data("http.response.status_code", "404") + + assert len(events) == 1 + + +def test_records_lost_events(sentry_init, capture_record_lost_event_calls): + sentry_init( + traces_sample_rate=1.0, + trace_ignore_status_codes={ + 404, + }, + ) + record_lost_event_calls = capture_record_lost_event_calls() + + with start_transaction(op="http", name="GET /"): + span_or_tx = sentry_sdk.get_current_span() + span_or_tx.set_data("http.response.status_code", 404) + + with start_span(op="child-span"): + with start_span(op="child-child-span"): + pass + + assert Counter(record_lost_event_calls) == Counter( + [ + ("event_processor", "transaction", None, 1), + ("event_processor", "span", None, 3), + ] + )