Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()`.
Expand Down Expand Up @@ -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
Expand Down
37 changes: 34 additions & 3 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Span Recorder Limit Causes Inaccurate Lost Span Count

When a transaction is discarded because its HTTP status code is ignored, the num_spans calculation for recording lost span events doesn't include spans that were dropped due to the span recorder limit. This results in an undercount of lost span events.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They were not previously recorded by record_lost_event, so to me this seems like a separate issue.

I believe the data in ClientReport is best-effort.

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.
Expand Down Expand Up @@ -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):
Expand Down
139 changes: 139 additions & 0 deletions tests/tracing/test_ignore_status_codes.py
Original file line number Diff line number Diff line change
@@ -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),
]
)