From d632d1cf3d8fe28533bcc663832bc7825fc71760 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 13 Jun 2025 15:05:09 +0200 Subject: [PATCH 1/7] feat(tracing): Add option to disable specific span origins --- sentry_sdk/consts.py | 14 ++++ sentry_sdk/tracing.py | 9 ++- sentry_sdk/tracing_utils.py | 16 ++++ tests/tracing/test_span_origin.py | 126 ++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 50d08991c6..d0f3ce780c 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -649,6 +649,7 @@ def __init__( trace_propagation_targets=[ # noqa: B006 MATCH_ALL ], # type: Optional[Sequence[str]] + disabled_span_origins=None, # type: Optional[Sequence[str]] functions_to_trace=[], # type: Sequence[Dict[str, str]] # noqa: B006 event_scrubber=None, # type: Optional[sentry_sdk.scrubber.EventScrubber] max_value_length=DEFAULT_MAX_VALUE_LENGTH, # type: int @@ -980,6 +981,19 @@ def __init__( If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the instrumented client. + :param disabled_span_origins: An optional list of strings or regex patterns to disable span creation. + + When a span's origin would match any of the provided regex patterns, the span will not be created. + + This can be useful to disable automatic span creation from specific integrations without disabling the + entire integration. + + The option may contain a list of strings or regex against which the span origins are matched. + String entries do not have to be full matches, meaning a span origin is matched when it _contains_ + a string provided through the option. + + If `disabled_span_origins` is not provided, all spans will be created normally. + :param functions_to_trace: An optional list of functions that should be set up for tracing. For each function in the list, a span will be created when the function is executed. diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index f15f07065a..bbec1a475f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -37,7 +37,10 @@ get_sentry_meta, serialize_trace_state, ) -from sentry_sdk.tracing_utils import get_span_status_from_http_code +from sentry_sdk.tracing_utils import ( + get_span_status_from_http_code, + is_span_origin_disabled, +) from sentry_sdk.utils import ( _serialize_span_attribute, get_current_thread_meta, @@ -205,10 +208,12 @@ def __init__( not parent_span_context.is_valid or parent_span_context.is_remote ) + if not skip_span and is_span_origin_disabled(origin): + skip_span = True + if skip_span: self._otel_span = INVALID_SPAN else: - if start_timestamp is not None: # OTel timestamps have nanosecond precision start_timestamp = convert_to_otel_timestamp(start_timestamp) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 140ce57139..a8a1f1cc22 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -689,6 +689,22 @@ def should_propagate_trace(client, url): return match_regex_list(url, trace_propagation_targets, substring_matching=True) +def is_span_origin_disabled(origin): + # type: (Optional[str]) -> bool + """ + Check if spans with this origin should be ignored. + """ + if origin is None: + return False + + client = sentry_sdk.get_client() + disabled_span_origins = client.options.get("disabled_span_origins") + if not disabled_span_origins: + return False + + return match_regex_list(origin, disabled_span_origins, substring_matching=True) + + def normalize_incoming_data(incoming_data): # type: (Dict[str, Any]) -> Dict[str, Any] """ diff --git a/tests/tracing/test_span_origin.py b/tests/tracing/test_span_origin.py index 649f704b1b..c861f22a0d 100644 --- a/tests/tracing/test_span_origin.py +++ b/tests/tracing/test_span_origin.py @@ -1,3 +1,4 @@ +import pytest from sentry_sdk import start_span @@ -36,3 +37,128 @@ def test_span_origin_custom(sentry_init, capture_events): assert second_transaction["contexts"]["trace"]["origin"] == "ho.ho2.ho3" assert second_transaction["spans"][0]["origin"] == "baz.baz2.baz3" + + +@pytest.mark.parametrize("disabled_origins", [None, [], "noop"]) +def test_disabled_span_origins_empty_config( + sentry_init, capture_events, disabled_origins +): + """Test that when disabled_span_origins is None or empty, all spans are allowed.""" + if disabled_origins in (None, []): + sentry_init(traces_sample_rate=1.0, disabled_span_origins=disabled_origins) + elif disabled_origins == "noop": + sentry_init( + traces_sample_rate=1.0, + # default is None + ) + + events = capture_events() + + with start_span(name="span1"): + pass + with start_span(name="span2", origin="auto.http.requests"): + pass + with start_span(name="span3", origin="auto.db.postgres"): + pass + + assert len(events) == 3 + + +@pytest.mark.parametrize( + "disabled_origins,origins,expected_events_count,expected_allowed_origins", + [ + # Regexes + ( + [r"auto\.http\..*", r"auto\.db\..*"], + [ + "auto.http.requests", + "auto.db.sqlite", + "manual", + ], + 1, + ["manual"], + ), + # Substring matching + ( + ["http"], + [ + "auto.http.requests", + "http.client", + "my.http.integration", + "manual", + "auto.db.postgres", + ], + 2, + ["manual", "auto.db.postgres"], + ), + # Mix and match + ( + ["manual", r"auto\.http\..*", "db"], + [ + "manual", + "auto.http.requests", + "auto.db.postgres", + "auto.grpc.server", + ], + 1, + ["auto.grpc.server"], + ), + ], +) +def test_disabled_span_origins_filtering( + sentry_init, + capture_events, + disabled_origins, + origins, + expected_events_count, + expected_allowed_origins, +): + """Test disabled_span_origins with various pattern configurations.""" + sentry_init( + traces_sample_rate=1.0, + disabled_span_origins=disabled_origins, + ) + + events = capture_events() + + for origin in origins: + with start_span(name="span", origin=origin): + pass + + # Check total events captured + assert len(events) == expected_events_count + + # Check that only expected origins were captured + if expected_events_count > 0: + captured_origins = {event["contexts"]["trace"]["origin"] for event in events} + assert captured_origins == set(expected_allowed_origins) + + +def test_disabled_span_origins_with_child_spans(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0, disabled_span_origins=[r"auto\.http\..*"]) + events = capture_events() + + with start_span(name="parent", origin="manual"): + with start_span(name="http-child", origin="auto.http.requests"): + pass + with start_span(name="db-child", origin="auto.db.postgres"): + pass + + assert len(events) == 1 + assert events[0]["contexts"]["trace"]["origin"] == "manual" + assert len(events[0]["spans"]) == 1 + assert events[0]["spans"][0]["origin"] == "auto.db.postgres" + + +def test_disabled_span_origin_parent_with_child_spans(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0, disabled_span_origins=[r"auto\.http\..*"]) + events = capture_events() + + with start_span(name="parent", origin="auto.http.requests"): + with start_span( + name="db-child", origin="auto.db.postgres", only_if_parent=True + ): + # Note: without only_if_parent, the child span would be promoted to a transaction + pass + + assert len(events) == 0 From 872c429ea036b83037f0315b5a000f4642929171 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Jun 2025 08:25:12 +0200 Subject: [PATCH 2/7] reword --- sentry_sdk/consts.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index d0f3ce780c..8fa01c6c69 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -981,19 +981,17 @@ def __init__( If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the instrumented client. - :param disabled_span_origins: An optional list of strings or regex patterns to disable span creation. - - When a span's origin would match any of the provided regex patterns, the span will not be created. + :param disabled_span_origins: An optional list of strings or regex patterns to disable span creation based + on span origin. When a span's origin would match any of the provided patterns, the span will not be + created. This can be useful to disable automatic span creation from specific integrations without disabling the entire integration. - The option may contain a list of strings or regex against which the span origins are matched. - String entries do not have to be full matches, meaning a span origin is matched when it _contains_ + The option may contain a list of strings or regexes against which the span origins are matched. + String entries do not have to be full matches, meaning a span origin is matched when it contains a string provided through the option. - If `disabled_span_origins` is not provided, all spans will be created normally. - :param functions_to_trace: An optional list of functions that should be set up for tracing. For each function in the list, a span will be created when the function is executed. From a9e26694712817efe74385cf067e2642a7ff65c8 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Jun 2025 10:41:51 +0200 Subject: [PATCH 3/7] rename to exclude --- sentry_sdk/consts.py | 6 +++--- sentry_sdk/tracing.py | 4 ++-- sentry_sdk/tracing_utils.py | 10 +++++----- tests/tracing/test_span_origin.py | 30 +++++++++++++----------------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 8fa01c6c69..d2b1402198 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -649,7 +649,7 @@ def __init__( trace_propagation_targets=[ # noqa: B006 MATCH_ALL ], # type: Optional[Sequence[str]] - disabled_span_origins=None, # type: Optional[Sequence[str]] + exclude_span_origins=None, # type: Optional[Sequence[str]] functions_to_trace=[], # type: Sequence[Dict[str, str]] # noqa: B006 event_scrubber=None, # type: Optional[sentry_sdk.scrubber.EventScrubber] max_value_length=DEFAULT_MAX_VALUE_LENGTH, # type: int @@ -981,11 +981,11 @@ def __init__( If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the instrumented client. - :param disabled_span_origins: An optional list of strings or regex patterns to disable span creation based + :param exclude_span_origins: An optional list of strings or regex patterns to exclude span creation based on span origin. When a span's origin would match any of the provided patterns, the span will not be created. - This can be useful to disable automatic span creation from specific integrations without disabling the + This can be useful to exclude automatic span creation from specific integrations without disabling the entire integration. The option may contain a list of strings or regexes against which the span origins are matched. diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index bbec1a475f..0457fe6451 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -39,7 +39,7 @@ ) from sentry_sdk.tracing_utils import ( get_span_status_from_http_code, - is_span_origin_disabled, + _is_span_origin_disabled, ) from sentry_sdk.utils import ( _serialize_span_attribute, @@ -208,7 +208,7 @@ def __init__( not parent_span_context.is_valid or parent_span_context.is_remote ) - if not skip_span and is_span_origin_disabled(origin): + if not skip_span and _is_span_origin_disabled(origin): skip_span = True if skip_span: diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index a8a1f1cc22..ceea347b4d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -689,20 +689,20 @@ def should_propagate_trace(client, url): return match_regex_list(url, trace_propagation_targets, substring_matching=True) -def is_span_origin_disabled(origin): +def _is_span_origin_disabled(origin): # type: (Optional[str]) -> bool """ - Check if spans with this origin should be ignored. + Check if spans with this origin should be ignored based on the `exclude_span_origins` option. """ if origin is None: return False client = sentry_sdk.get_client() - disabled_span_origins = client.options.get("disabled_span_origins") - if not disabled_span_origins: + exclude_span_origins = client.options.get("exclude_span_origins") + if not exclude_span_origins: return False - return match_regex_list(origin, disabled_span_origins, substring_matching=True) + return match_regex_list(origin, exclude_span_origins, substring_matching=True) def normalize_incoming_data(incoming_data): diff --git a/tests/tracing/test_span_origin.py b/tests/tracing/test_span_origin.py index c861f22a0d..3eb9938245 100644 --- a/tests/tracing/test_span_origin.py +++ b/tests/tracing/test_span_origin.py @@ -39,14 +39,11 @@ def test_span_origin_custom(sentry_init, capture_events): assert second_transaction["spans"][0]["origin"] == "baz.baz2.baz3" -@pytest.mark.parametrize("disabled_origins", [None, [], "noop"]) -def test_disabled_span_origins_empty_config( - sentry_init, capture_events, disabled_origins -): - """Test that when disabled_span_origins is None or empty, all spans are allowed.""" - if disabled_origins in (None, []): - sentry_init(traces_sample_rate=1.0, disabled_span_origins=disabled_origins) - elif disabled_origins == "noop": +@pytest.mark.parametrize("excluded_origins", [None, [], "noop"]) +def test_exclude_span_origins_empty(sentry_init, capture_events, excluded_origins): + if excluded_origins in (None, []): + sentry_init(traces_sample_rate=1.0, exclude_span_origins=excluded_origins) + elif excluded_origins == "noop": sentry_init( traces_sample_rate=1.0, # default is None @@ -65,7 +62,7 @@ def test_disabled_span_origins_empty_config( @pytest.mark.parametrize( - "disabled_origins,origins,expected_events_count,expected_allowed_origins", + "excluded_origins,origins,expected_events_count,expected_allowed_origins", [ # Regexes ( @@ -105,18 +102,17 @@ def test_disabled_span_origins_empty_config( ), ], ) -def test_disabled_span_origins_filtering( +def test_exclude_span_origins_patterns( sentry_init, capture_events, - disabled_origins, + excluded_origins, origins, expected_events_count, expected_allowed_origins, ): - """Test disabled_span_origins with various pattern configurations.""" sentry_init( traces_sample_rate=1.0, - disabled_span_origins=disabled_origins, + exclude_span_origins=excluded_origins, ) events = capture_events() @@ -134,8 +130,8 @@ def test_disabled_span_origins_filtering( assert captured_origins == set(expected_allowed_origins) -def test_disabled_span_origins_with_child_spans(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0, disabled_span_origins=[r"auto\.http\..*"]) +def test_exclude_span_origins_with_child_spans(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0, exclude_span_origins=[r"auto\.http\..*"]) events = capture_events() with start_span(name="parent", origin="manual"): @@ -150,8 +146,8 @@ def test_disabled_span_origins_with_child_spans(sentry_init, capture_events): assert events[0]["spans"][0]["origin"] == "auto.db.postgres" -def test_disabled_span_origin_parent_with_child_spans(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0, disabled_span_origins=[r"auto\.http\..*"]) +def test_exclude_span_origins_parent_with_child_spans(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0, exclude_span_origins=[r"auto\.http\..*"]) events = capture_events() with start_span(name="parent", origin="auto.http.requests"): From 11c7ce188a19596d2b4b4bcfe2f56f8628d0e8b6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Jun 2025 10:43:50 +0200 Subject: [PATCH 4/7] wording --- sentry_sdk/consts.py | 2 +- sentry_sdk/tracing.py | 4 ++-- sentry_sdk/tracing_utils.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index d2b1402198..a55f9024a6 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -981,7 +981,7 @@ def __init__( If `trace_propagation_targets` is not provided, trace data is attached to every outgoing request from the instrumented client. - :param exclude_span_origins: An optional list of strings or regex patterns to exclude span creation based + :param exclude_span_origins: An optional list of strings or regex patterns to disable span creation based on span origin. When a span's origin would match any of the provided patterns, the span will not be created. diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0457fe6451..2c09308afe 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -39,7 +39,7 @@ ) from sentry_sdk.tracing_utils import ( get_span_status_from_http_code, - _is_span_origin_disabled, + _is_span_origin_excluded, ) from sentry_sdk.utils import ( _serialize_span_attribute, @@ -208,7 +208,7 @@ def __init__( not parent_span_context.is_valid or parent_span_context.is_remote ) - if not skip_span and _is_span_origin_disabled(origin): + if not skip_span and _is_span_origin_excluded(origin): skip_span = True if skip_span: diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index ceea347b4d..4f5a4ceb6d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -689,7 +689,7 @@ def should_propagate_trace(client, url): return match_regex_list(url, trace_propagation_targets, substring_matching=True) -def _is_span_origin_disabled(origin): +def _is_span_origin_excluded(origin): # type: (Optional[str]) -> bool """ Check if spans with this origin should be ignored based on the `exclude_span_origins` option. From 4f231e8090815154e4a1f9a5191437f6bc3f95e1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Jun 2025 10:45:27 +0200 Subject: [PATCH 5/7] more unused comments --- tests/tracing/test_span_origin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/tracing/test_span_origin.py b/tests/tracing/test_span_origin.py index 3eb9938245..f9d0f7ce3f 100644 --- a/tests/tracing/test_span_origin.py +++ b/tests/tracing/test_span_origin.py @@ -121,10 +121,8 @@ def test_exclude_span_origins_patterns( with start_span(name="span", origin=origin): pass - # Check total events captured assert len(events) == expected_events_count - # Check that only expected origins were captured if expected_events_count > 0: captured_origins = {event["contexts"]["trace"]["origin"] for event in events} assert captured_origins == set(expected_allowed_origins) From 39a0689cc42c531d123fabe693fa2332e40a8878 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Jun 2025 12:45:22 +0200 Subject: [PATCH 6/7] . --- sentry_sdk/tracing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 2c09308afe..0ac5c29a4f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -208,6 +208,7 @@ def __init__( not parent_span_context.is_valid or parent_span_context.is_remote ) + origin = origin or DEFAULT_SPAN_ORIGIN if not skip_span and _is_span_origin_excluded(origin): skip_span = True @@ -244,7 +245,7 @@ def __init__( attributes=attributes, ) - self.origin = origin or DEFAULT_SPAN_ORIGIN + self.origin = origin self.description = description self.name = span_name From 2f4016ad088ebb0687abf19518310b2b5b8c5e6d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 16 Jun 2025 13:03:30 +0200 Subject: [PATCH 7/7] redundant test stuff --- tests/tracing/test_span_origin.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/tracing/test_span_origin.py b/tests/tracing/test_span_origin.py index f9d0f7ce3f..7aaaa09e9a 100644 --- a/tests/tracing/test_span_origin.py +++ b/tests/tracing/test_span_origin.py @@ -62,7 +62,7 @@ def test_exclude_span_origins_empty(sentry_init, capture_events, excluded_origin @pytest.mark.parametrize( - "excluded_origins,origins,expected_events_count,expected_allowed_origins", + "excluded_origins,origins,expected_allowed_origins", [ # Regexes ( @@ -72,7 +72,6 @@ def test_exclude_span_origins_empty(sentry_init, capture_events, excluded_origin "auto.db.sqlite", "manual", ], - 1, ["manual"], ), # Substring matching @@ -85,7 +84,6 @@ def test_exclude_span_origins_empty(sentry_init, capture_events, excluded_origin "manual", "auto.db.postgres", ], - 2, ["manual", "auto.db.postgres"], ), # Mix and match @@ -97,7 +95,6 @@ def test_exclude_span_origins_empty(sentry_init, capture_events, excluded_origin "auto.db.postgres", "auto.grpc.server", ], - 1, ["auto.grpc.server"], ), ], @@ -107,7 +104,6 @@ def test_exclude_span_origins_patterns( capture_events, excluded_origins, origins, - expected_events_count, expected_allowed_origins, ): sentry_init( @@ -121,9 +117,9 @@ def test_exclude_span_origins_patterns( with start_span(name="span", origin=origin): pass - assert len(events) == expected_events_count + assert len(events) == len(expected_allowed_origins) - if expected_events_count > 0: + if len(expected_allowed_origins) > 0: captured_origins = {event["contexts"]["trace"]["origin"] for event in events} assert captured_origins == set(expected_allowed_origins)