Skip to content

Commit b838765

Browse files
feat: Option to not trace HTTP requests based on status codes (#4869)
Add `trace_ignore_status_codes` option to exclude HTTP requests from tracing if they have certain status codes. A debug-level log message is printed if a transaction is dropped because its status code is ignored. By default no status codes automatically cause transactions to be dropped. Closes #4812 --------- Co-authored-by: Anton Pirker <[email protected]>
1 parent 2872675 commit b838765

File tree

3 files changed

+183
-3
lines changed

3 files changed

+183
-3
lines changed

sentry_sdk/consts.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class CompressionAlgo(Enum):
4040
from typing import Any
4141
from typing import Sequence
4242
from typing import Tuple
43+
from typing import AbstractSet
4344
from typing_extensions import Literal
4445
from typing_extensions import TypedDict
4546

@@ -919,6 +920,7 @@ def __init__(
919920
max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
920921
enable_logs=False, # type: bool
921922
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
923+
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
922924
):
923925
# type: (...) -> None
924926
"""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__(
13071309
function will be retained. If the function returns None, the log will
13081310
not be sent to Sentry.
13091311
1312+
:param trace_ignore_status_codes: An optional property that disables tracing for
1313+
HTTP requests with certain status codes.
1314+
1315+
Requests are not traced if the status code is contained in the provided set.
1316+
1317+
If `trace_ignore_status_codes` is not provided, requests with any status code
1318+
may be traced.
1319+
13101320
:param _experiments:
13111321
"""
13121322
pass

sentry_sdk/tracing.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from typing import Tuple
3131
from typing import Union
3232
from typing import TypeVar
33+
from typing import Set
3334

3435
from typing_extensions import TypedDict, Unpack
3536

@@ -970,6 +971,12 @@ def _get_scope_from_finish_args(
970971

971972
return scope_or_hub
972973

974+
def _get_log_representation(self):
975+
# type: () -> str
976+
return "{op}transaction <{name}>".format(
977+
op=("<" + self.op + "> " if self.op else ""), name=self.name
978+
)
979+
973980
def finish(
974981
self,
975982
scope=None, # type: Optional[sentry_sdk.Scope]
@@ -1039,6 +1046,32 @@ def finish(
10391046

10401047
super().finish(scope, end_timestamp)
10411048

1049+
status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
1050+
if (
1051+
status_code is not None
1052+
and status_code in client.options["trace_ignore_status_codes"]
1053+
):
1054+
logger.debug(
1055+
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
1056+
transaction_description=self._get_log_representation(),
1057+
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
1058+
trace_ignore_status_codes=client.options[
1059+
"trace_ignore_status_codes"
1060+
],
1061+
)
1062+
)
1063+
if client.transport:
1064+
client.transport.record_lost_event(
1065+
"event_processor", data_category="transaction"
1066+
)
1067+
1068+
num_spans = len(self._span_recorder.spans) + 1
1069+
client.transport.record_lost_event(
1070+
"event_processor", data_category="span", quantity=num_spans
1071+
)
1072+
1073+
self.sampled = False
1074+
10421075
if not self.sampled:
10431076
# At this point a `sampled = None` should have already been resolved
10441077
# to a concrete decision.
@@ -1186,9 +1219,7 @@ def _set_initial_sampling_decision(self, sampling_context):
11861219
"""
11871220
client = sentry_sdk.get_client()
11881221

1189-
transaction_description = "{op}transaction <{name}>".format(
1190-
op=("<" + self.op + "> " if self.op else ""), name=self.name
1191-
)
1222+
transaction_description = self._get_log_representation()
11921223

11931224
# nothing to do if tracing is disabled
11941225
if not has_tracing_enabled(client.options):
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import sentry_sdk
2+
from sentry_sdk import start_transaction, start_span
3+
4+
import pytest
5+
6+
from collections import Counter
7+
8+
9+
def test_no_ignored_codes(sentry_init, capture_events):
10+
sentry_init(
11+
traces_sample_rate=1.0,
12+
)
13+
events = capture_events()
14+
15+
with start_transaction(op="http", name="GET /"):
16+
span_or_tx = sentry_sdk.get_current_span()
17+
span_or_tx.set_data("http.response.status_code", 404)
18+
19+
assert len(events) == 1
20+
21+
22+
@pytest.mark.parametrize("status_code", [200, 404])
23+
def test_single_code_ignored(sentry_init, capture_events, status_code):
24+
sentry_init(
25+
traces_sample_rate=1.0,
26+
trace_ignore_status_codes={
27+
404,
28+
},
29+
)
30+
events = capture_events()
31+
32+
with start_transaction(op="http", name="GET /"):
33+
span_or_tx = sentry_sdk.get_current_span()
34+
span_or_tx.set_data("http.response.status_code", status_code)
35+
36+
if status_code == 404:
37+
assert not events
38+
else:
39+
assert len(events) == 1
40+
41+
42+
@pytest.mark.parametrize("status_code", [200, 305, 307, 399, 404])
43+
def test_range_ignored(sentry_init, capture_events, status_code):
44+
sentry_init(
45+
traces_sample_rate=1.0,
46+
trace_ignore_status_codes=set(
47+
range(
48+
305,
49+
400,
50+
),
51+
),
52+
)
53+
events = capture_events()
54+
55+
with start_transaction(op="http", name="GET /"):
56+
span_or_tx = sentry_sdk.get_current_span()
57+
span_or_tx.set_data("http.response.status_code", status_code)
58+
59+
if 305 <= status_code <= 399:
60+
assert not events
61+
else:
62+
assert len(events) == 1
63+
64+
65+
@pytest.mark.parametrize("status_code", [200, 301, 303, 355, 404])
66+
def test_variety_ignored(sentry_init, capture_events, status_code):
67+
sentry_init(
68+
traces_sample_rate=1.0,
69+
trace_ignore_status_codes={
70+
301,
71+
302,
72+
303,
73+
*range(
74+
305,
75+
400,
76+
),
77+
*range(
78+
401,
79+
405,
80+
),
81+
},
82+
)
83+
events = capture_events()
84+
85+
with start_transaction(op="http", name="GET /"):
86+
span_or_tx = sentry_sdk.get_current_span()
87+
span_or_tx.set_data("http.response.status_code", status_code)
88+
89+
if (
90+
301 <= status_code <= 303
91+
or 305 <= status_code <= 399
92+
or 401 <= status_code <= 404
93+
):
94+
assert not events
95+
else:
96+
assert len(events) == 1
97+
98+
99+
def test_transaction_not_ignored_when_status_code_has_invalid_type(
100+
sentry_init, capture_events
101+
):
102+
sentry_init(
103+
traces_sample_rate=1.0,
104+
trace_ignore_status_codes=set(
105+
range(401, 404),
106+
),
107+
)
108+
events = capture_events()
109+
110+
with start_transaction(op="http", name="GET /"):
111+
span_or_tx = sentry_sdk.get_current_span()
112+
span_or_tx.set_data("http.response.status_code", "404")
113+
114+
assert len(events) == 1
115+
116+
117+
def test_records_lost_events(sentry_init, capture_record_lost_event_calls):
118+
sentry_init(
119+
traces_sample_rate=1.0,
120+
trace_ignore_status_codes={
121+
404,
122+
},
123+
)
124+
record_lost_event_calls = capture_record_lost_event_calls()
125+
126+
with start_transaction(op="http", name="GET /"):
127+
span_or_tx = sentry_sdk.get_current_span()
128+
span_or_tx.set_data("http.response.status_code", 404)
129+
130+
with start_span(op="child-span"):
131+
with start_span(op="child-child-span"):
132+
pass
133+
134+
assert Counter(record_lost_event_calls) == Counter(
135+
[
136+
("event_processor", "transaction", None, 1),
137+
("event_processor", "span", None, 3),
138+
]
139+
)

0 commit comments

Comments
 (0)