From af35dad9e67b175ed5092611c8f2ad945b1283fe Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 09:37:03 +0200 Subject: [PATCH 01/11] chore: Adapt more tests --- .../celery/test_update_celery_task_headers.py | 28 +++++++++---------- tests/integrations/dramatiq/test_dramatiq.py | 1 + .../google_genai/test_google_genai.py | 1 + tests/integrations/httpx/test_httpx.py | 2 ++ .../integrations/langchain/test_langchain.py | 5 ++++ tests/integrations/litellm/test_litellm.py | 1 + tests/integrations/mcp/test_mcp.py | 1 + .../openai_agents/test_openai_agents.py | 1 + .../integrations/threading/test_threading.py | 1 + 9 files changed, 27 insertions(+), 14 deletions(-) diff --git a/tests/integrations/celery/test_update_celery_task_headers.py b/tests/integrations/celery/test_update_celery_task_headers.py index 4418653885..d77b446caf 100644 --- a/tests/integrations/celery/test_update_celery_task_headers.py +++ b/tests/integrations/celery/test_update_celery_task_headers.py @@ -70,41 +70,41 @@ def test_monitor_beat_tasks_with_headers(monitor_beat_tasks): def test_span_with_transaction(sentry_init): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") headers = {} monitor_beat_tasks = False - with sentry_sdk.start_transaction(name="test_transaction") as transaction: - with sentry_sdk.start_span(op="test_span") as span: + with sentry_sdk.traces.start_span(name="test_segment") as segment: + with sentry_sdk.traces.start_span(name="test_span") as span: outgoing_headers = _update_celery_task_headers( headers, span, monitor_beat_tasks ) - assert outgoing_headers["sentry-trace"] == span.to_traceparent() - assert outgoing_headers["headers"]["sentry-trace"] == span.to_traceparent() - assert outgoing_headers["baggage"] == transaction.get_baggage().serialize() + assert outgoing_headers["sentry-trace"] == span._to_traceparent() + assert outgoing_headers["headers"]["sentry-trace"] == span._to_traceparent() + assert outgoing_headers["baggage"] == segment._get_baggage().serialize() assert ( outgoing_headers["headers"]["baggage"] - == transaction.get_baggage().serialize() + == segment._get_baggage().serialize() ) def test_span_with_transaction_custom_headers(sentry_init): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") headers = { "baggage": BAGGAGE_VALUE, "sentry-trace": SENTRY_TRACE_VALUE, } - with sentry_sdk.start_transaction(name="test_transaction") as transaction: - with sentry_sdk.start_span(op="test_span") as span: + with sentry_sdk.traces.start_span(name="test_segment") as segment: + with sentry_sdk.traces.start_span(name="test_span") as span: outgoing_headers = _update_celery_task_headers(headers, span, False) - assert outgoing_headers["sentry-trace"] == span.to_traceparent() - assert outgoing_headers["headers"]["sentry-trace"] == span.to_traceparent() + assert outgoing_headers["sentry-trace"] == span._to_traceparent() + assert outgoing_headers["headers"]["sentry-trace"] == span._to_traceparent() incoming_baggage = Baggage.from_incoming_header(headers["baggage"]) - combined_baggage = copy(transaction.get_baggage()) + combined_baggage = copy(segment._get_baggage()) combined_baggage.sentry_items.update(incoming_baggage.sentry_items) combined_baggage.third_party_items = ",".join( [ @@ -169,7 +169,7 @@ def test_celery_trace_propagation_traces_sample_rate( The Celery integration has its own mechanism to propagate traces: https://docs.sentry.io/platforms/python/integrations/celery/#distributed-traces """ - sentry_init(traces_sample_rate=traces_sample_rate) + sentry_init(traces_sample_rate=traces_sample_rate, trace_lifecycle="stream") headers = {} span = None diff --git a/tests/integrations/dramatiq/test_dramatiq.py b/tests/integrations/dramatiq/test_dramatiq.py index 100b874423..1bab8d1dfe 100644 --- a/tests/integrations/dramatiq/test_dramatiq.py +++ b/tests/integrations/dramatiq/test_dramatiq.py @@ -22,6 +22,7 @@ def broker(request, sentry_init): sentry_init( integrations=[DramatiqIntegration()], traces_sample_rate=param, + trace_lifecycle="stream", ) broker = StubBroker() broker.emit_after("process_boot") diff --git a/tests/integrations/google_genai/test_google_genai.py b/tests/integrations/google_genai/test_google_genai.py index f3cc2e8d0f..845d7a971e 100644 --- a/tests/integrations/google_genai/test_google_genai.py +++ b/tests/integrations/google_genai/test_google_genai.py @@ -863,6 +863,7 @@ def test_tool_with_async_function(sentry_init): sentry_init( integrations=[GoogleGenAIIntegration()], traces_sample_rate=1.0, + trace_lifecycle="stream", ) # Create an async tool function diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 18ebefc7ea..899533ec2a 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -388,6 +388,7 @@ def test_option_trace_propagation_targets_sync( release="test", trace_propagation_targets=trace_propagation_targets, traces_sample_rate=1.0, + trace_lifecycle="stream", integrations=[HttpxIntegration()], ) @@ -466,6 +467,7 @@ async def test_option_trace_propagation_targets_async( release="test", trace_propagation_targets=trace_propagation_targets, traces_sample_rate=1.0, + trace_lifecycle="stream", integrations=[HttpxIntegration()], ) diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 84151e563e..89086e4120 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -1965,6 +1965,7 @@ def test_langchain_tool_error( integrations=[LangchainIntegration(include_prompts=True)], disabled_integrations=[StdlibIntegration], traces_sample_rate=1.0, + trace_lifecycle="stream", ) responses = nonstreaming_responses_tool_call_model_responses( @@ -2108,6 +2109,7 @@ def test_langchain_callback_manager(sentry_init): integrations=[LangchainIntegration()], disabled_integrations=[StdlibIntegration], traces_sample_rate=1.0, + trace_lifecycle="stream", ) local_manager = BaseCallbackManager(handlers=[]) @@ -2141,6 +2143,7 @@ def test_langchain_callback_manager_with_sentry_callback(sentry_init): integrations=[LangchainIntegration()], disabled_integrations=[StdlibIntegration], traces_sample_rate=1.0, + trace_lifecycle="stream", ) sentry_callback = SentryLangchainCallback(False) local_manager = BaseCallbackManager(handlers=[sentry_callback]) @@ -2174,6 +2177,7 @@ def test_langchain_callback_list(sentry_init): integrations=[LangchainIntegration()], disabled_integrations=[StdlibIntegration], traces_sample_rate=1.0, + trace_lifecycle="stream", ) local_callbacks = [] @@ -2207,6 +2211,7 @@ def test_langchain_callback_list_existing_callback(sentry_init): integrations=[LangchainIntegration()], disabled_integrations=[StdlibIntegration], traces_sample_rate=1.0, + trace_lifecycle="stream", ) sentry_callback = SentryLangchainCallback(False) local_callbacks = [sentry_callback] diff --git a/tests/integrations/litellm/test_litellm.py b/tests/integrations/litellm/test_litellm.py index 655d3221ca..f22505e271 100644 --- a/tests/integrations/litellm/test_litellm.py +++ b/tests/integrations/litellm/test_litellm.py @@ -1514,6 +1514,7 @@ def test_integration_setup(sentry_init): integrations=[LiteLLMIntegration()], disabled_integrations=[StdlibIntegration], traces_sample_rate=1.0, + trace_lifecycle="stream", ) # Check that callbacks are registered diff --git a/tests/integrations/mcp/test_mcp.py b/tests/integrations/mcp/test_mcp.py index 2180db046e..27b494c667 100644 --- a/tests/integrations/mcp/test_mcp.py +++ b/tests/integrations/mcp/test_mcp.py @@ -151,6 +151,7 @@ def test_integration_patches_server(sentry_init): sentry_init( integrations=[MCPIntegration()], traces_sample_rate=1.0, + trace_lifecycle="stream", ) assert Server.call_tool is not original_call_tool diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 96ea5bb576..8e6d66753a 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -3175,6 +3175,7 @@ def test_openai_agents_message_role_mapping(sentry_init, test_message, expected_ sentry_init( integrations=[OpenAIAgentsIntegration()], traces_sample_rate=1.0, + trace_lifecycle="stream", send_default_pii=True, ) diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py index eedf1bbdfb..77a4ea539a 100644 --- a/tests/integrations/threading/test_threading.py +++ b/tests/integrations/threading/test_threading.py @@ -197,6 +197,7 @@ def test_spans_from_threadpool( ): sentry_init( traces_sample_rate=1.0, + trace_lifecycle="stream", integrations=[ThreadingIntegration(propagate_scope=propagate_scope)], ) events = capture_events() From c68857dfd75e4c54b78e52d2ebf9021ef464bb98 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 09:48:19 +0200 Subject: [PATCH 02/11] . --- .../integrations/threading/test_threading.py | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py index 77a4ea539a..372e14c4cd 100644 --- a/tests/integrations/threading/test_threading.py +++ b/tests/integrations/threading/test_threading.py @@ -1,6 +1,5 @@ import sys from concurrent import futures -from textwrap import dedent from threading import Thread import pytest @@ -193,60 +192,58 @@ def do_some_work(number): ids=["propagate_scope=True", "propagate_scope=False"], ) def test_spans_from_threadpool( - sentry_init, capture_events, render_span_tree, propagate_scope + sentry_init, capture_items, render_span_tree, propagate_scope ): sentry_init( traces_sample_rate=1.0, trace_lifecycle="stream", integrations=[ThreadingIntegration(propagate_scope=propagate_scope)], ) - events = capture_events() + items = capture_items() def do_some_work(number): - with sentry_sdk.start_span( - op=f"inner-run-{number}", name=f"Thread: child-{number}" - ): - pass + with sentry_sdk.traces.start_span(name=f"inner-run-{number}") as span: + inner_run_spans[number] = span - with sentry_sdk.start_transaction(op="outer-trx"): + outer_submit_spans = {} + inner_run_spans = {} + + with sentry_sdk.traces.start_span(name="outer") as outer: with futures.ThreadPoolExecutor(max_workers=1) as executor: for number in range(5): - with sentry_sdk.start_span( - op=f"outer-submit-{number}", name="Thread: main" - ): + with sentry_sdk.traces.start_span( + name=f"outer-submit-{number}" + ) as span: + outer_submit_spans[number] = span future = executor.submit(do_some_work, number) future.result() - (event,) = events + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 11 + + # Free-threaded builds set thread_inherit_context to True, otherwise thread_inherit_context is False + for span in spans: + if span["name"] == "outer-seg": + assert span["is_segment"] is True + assert "parent_span_id" not in span + + if span["name"].startswith("outer-submit-"): + assert span["is_segment"] is False + assert span["parent_span_id"] == outer.span_id # Free-threaded builds set thread_inherit_context to True, otherwise thread_inherit_context is False if propagate_scope or getattr(sys.flags, "thread_inherit_context", None): - assert event["type"] == "transaction" - assert render_span_tree(event["spans"], event["contexts"]["trace"]) == dedent( - """\ - - op="outer-trx": description=null - - op="outer-submit-0": description="Thread: main" - - op="inner-run-0": description="Thread: child-0" - - op="outer-submit-1": description="Thread: main" - - op="inner-run-1": description="Thread: child-1" - - op="outer-submit-2": description="Thread: main" - - op="inner-run-2": description="Thread: child-2" - - op="outer-submit-3": description="Thread: main" - - op="inner-run-3": description="Thread: child-3" - - op="outer-submit-4": description="Thread: main" - - op="inner-run-4": description="Thread: child-4"\ -""" - ) + for span in spans: + if span["name"].startswith("inner-run-"): + num = int(span["name"][-1]) + assert span["is_segment"] is False + assert span["parent_span_id"] == outer_submit_spans[num].span_id elif not propagate_scope: - assert event["type"] == "transaction" - assert render_span_tree(event["spans"], event["contexts"]["trace"]) == dedent( - """\ - - op="outer-trx": description=null - - op="outer-submit-0": description="Thread: main" - - op="outer-submit-1": description="Thread: main" - - op="outer-submit-2": description="Thread: main" - - op="outer-submit-3": description="Thread: main" - - op="outer-submit-4": description="Thread: main"\ -""" - ) + for span in spans: + if span["name"].startswith("inner-run-"): + num = int(span["name"][-1]) + assert span["is_segment"] is True + assert "parent_span_id" not in span From 14314802051d77014505aa464c09bc0fe36fe0f3 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 09:54:18 +0200 Subject: [PATCH 03/11] more tests --- tests/test_api.py | 72 ----------- tests/test_basics.py | 302 +++++++++++-------------------------------- 2 files changed, 72 insertions(+), 302 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index a1a8d7b285..981ca13bca 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,7 +4,6 @@ import sentry_sdk from sentry_sdk import ( capture_exception, - continue_trace, get_baggage, get_client, get_current_scope, @@ -14,7 +13,6 @@ get_traceparent, is_initialized, set_tags, - start_transaction, ) from sentry_sdk.client import Client, NonRecordingClient from sentry_sdk.traces import StreamedSpan @@ -64,15 +62,6 @@ def test_get_current_span_current_scope_span_streaming(sentry_init): assert sentry_sdk.traces.get_current_span() == fake_span -def test_get_current_span_with_transaction(sentry_init): - sentry_init() - - assert get_current_span() is None - - with start_transaction() as new_transaction: - assert get_current_span() == new_transaction - - def test_get_current_span_with_segment(sentry_init): sentry_init(trace_lifecycle="stream") @@ -83,17 +72,6 @@ def test_get_current_span_with_segment(sentry_init): def test_traceparent_with_tracing_enabled(sentry_init): - sentry_init(traces_sample_rate=1.0) - - with start_transaction() as transaction: - expected_traceparent = "%s-%s-1" % ( - transaction.trace_id, - transaction.span_id, - ) - assert get_traceparent() == expected_traceparent - - -def test_traceparent_with_tracing_enabled_span_streaming(sentry_init): sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") with sentry_sdk.traces.start_span(name="span") as segment: @@ -149,15 +127,6 @@ def test_baggage_with_tracing_disabled_span_streaming(sentry_init): def test_baggage_with_tracing_enabled(sentry_init): - sentry_init(traces_sample_rate=1.0, release="1.0.0", environment="dev") - with start_transaction() as transaction: - expected_baggage_re = r"^sentry-trace_id={},sentry-sample_rand=0\.\d{{6}},sentry-environment=dev,sentry-release=1\.0\.0,sentry-sample_rate=1\.0,sentry-sampled={}$".format( - transaction.trace_id, "true" if transaction.sampled else "false" - ) - assert re.match(expected_baggage_re, get_baggage()) - - -def test_baggage_with_tracing_enabled_span_streaming(sentry_init): sentry_init( traces_sample_rate=1.0, trace_lifecycle="stream", @@ -172,21 +141,6 @@ def test_baggage_with_tracing_enabled_span_streaming(sentry_init): def test_baggage_with_dsn(sentry_init): - sentry_init( - dsn="http://97333d956c9e40989a0139756c121c34@sentry-x.sentry-y.s.c.local/976543210", - traces_sample_rate=1.0, - release="2.0.0", - environment="dev", - transport=TestTransportWithOptions, - ) - with start_transaction() as transaction: - expected_baggage_re = r"^sentry-trace_id={},sentry-sample_rand=0\.\d{{6}},sentry-environment=dev,sentry-release=2\.0\.0,sentry-public_key=97333d956c9e40989a0139756c121c34,sentry-sample_rate=1\.0,sentry-sampled={}$".format( - transaction.trace_id, "true" if transaction.sampled else "false" - ) - assert re.match(expected_baggage_re, get_baggage()) - - -def test_baggage_with_dsn_span_streaming(sentry_init): sentry_init( dsn="http://97333d956c9e40989a0139756c121c34@sentry-x.sentry-y.s.c.local/976543210", traces_sample_rate=1.0, @@ -203,32 +157,6 @@ def test_baggage_with_dsn_span_streaming(sentry_init): def test_continue_trace(sentry_init): - sentry_init() - - trace_id = "471a43a4192642f0b136d5159a501701" - parent_span_id = "6e8f22c393e68f19" - parent_sampled = 1 - transaction = continue_trace( - { - "sentry-trace": "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled), - "baggage": "sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-sample_rand=0.123456", - }, - name="some name", - ) - with start_transaction(transaction): - assert transaction.name == "some name" - - propagation_context = get_isolation_scope()._propagation_context - assert propagation_context.trace_id == transaction.trace_id == trace_id - assert propagation_context.parent_span_id == parent_span_id - assert propagation_context.parent_sampled == parent_sampled - assert propagation_context.dynamic_sampling_context == { - "trace_id": "566e3688a61d4bc888951642d6f14a19", - "sample_rand": "0.123456", - } - - -def test_continue_trace_span_streaming(sentry_init): sentry_init(trace_lifecycle="stream") trace_id = "471a43a4192642f0b136d5159a501701" diff --git a/tests/test_basics.py b/tests/test_basics.py index f1b9fef102..72866e5aea 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -19,7 +19,6 @@ isolation_scope, last_event_id, new_scope, - start_transaction, ) from sentry_sdk.integrations import ( _AUTO_ENABLING_INTEGRATIONS, @@ -161,40 +160,6 @@ def do_this(): assert len(events) == 0 -def test_option_before_send_transaction(sentry_init, capture_events): - def before_send_transaction(event, hint): - assert event["type"] == "transaction" - event["extra"] = {"before_send_transaction_called": True} - return event - - sentry_init( - before_send_transaction=before_send_transaction, - traces_sample_rate=1.0, - ) - events = capture_events() - transaction = start_transaction(name="foo") - transaction.finish() - - (event,) = events - assert event["transaction"] == "foo" - assert event["extra"] == {"before_send_transaction_called": True} - - -def test_option_before_send_transaction_discard(sentry_init, capture_events): - def before_send_transaction_discard(event, hint): - return None - - sentry_init( - before_send_transaction=before_send_transaction_discard, - traces_sample_rate=1.0, - ) - events = capture_events() - transaction = start_transaction(name="foo") - transaction.finish() - - assert len(events) == 0 - - def test_option_before_breadcrumb(sentry_init, capture_events, monkeypatch): drop_events = False drop_breadcrumbs = False @@ -683,16 +648,12 @@ def foo(event, hint): capture_message("dropped") - with start_transaction(name="dropped"): - pass - assert len(events) == 0 # Using Counter because order of record_lost_event calls does not matter assert Counter(record_lost_event_calls) == Counter( [ ("event_processor", "error", None, 1), - ("event_processor", "transaction", None, 1), ("event_processor", "span", None, 1), ] ) @@ -776,8 +737,7 @@ def _hello_world(word): return "Hello, {}".format(word) -@pytest.mark.parametrize("span_streaming", [True, False]) -def test_functions_to_trace(sentry_init, capture_events, capture_items, span_streaming): +def test_functions_to_trace(sentry_init, capture_items): functions_to_trace = [ {"qualified_name": "tests.test_basics._hello_world"}, {"qualified_name": "time.sleep"}, @@ -791,45 +751,26 @@ def test_functions_to_trace(sentry_init, capture_events, capture_items, span_str sentry_init( traces_sample_rate=1.0, functions_to_trace=functions_to_trace, - trace_lifecycle="stream" if span_streaming else "static", + trace_lifecycle="stream", ) - if span_streaming: - items = capture_items("span") - else: - events = capture_events() - - if span_streaming: - with sentry_sdk.traces.start_span(name="something"): - time.sleep(0) - - for word in ["World", "You"]: - _hello_world(word) - - sentry_sdk.flush() - spans = [item.payload for item in items] - child_spans = [s for s in spans if not s.get("is_segment")] - child_spans.sort(key=lambda s: s["start_timestamp"]) - - assert len(child_spans) == 3 - assert child_spans[0]["name"] == "time.sleep" - assert child_spans[1]["name"] == "tests.test_basics._hello_world" - assert child_spans[2]["name"] == "tests.test_basics._hello_world" - else: - with start_transaction(name="something"): - time.sleep(0) + items = capture_items("span") - for word in ["World", "You"]: - _hello_world(word) + with sentry_sdk.traces.start_span(name="something"): + time.sleep(0) - assert len(events) == 1 + for word in ["World", "You"]: + _hello_world(word) - (event,) = events + sentry_sdk.flush() + spans = [item.payload for item in items] + child_spans = [s for s in spans if not s.get("is_segment")] + child_spans.sort(key=lambda s: s["start_timestamp"]) - assert len(event["spans"]) == 3 - assert event["spans"][0]["description"] == "time.sleep" - assert event["spans"][1]["description"] == "tests.test_basics._hello_world" - assert event["spans"][2]["description"] == "tests.test_basics._hello_world" + assert len(child_spans) == 3 + assert child_spans[0]["name"] == "time.sleep" + assert child_spans[1]["name"] == "tests.test_basics._hello_world" + assert child_spans[2]["name"] == "tests.test_basics._hello_world" finally: _hello_world = original_hello_world time.sleep = original_sleep @@ -843,10 +784,7 @@ def greet(self, new_word=None): return "Hello, {}".format(new_word if new_word else self.word) -@pytest.mark.parametrize("span_streaming", [True, False]) -def test_functions_to_trace_with_class( - sentry_init, capture_events, capture_items, span_streaming -): +def test_functions_to_trace_with_class(sentry_init, capture_items): functions_to_trace = [ {"qualified_name": "tests.test_basics.WorldGreeter.greet"}, ] @@ -857,46 +795,24 @@ def test_functions_to_trace_with_class( sentry_init( traces_sample_rate=1.0, functions_to_trace=functions_to_trace, - trace_lifecycle="stream" if span_streaming else "static", + trace_lifecycle="stream", ) - if span_streaming: - items = capture_items("span") - else: - events = capture_events() - - if span_streaming: - with sentry_sdk.traces.start_span(name="something"): - wg = WorldGreeter("World") - wg.greet() - wg.greet("You") - - sentry_sdk.flush() - spans = [item.payload for item in items] - child_spans = [s for s in spans if not s.get("is_segment")] - - assert len(child_spans) == 2 - assert child_spans[0]["name"] == "tests.test_basics.WorldGreeter.greet" - assert child_spans[1]["name"] == "tests.test_basics.WorldGreeter.greet" - else: - with start_transaction(name="something"): - wg = WorldGreeter("World") - wg.greet() - wg.greet("You") - - assert len(events) == 1 - - (event,) = events - - assert len(event["spans"]) == 2 - assert ( - event["spans"][0]["description"] - == "tests.test_basics.WorldGreeter.greet" - ) - assert ( - event["spans"][1]["description"] - == "tests.test_basics.WorldGreeter.greet" - ) + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="something"): + wg = WorldGreeter("World") + wg.greet() + wg.greet("You") + + sentry_sdk.flush() + spans = [item.payload for item in items] + child_spans = [s for s in spans if not s.get("is_segment")] + + assert len(child_spans) == 2 + assert child_spans[0]["name"] == "tests.test_basics.WorldGreeter.greet" + assert child_spans[1]["name"] == "tests.test_basics.WorldGreeter.greet" + finally: WorldGreeter.greet = original_function @@ -921,166 +837,103 @@ def class_(cls, arg): # We need to fork here because the test modifies tests.test_basics.TracingTestClass @pytest.mark.forked -@pytest.mark.parametrize("span_streaming", [True, False]) -def test_staticmethod_class_tracing( - sentry_init, capture_events, capture_items, span_streaming -): +def test_staticmethod_class_tracing(sentry_init, capture_items): sentry_init( debug=True, traces_sample_rate=1.0, functions_to_trace=[ {"qualified_name": "tests.test_basics.TracingTestClass.static"} ], - trace_lifecycle="stream" if span_streaming else "static", + trace_lifecycle="stream", ) - if span_streaming: - items = capture_items("span") - - with sentry_sdk.traces.start_span(name="test"): - assert TracingTestClass.static(1) == 1 - - sentry_sdk.flush() - spans = [item.payload for item in items] - child_spans = [s for s in spans if not s.get("is_segment")] + items = capture_items("span") - assert len(child_spans) == 1 - assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.static" - else: - events = capture_events() + with sentry_sdk.traces.start_span(name="test"): + assert TracingTestClass.static(1) == 1 - with sentry_sdk.start_transaction(name="test"): - assert TracingTestClass.static(1) == 1 + sentry_sdk.flush() - (event,) = events - assert event["type"] == "transaction" - assert event["transaction"] == "test" + spans = [item.payload for item in items] + child_spans = [s for s in spans if not s.get("is_segment")] - (span,) = event["spans"] - assert span["description"] == "tests.test_basics.TracingTestClass.static" + assert len(child_spans) == 1 + assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.static" # We need to fork here because the test modifies tests.test_basics.TracingTestClass @pytest.mark.forked -@pytest.mark.parametrize("span_streaming", [True, False]) -def test_staticmethod_instance_tracing( - sentry_init, capture_events, capture_items, span_streaming -): +def test_staticmethod_instance_tracing(sentry_init, capture_items): sentry_init( debug=True, traces_sample_rate=1.0, functions_to_trace=[ {"qualified_name": "tests.test_basics.TracingTestClass.static"} ], - trace_lifecycle="stream" if span_streaming else "static", + trace_lifecycle="stream", ) - if span_streaming: - items = capture_items("span") - - with sentry_sdk.traces.start_span(name="test"): - assert TracingTestClass().static(1) == 1 + items = capture_items("span") - sentry_sdk.flush() - spans = [item.payload for item in items] - child_spans = [s for s in spans if not s.get("is_segment")] - - assert len(child_spans) == 1 - assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.static" - else: - events = capture_events() - - with sentry_sdk.start_transaction(name="test"): - assert TracingTestClass().static(1) == 1 + with sentry_sdk.traces.start_span(name="test"): + assert TracingTestClass().static(1) == 1 - (event,) = events - assert event["type"] == "transaction" - assert event["transaction"] == "test" + sentry_sdk.flush() + spans = [item.payload for item in items] + child_spans = [s for s in spans if not s.get("is_segment")] - (span,) = event["spans"] - assert span["description"] == "tests.test_basics.TracingTestClass.static" + assert len(child_spans) == 1 + assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.static" # We need to fork here because the test modifies tests.test_basics.TracingTestClass @pytest.mark.forked -@pytest.mark.parametrize("span_streaming", [True, False]) -def test_classmethod_class_tracing( - sentry_init, capture_events, capture_items, span_streaming -): +def test_classmethod_class_tracing(sentry_init, capture_items): sentry_init( debug=True, traces_sample_rate=1.0, functions_to_trace=[ {"qualified_name": "tests.test_basics.TracingTestClass.class_"} ], - trace_lifecycle="stream" if span_streaming else "static", + trace_lifecycle="stream", ) - if span_streaming: - items = capture_items("span") + items = capture_items("span") - with sentry_sdk.traces.start_span(name="test"): - assert TracingTestClass.class_(1) == (TracingTestClass, 1) + with sentry_sdk.traces.start_span(name="test"): + assert TracingTestClass.class_(1) == (TracingTestClass, 1) - sentry_sdk.flush() - spans = [item.payload for item in items] - child_spans = [s for s in spans if not s.get("is_segment")] + sentry_sdk.flush() + spans = [item.payload for item in items] + child_spans = [s for s in spans if not s.get("is_segment")] - assert len(child_spans) == 1 - assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.class_" - else: - events = capture_events() - - with sentry_sdk.start_transaction(name="test"): - assert TracingTestClass.class_(1) == (TracingTestClass, 1) - - (event,) = events - assert event["type"] == "transaction" - assert event["transaction"] == "test" - - (span,) = event["spans"] - assert span["description"] == "tests.test_basics.TracingTestClass.class_" + assert len(child_spans) == 1 + assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.class_" # We need to fork here because the test modifies tests.test_basics.TracingTestClass @pytest.mark.forked -@pytest.mark.parametrize("span_streaming", [True, False]) -def test_classmethod_instance_tracing( - sentry_init, capture_events, capture_items, span_streaming -): +def test_classmethod_instance_tracing(sentry_init, capture_items): sentry_init( debug=True, traces_sample_rate=1.0, functions_to_trace=[ {"qualified_name": "tests.test_basics.TracingTestClass.class_"} ], - trace_lifecycle="stream" if span_streaming else "static", + trace_lifecycle="stream", ) - if span_streaming: - items = capture_items("span") - - with sentry_sdk.traces.start_span(name="test"): - assert TracingTestClass().class_(1) == (TracingTestClass, 1) - - sentry_sdk.flush() - spans = [item.payload for item in items] - child_spans = [s for s in spans if not s.get("is_segment")] + items = capture_items("span") - assert len(child_spans) == 1 - assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.class_" - else: - events = capture_events() + with sentry_sdk.traces.start_span(name="test"): + assert TracingTestClass().class_(1) == (TracingTestClass, 1) - with sentry_sdk.start_transaction(name="test"): - assert TracingTestClass().class_(1) == (TracingTestClass, 1) + sentry_sdk.flush() + spans = [item.payload for item in items] + child_spans = [s for s in spans if not s.get("is_segment")] - (event,) = events - assert event["type"] == "transaction" - assert event["transaction"] == "test" - - (span,) = event["spans"] - assert span["description"] == "tests.test_basics.TracingTestClass.class_" + assert len(child_spans) == 1 + assert child_spans[0]["name"] == "tests.test_basics.TracingTestClass.class_" def test_functions_to_trace_no_dot_does_not_crash(sentry_init): @@ -1101,17 +954,6 @@ def test_last_event_id(sentry_init): assert last_event_id() is not None -def test_last_event_id_transaction(sentry_init): - sentry_init(traces_sample_rate=1.0) - - assert last_event_id() is None - - with start_transaction(name="test"): - pass - - assert last_event_id() is None, "Transaction should not set last_event_id" - - def test_last_event_id_scope(sentry_init): sentry_init(traces_sample_rate=1.0) From 7e1aef733a040486d586db830e27add260479939 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 09:58:55 +0200 Subject: [PATCH 04/11] adapt --- tests/integrations/openai_agents/test_openai_agents.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 8e6d66753a..b47659fadd 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -68,7 +68,6 @@ from typing import Any, cast import sentry_sdk -from sentry_sdk import start_span from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.openai_agents import OpenAIAgentsIntegration @@ -3181,13 +3180,13 @@ def test_openai_agents_message_role_mapping(sentry_init, test_message, expected_ get_response_kwargs = {"input": [test_message]} - with start_span(op="test") as span: + with sentry_sdk.traces.start_span(name="test") as span: _set_input_data(span, get_response_kwargs) # Verify that messages were processed and roles were mapped from sentry_sdk.consts import SPANDATA - stored_messages = json.loads(span._data[SPANDATA.GEN_AI_REQUEST_MESSAGES]) + stored_messages = json.loads(span._attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES]) # Verify roles were properly mapped assert stored_messages[0]["role"] == expected_role From eaa56f069aab8bd52d5a7ab0d2bb7ce33e4f0602 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 10:05:00 +0200 Subject: [PATCH 05/11] . --- tests/test_basics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index 72866e5aea..ea7224a3ac 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -630,7 +630,7 @@ def before_send(event, hint): def test_event_processor_drop_records_client_report( sentry_init, capture_events, capture_record_lost_event_calls ): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") events = capture_events() record_lost_event_calls = capture_record_lost_event_calls() @@ -654,7 +654,6 @@ def foo(event, hint): assert Counter(record_lost_event_calls) == Counter( [ ("event_processor", "error", None, 1), - ("event_processor", "span", None, 1), ] ) From ba6794021100f545eb193b82b2239a2d5715d45f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 10:09:03 +0200 Subject: [PATCH 06/11] chore: Adapt more tests (2) --- tests/integrations/httpx/test_httpx.py | 8 ++------ tests/integrations/httpx2/test_httpx2.py | 8 ++------ tests/integrations/pyreqwest/test_pyreqwest.py | 7 +++---- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 899533ec2a..210dcbb95b 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -392,8 +392,7 @@ def test_option_trace_propagation_targets_sync( integrations=[HttpxIntegration()], ) - with sentry_sdk.start_transaction(): - httpx.Client().get(url) + httpx.Client().get(url) request_headers = httpx_mock.get_request().headers @@ -471,8 +470,7 @@ async def test_option_trace_propagation_targets_async( integrations=[HttpxIntegration()], ) - with sentry_sdk.start_transaction(): - await httpx.AsyncClient().get(url) + await httpx.AsyncClient().get(url) request_headers = httpx_mock.get_request().headers @@ -634,7 +632,6 @@ def test_outgoing_trace_headers_no_current_span(sentry_init, httpx_mock): httpx_client = httpx.Client() - # No start_span / start_transaction -> get_current_span() is None assert sentry_sdk.traces.get_current_span() is None response = httpx_client.get(url) @@ -672,7 +669,6 @@ async def test_outgoing_trace_headers_no_current_span_async(sentry_init, httpx_m httpx_client = httpx.AsyncClient() - # No start_span / start_transaction -> get_current_span() is None assert sentry_sdk.traces.get_current_span() is None response = await httpx_client.get(url) diff --git a/tests/integrations/httpx2/test_httpx2.py b/tests/integrations/httpx2/test_httpx2.py index 629d3d5139..a22c40039d 100644 --- a/tests/integrations/httpx2/test_httpx2.py +++ b/tests/integrations/httpx2/test_httpx2.py @@ -392,8 +392,7 @@ def test_option_trace_propagation_targets_sync( integrations=[Httpx2Integration()], ) - with sentry_sdk.traces.start_span(name="span"): - httpx2.Client().get(url) + httpx2.Client().get(url) request_headers = httpx2_mock.get_request().headers @@ -471,8 +470,7 @@ async def test_option_trace_propagation_targets_async( integrations=[Httpx2Integration()], ) - with sentry_sdk.traces.start_span(name="span"): - await httpx2.AsyncClient().get(url) + await httpx2.AsyncClient().get(url) request_headers = httpx2_mock.get_request().headers @@ -634,7 +632,6 @@ def test_outgoing_trace_headers_no_current_span(sentry_init, httpx2_mock): httpx2_client = httpx2.Client() - # No start_span / start_transaction -> get_current_span() is None assert sentry_sdk.traces.get_current_span() is None response = httpx2_client.get(url) @@ -672,7 +669,6 @@ async def test_outgoing_trace_headers_no_current_span_async(sentry_init, httpx2_ httpx2_client = httpx2.AsyncClient() - # No start_span / start_transaction -> get_current_span() is None assert sentry_sdk.traces.get_current_span() is None response = await httpx2_client.get(url) diff --git a/tests/integrations/pyreqwest/test_pyreqwest.py b/tests/integrations/pyreqwest/test_pyreqwest.py index adb4d45de6..48fed1f09f 100644 --- a/tests/integrations/pyreqwest/test_pyreqwest.py +++ b/tests/integrations/pyreqwest/test_pyreqwest.py @@ -11,7 +11,7 @@ from pyreqwest.simple.sync_request import pyreqwest_get as sync_pyreqwest_get import sentry_sdk -from sentry_sdk import capture_message, start_transaction +from sentry_sdk import capture_message from sentry_sdk.consts import MATCH_ALL, SPANDATA from sentry_sdk.integrations.pyreqwest import PyreqwestIntegration from tests.conftest import ApproxDict, get_free_port @@ -350,9 +350,8 @@ def test_trace_propagation_targets( url = f"http://localhost:{server_port}/propagation" - with start_transaction(): - client = SyncClientBuilder().build() - client.get(url).build().send() + client = SyncClientBuilder().build() + client.get(url).build().send() assert len(PyreqwestMockHandler.captured_requests) == 1 headers = PyreqwestMockHandler.captured_requests[0]["headers"] From 495410d99c38bec4267eee0e90f3e7fc1d69b598 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 10:24:18 +0200 Subject: [PATCH 07/11] chore: Adapt more tests (3) --- tests/profiler/test_continuous_profiler.py | 319 --------------------- tests/test_client.py | 10 - tests/test_dsc.py | 297 ------------------- tests/test_logs.py | 33 --- tests/test_metrics.py | 17 -- tests/test_monitor.py | 32 --- tests/test_scope.py | 51 +--- tests/test_scrubber.py | 21 +- 8 files changed, 6 insertions(+), 774 deletions(-) diff --git a/tests/profiler/test_continuous_profiler.py b/tests/profiler/test_continuous_profiler.py index b47e9355d0..1f5fb65be5 100644 --- a/tests/profiler/test_continuous_profiler.py +++ b/tests/profiler/test_continuous_profiler.py @@ -286,60 +286,6 @@ def test_continuous_profiler_auto_start_and_manual_stop( capture_envelopes, mode, teardown_profiling, -): - options = make_options(mode=mode, auto_start=True) - sentry_init( - traces_sample_rate=1.0, - **options, - ) - - envelopes = capture_envelopes() - - thread = threading.current_thread() - - with sentry_sdk.start_transaction(name="profiling"): - with sentry_sdk.start_span(op="op"): - pass - - for _ in range(3): - stop_profiler() - - assert_single_transaction_with_profile_chunks(envelopes, thread) - - envelopes.clear() - - with sentry_sdk.start_transaction(name="profiling"): - with sentry_sdk.start_span(op="op"): - time.sleep(0.05) - - assert_single_transaction_without_profile_chunks(envelopes) - - start_profiler() - - envelopes.clear() - - with sentry_sdk.start_transaction(name="profiling"): - with sentry_sdk.start_span(op="op"): - pass - - stop_profiler() - - assert_single_transaction_with_profile_chunks(envelopes, thread) - - -@pytest.mark.forked -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -def test_continuous_profiler_auto_start_and_manual_stop_span_streaming( - sentry_init, - capture_envelopes, - mode, - teardown_profiling, ): options = make_options(mode=mode, auto_start=True) sentry_init( @@ -398,63 +344,6 @@ def test_continuous_profiler_manual_start_and_stop_sampled( capture_envelopes, mode, teardown_profiling, -): - options = make_options( - mode=mode, profile_session_sample_rate=1.0, lifecycle="manual" - ) - sentry_init( - traces_sample_rate=1.0, - **options, - ) - - envelopes = capture_envelopes() - - thread = threading.current_thread() - - for _ in range(3): - start_profiler() - - envelopes.clear() - - with sentry_sdk.start_transaction(name="profiling"): - assert get_profiler_id() is not None, "profiler should be running" - with sentry_sdk.start_span(op="op"): - time.sleep(0.1) - assert get_profiler_id() is not None, "profiler should be running" - - assert get_profiler_id() is not None, "profiler should be running" - - stop_profiler() - - assert_single_transaction_with_profile_chunks(envelopes, thread) - - # the profiler stops immediately in manual mode - assert get_profiler_id() is None, "profiler should not be running" - - envelopes.clear() - - with sentry_sdk.start_transaction(name="profiling"): - assert get_profiler_id() is None, "profiler should not be running" - with sentry_sdk.start_span(op="op"): - time.sleep(0.1) - assert get_profiler_id() is None, "profiler should not be running" - - assert_single_transaction_without_profile_chunks(envelopes) - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@mock.patch("sentry_sdk.profiler.continuous_profiler.PROFILE_BUFFER_SECONDS", 0.01) -def test_continuous_profiler_manual_start_and_stop_sampled_span_streaming( - sentry_init, - capture_envelopes, - mode, - teardown_profiling, ): options = make_options( mode=mode, profile_session_sample_rate=1.0, lifecycle="manual" @@ -514,40 +403,6 @@ def test_continuous_profiler_manual_start_and_stop_unsampled( capture_envelopes, mode, teardown_profiling, -): - options = make_options( - mode=mode, profile_session_sample_rate=0.0, lifecycle="manual" - ) - sentry_init( - traces_sample_rate=1.0, - **options, - ) - - envelopes = capture_envelopes() - - start_profiler() - - with sentry_sdk.start_transaction(name="profiling"): - with sentry_sdk.start_span(op="op"): - pass - - stop_profiler() - - assert_single_transaction_without_profile_chunks(envelopes) - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -def test_continuous_profiler_manual_start_and_stop_unsampled_span_streaming( - sentry_init, - capture_envelopes, - mode, - teardown_profiling, ): options = make_options( mode=mode, profile_session_sample_rate=0.0, lifecycle="manual" @@ -585,79 +440,6 @@ def test_continuous_profiler_auto_start_and_stop_sampled( capture_envelopes, mode, teardown_profiling, -): - options = make_options( - mode=mode, profile_session_sample_rate=1.0, lifecycle="trace" - ) - sentry_init( - traces_sample_rate=1.0, - **options, - ) - - envelopes = capture_envelopes() - - thread = threading.current_thread() - - all_profiler_ids = set() - - for _ in range(3): - envelopes.clear() - - profiler_ids = set() - - with sentry_sdk.start_transaction(name="profiling 1"): - profiler_id = get_profiler_id() - assert profiler_id is not None, "profiler should be running" - profiler_ids.add(profiler_id) - with sentry_sdk.start_span(op="op"): - time.sleep(0.1) - profiler_id = get_profiler_id() - assert profiler_id is not None, "profiler should be running" - profiler_ids.add(profiler_id) - - time.sleep(0.03) - - # the profiler takes a while to stop in auto mode so if we start - # a transaction immediately, it'll be part of the same chunk - profiler_id = get_profiler_id() - assert profiler_id is not None, "profiler should be running" - profiler_ids.add(profiler_id) - - with sentry_sdk.start_transaction(name="profiling 2"): - profiler_id = get_profiler_id() - assert profiler_id is not None, "profiler should be running" - profiler_ids.add(profiler_id) - with sentry_sdk.start_span(op="op"): - time.sleep(0.1) - profiler_id = get_profiler_id() - assert profiler_id is not None, "profiler should be running" - profiler_ids.add(profiler_id) - - wait_for_profiler_to_stop(envelopes) - - assert len(profiler_ids) == 1 - all_profiler_ids.add(profiler_ids.pop()) - - assert_single_transaction_with_profile_chunks( - envelopes, thread, max_chunks=1, transactions=2 - ) - - assert len(all_profiler_ids) == 3 - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@mock.patch("sentry_sdk.profiler.continuous_profiler.DEFAULT_SAMPLING_FREQUENCY", 21) -def test_continuous_profiler_auto_start_and_stop_sampled_span_streaming( - sentry_init, - capture_envelopes, - mode, - teardown_profiling, ): options = make_options( mode=mode, profile_session_sample_rate=1.0, lifecycle="trace" @@ -733,43 +515,6 @@ def test_continuous_profiler_auto_start_and_stop_unsampled( capture_envelopes, mode, teardown_profiling, -): - options = make_options( - mode=mode, profile_session_sample_rate=0.0, lifecycle="trace" - ) - sentry_init( - traces_sample_rate=1.0, - **options, - ) - - envelopes = capture_envelopes() - - for _ in range(3): - envelopes.clear() - - with sentry_sdk.start_transaction(name="profiling"): - assert get_profiler_id() is None, "profiler should not be running" - with sentry_sdk.start_span(op="op"): - time.sleep(0.05) - assert get_profiler_id() is None, "profiler should not be running" - - assert get_profiler_id() is None, "profiler should not be running" - assert_single_transaction_without_profile_chunks(envelopes) - - -@pytest.mark.parametrize( - "mode", - [ - pytest.param("thread"), - pytest.param("gevent", marks=requires_gevent), - ], -) -@mock.patch("sentry_sdk.profiler.continuous_profiler.PROFILE_BUFFER_SECONDS", 0.01) -def test_continuous_profiler_auto_start_and_stop_unsampled_span_streaming( - sentry_init, - capture_envelopes, - mode, - teardown_profiling, ): options = make_options( mode=mode, profile_session_sample_rate=0.0, lifecycle="trace" @@ -894,70 +639,6 @@ def test_continuous_profiler_run_does_not_null_buffer( """ from sentry_sdk.profiler import continuous_profiler as cp - options = make_options( - mode="thread", profile_session_sample_rate=1.0, lifecycle="manual" - ) - sentry_init(traces_sample_rate=1.0, **options) - envelopes = capture_envelopes() - thread = threading.current_thread() - - # Start and verify profiler works - start_profiler() - envelopes.clear() - with sentry_sdk.start_transaction(name="profiling"): - with sentry_sdk.start_span(op="op"): - pass - - # Get the scheduler and create a sentinel buffer. - # We'll call run() directly to verify it doesn't null out self.buffer. - scheduler = cp._scheduler - assert scheduler is not None - - # Stop the profiler so the thread exits cleanly - stop_profiler() - - assert_single_transaction_with_profile_chunks(envelopes, thread) - - # Now set up a fresh buffer and mark the scheduler as not running - # (simulating the state right after ensure_running() created a new buffer - # but the old thread hasn't done cleanup yet). - scheduler.reset_buffer() - buffer_before = scheduler.buffer - assert buffer_before is not None - - # Simulate what happens when run() exits its while loop: - # self.running is already False, so the while loop exits immediately. - scheduler.running = False - scheduler.run() - - # After the fix, run() should NOT have set self.buffer = None. - # It should only flush using a local reference. - assert scheduler.buffer is not None, ( - "run() must not set self.buffer = None; " - "this would destroy buffers created by concurrent ensure_running() calls" - ) - - -def test_continuous_profiler_run_does_not_null_buffer_span_streaming( - sentry_init, - capture_envelopes, - teardown_profiling, -): - """ - Verifies that ContinuousScheduler.run() does not set self.buffer = None - after exiting its sampling loop. - - Previously, run() would execute `self.buffer = None` after the while - loop exited. During rapid stop/start cycles, this could race with - ensure_running() which creates a new buffer: the old thread's cleanup - would destroy the newly-created buffer, causing the new profiler thread - to silently drop all samples (self.buffer is None in the sampler). - - The fix uses a local buffer reference for flushing and never sets - self.buffer = None from run(). - """ - from sentry_sdk.profiler import continuous_profiler as cp - options = make_options( mode="thread", profile_session_sample_rate=1.0, lifecycle="manual" ) diff --git a/tests/test_client.py b/tests/test_client.py index 0d010ec99e..4c434ca57e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -19,7 +19,6 @@ capture_exception, capture_message, set_tag, - start_transaction, ) from sentry_sdk._compat import PY38 from sentry_sdk.consts import DEFAULT_MAX_BREADCRUMBS @@ -566,15 +565,6 @@ def test_attach_stacktrace_disabled(sentry_init, capture_events): assert "threads" not in event -def test_attach_stacktrace_transaction(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0, attach_stacktrace=True) - events = capture_events() - with start_transaction(name="transaction"): - pass - (event,) = events - assert "threads" not in event - - def test_capture_event_works(sentry_init): sentry_init(transport=_TestTransport()) pytest.raises(EnvelopeCapturedError, lambda: capture_event({})) diff --git a/tests/test_dsc.py b/tests/test_dsc.py index 7aa4669c60..ea1aa2fa3b 100644 --- a/tests/test_dsc.py +++ b/tests/test_dsc.py @@ -17,61 +17,6 @@ def test_dsc_head_of_trace(sentry_init, capture_envelopes): - """ - Our service is the head of the trace (it starts a new trace) - and sends a transaction event to Sentry. - """ - sentry_init( - dsn="https://mysecret@o1234.ingest.sentry.io/12312012", - release="myapp@0.0.1", - environment="canary", - traces_sample_rate=1.0, - transport=TestTransportWithOptions, - ) - envelopes = capture_envelopes() - - # We start a new transaction - with sentry_sdk.start_transaction(name="foo"): - pass - - assert len(envelopes) == 1 - - transaction_envelope = envelopes[0] - envelope_trace_header = transaction_envelope.headers["trace"] - - assert "trace_id" in envelope_trace_header - assert type(envelope_trace_header["trace_id"]) == str - - assert "public_key" in envelope_trace_header - assert type(envelope_trace_header["public_key"]) == str - assert envelope_trace_header["public_key"] == "mysecret" - - assert "org_id" in envelope_trace_header - assert type(envelope_trace_header["org_id"]) == str - assert envelope_trace_header["org_id"] == "1234" - - assert "sample_rate" in envelope_trace_header - assert type(envelope_trace_header["sample_rate"]) == str - assert envelope_trace_header["sample_rate"] == "1.0" - - assert "sampled" in envelope_trace_header - assert type(envelope_trace_header["sampled"]) == str - assert envelope_trace_header["sampled"] == "true" - - assert "release" in envelope_trace_header - assert type(envelope_trace_header["release"]) == str - assert envelope_trace_header["release"] == "myapp@0.0.1" - - assert "environment" in envelope_trace_header - assert type(envelope_trace_header["environment"]) == str - assert envelope_trace_header["environment"] == "canary" - - assert "transaction" in envelope_trace_header - assert type(envelope_trace_header["transaction"]) == str - assert envelope_trace_header["transaction"] == "foo" - - -def test_dsc_head_of_trace_span_streaming(sentry_init, capture_envelopes): """ Our service is the head of the trace (it starts a new trace) and sends a segment to Sentry. @@ -130,37 +75,6 @@ def test_dsc_head_of_trace_span_streaming(sentry_init, capture_envelopes): def test_dsc_head_of_trace_uses_custom_org_id(sentry_init, capture_envelopes): - """ - Our service is the head of the trace (it starts a new trace) - and sends a transaction event to Sentry. - """ - sentry_init( - dsn="https://mysecret@o1234.ingest.sentry.io/12312012", - org_id="9999", - release="myapp@0.0.1", - environment="canary", - traces_sample_rate=1.0, - transport=TestTransportWithOptions, - ) - envelopes = capture_envelopes() - - # We start a new transaction - with sentry_sdk.start_transaction(name="foo"): - pass - - assert len(envelopes) == 1 - - transaction_envelope = envelopes[0] - envelope_trace_header = transaction_envelope.headers["trace"] - - assert "org_id" in envelope_trace_header - assert type(envelope_trace_header["org_id"]) == str - assert envelope_trace_header["org_id"] == "9999" - - -def test_dsc_head_of_trace_uses_custom_org_id_span_streaming( - sentry_init, capture_envelopes -): """ Our service is the head of the trace (it starts a new trace) and sends a segment to Sentry. @@ -193,77 +107,6 @@ def test_dsc_head_of_trace_uses_custom_org_id_span_streaming( def test_dsc_continuation_of_trace(sentry_init, capture_envelopes): - """ - Another service calls our service and passes tracing information to us. - Our service is continuing the trace and sends a transaction event to Sentry. - """ - sentry_init( - dsn="https://mysecret@o1234.ingest.sentry.io/12312012", - release="myapp@0.0.1", - environment="canary", - traces_sample_rate=1.0, - transport=TestTransportWithOptions, - ) - envelopes = capture_envelopes() - - # This is what the upstream service sends us - sentry_trace = "771a43a4192642f0b136d5159a501700-1234567890abcdef-1" - baggage = ( - "other-vendor-value-1=foo;bar;baz, " - "sentry-trace_id=771a43a4192642f0b136d5159a501700, " - "sentry-public_key=frontendpublickey, " - "sentry-sample_rate=0.01337, " - "sentry-sampled=true, " - "sentry-release=myfrontend@1.2.3, " - "sentry-environment=bird, " - "sentry-transaction=bar, " - "other-vendor-value-2=foo;bar;" - ) - incoming_http_headers = { - "HTTP_SENTRY_TRACE": sentry_trace, - "HTTP_BAGGAGE": baggage, - } - - # We continue the incoming trace and start a new transaction - transaction = sentry_sdk.continue_trace(incoming_http_headers) - with sentry_sdk.start_transaction(transaction, name="foo"): - pass - - assert len(envelopes) == 1 - - transaction_envelope = envelopes[0] - envelope_trace_header = transaction_envelope.headers["trace"] - - assert "trace_id" in envelope_trace_header - assert type(envelope_trace_header["trace_id"]) == str - assert envelope_trace_header["trace_id"] == "771a43a4192642f0b136d5159a501700" - - assert "public_key" in envelope_trace_header - assert type(envelope_trace_header["public_key"]) == str - assert envelope_trace_header["public_key"] == "frontendpublickey" - - assert "sample_rate" in envelope_trace_header - assert type(envelope_trace_header["sample_rate"]) == str - assert envelope_trace_header["sample_rate"] == "1.0" - - assert "sampled" in envelope_trace_header - assert type(envelope_trace_header["sampled"]) == str - assert envelope_trace_header["sampled"] == "true" - - assert "release" in envelope_trace_header - assert type(envelope_trace_header["release"]) == str - assert envelope_trace_header["release"] == "myfrontend@1.2.3" - - assert "environment" in envelope_trace_header - assert type(envelope_trace_header["environment"]) == str - assert envelope_trace_header["environment"] == "bird" - - assert "transaction" in envelope_trace_header - assert type(envelope_trace_header["transaction"]) == str - assert envelope_trace_header["transaction"] == "bar" - - -def test_dsc_continuation_of_trace_span_streaming(sentry_init, capture_envelopes): """ Another service calls our service and passes tracing information to us. Our service is continuing the trace and sends a segment to Sentry. @@ -346,85 +189,6 @@ def test_dsc_continuation_of_trace_sample_rate_changed_in_traces_sampler( The DSC propagated further should contain the updated sample rate. """ - def my_traces_sampler(sampling_context): - return 0.25 - - sentry_init( - dsn="https://mysecret@o1234.ingest.sentry.io/12312012", - release="myapp@0.0.1", - environment="canary", - traces_sampler=my_traces_sampler, - transport=TestTransportWithOptions, - ) - envelopes = capture_envelopes() - - # This is what the upstream service sends us - sentry_trace = "771a43a4192642f0b136d5159a501700-1234567890abcdef-1" - baggage = ( - "other-vendor-value-1=foo;bar;baz, " - "sentry-trace_id=771a43a4192642f0b136d5159a501700, " - "sentry-public_key=frontendpublickey, " - "sentry-sample_rate=1.0, " - "sentry-sampled=true, " - "sentry-release=myfrontend@1.2.3, " - "sentry-environment=bird, " - "sentry-transaction=bar, " - "other-vendor-value-2=foo;bar;" - ) - incoming_http_headers = { - "HTTP_SENTRY_TRACE": sentry_trace, - "HTTP_BAGGAGE": baggage, - } - - # We continue the incoming trace and start a new transaction - with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=125000): - transaction = sentry_sdk.continue_trace(incoming_http_headers) - with sentry_sdk.start_transaction(transaction, name="foo"): - pass - - assert len(envelopes) == 1 - - transaction_envelope = envelopes[0] - envelope_trace_header = transaction_envelope.headers["trace"] - - assert "trace_id" in envelope_trace_header - assert type(envelope_trace_header["trace_id"]) == str - assert envelope_trace_header["trace_id"] == "771a43a4192642f0b136d5159a501700" - - assert "public_key" in envelope_trace_header - assert type(envelope_trace_header["public_key"]) == str - assert envelope_trace_header["public_key"] == "frontendpublickey" - - assert "sample_rate" in envelope_trace_header - assert type(envelope_trace_header["sample_rate"]) == str - assert envelope_trace_header["sample_rate"] == "0.25" - - assert "sampled" in envelope_trace_header - assert type(envelope_trace_header["sampled"]) == str - assert envelope_trace_header["sampled"] == "true" - - assert "release" in envelope_trace_header - assert type(envelope_trace_header["release"]) == str - assert envelope_trace_header["release"] == "myfrontend@1.2.3" - - assert "environment" in envelope_trace_header - assert type(envelope_trace_header["environment"]) == str - assert envelope_trace_header["environment"] == "bird" - - assert "transaction" in envelope_trace_header - assert type(envelope_trace_header["transaction"]) == str - assert envelope_trace_header["transaction"] == "bar" - - -def test_dsc_continuation_of_trace_sample_rate_changed_in_traces_sampler_span_streaming( - sentry_init, capture_envelopes -): - """ - Another service calls our service and passes tracing information to us. - Our service is continuing the trace, but modifies the sample rate. - The DSC propagated further should contain the updated sample rate. - """ - def my_traces_sampler(sampling_context): return 0.25 @@ -600,67 +364,6 @@ def test_dsc_issue_span_streaming(sentry_init, capture_envelopes): def test_dsc_issue_with_tracing(sentry_init, capture_envelopes): - """ - Our service has tracing enabled and an error occurs in an transaction. - Envelopes containing errors also have the same DSC than the transaction envelopes. - """ - sentry_init( - dsn="https://mysecret@o1234.ingest.sentry.io/12312012", - release="myapp@0.0.1", - environment="canary", - traces_sample_rate=1.0, - transport=TestTransportWithOptions, - ) - envelopes = capture_envelopes() - - # We start a new transaction and an error occurs - with sentry_sdk.start_transaction(name="foo"): - try: - 1 / 0 - except ZeroDivisionError as exp: - sentry_sdk.capture_exception(exp) - - assert len(envelopes) == 2 - - error_envelope, transaction_envelope = envelopes - - assert error_envelope.headers["trace"] == transaction_envelope.headers["trace"] - - envelope_trace_header = error_envelope.headers["trace"] - - assert "trace_id" in envelope_trace_header - assert type(envelope_trace_header["trace_id"]) == str - - assert "public_key" in envelope_trace_header - assert type(envelope_trace_header["public_key"]) == str - assert envelope_trace_header["public_key"] == "mysecret" - - assert "org_id" in envelope_trace_header - assert type(envelope_trace_header["org_id"]) == str - assert envelope_trace_header["org_id"] == "1234" - - assert "sample_rate" in envelope_trace_header - assert envelope_trace_header["sample_rate"] == "1.0" - assert type(envelope_trace_header["sample_rate"]) == str - - assert "sampled" in envelope_trace_header - assert type(envelope_trace_header["sampled"]) == str - assert envelope_trace_header["sampled"] == "true" - - assert "release" in envelope_trace_header - assert type(envelope_trace_header["release"]) == str - assert envelope_trace_header["release"] == "myapp@0.0.1" - - assert "environment" in envelope_trace_header - assert type(envelope_trace_header["environment"]) == str - assert envelope_trace_header["environment"] == "canary" - - assert "transaction" in envelope_trace_header - assert type(envelope_trace_header["transaction"]) == str - assert envelope_trace_header["transaction"] == "foo" - - -def test_dsc_issue_with_tracing_span_streaming(sentry_init, capture_envelopes): """ Our service has tracing enabled and an error occurs in a segment. Envelopes containing errors also have the same DSC as the segment envelopes. diff --git a/tests/test_logs.py b/tests/test_logs.py index 2761ac6742..08d97f356d 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -226,23 +226,6 @@ def test_logs_message_params(sentry_init, capture_items): assert "sentry.message.template" not in logs[5]["attributes"] -def test_logs_tied_to_transactions(sentry_init, capture_items): - """ - Log messages are also tied to transactions. - """ - sentry_init(traces_sample_rate=1.0) - items = capture_items("log") - - with sentry_sdk.start_transaction(name="test-transaction") as trx: - sentry_sdk.logger.warning("This is a log tied to a transaction") - - get_client().flush() - logs = [item.payload for item in items] - - assert "span_id" in logs[0] - assert logs[0]["span_id"] == trx.span_id - - def test_logs_tied_to_segments(sentry_init, capture_items): """ Log messages are also tied to segments. @@ -278,22 +261,6 @@ def test_logs_no_span_id_without_active_span(sentry_init, capture_items): def test_logs_tied_to_spans(sentry_init, capture_items): - """ - Log messages are also tied to spans. - """ - sentry_init(traces_sample_rate=1.0) - items = capture_items("log") - - with sentry_sdk.start_transaction(name="test-transaction"): - with sentry_sdk.start_span(name="test-span") as span: - sentry_sdk.logger.warning("This is a log tied to a span") - - get_client().flush() - logs = [item.payload for item in items] - assert logs[0]["span_id"] == span.span_id - - -def test_logs_tied_to_spans_span_streaming(sentry_init, capture_items): """ Log messages are also tied to spans. """ diff --git a/tests/test_metrics.py b/tests/test_metrics.py index d177176943..fad2981c4a 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -102,23 +102,6 @@ def test_metrics_no_user_if_pii_off(sentry_init, capture_items): def test_metrics_with_span(sentry_init, capture_items): - sentry_init(traces_sample_rate=1.0) - items = capture_items("trace_metric") - - with sentry_sdk.start_transaction(op="test", name="test-span") as transaction: - sentry_sdk.metrics.count("test.span.counter", 1) - - get_client().flush() - - metrics = [item.payload for item in items] - assert len(metrics) == 1 - - assert metrics[0]["trace_id"] is not None - assert metrics[0]["trace_id"] == transaction.trace_id - assert metrics[0]["span_id"] == transaction.span_id - - -def test_metrics_with_span_span_streaming(sentry_init, capture_items): sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") items = capture_items("trace_metric") diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 96902abca2..fd992e48cc 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -58,38 +58,6 @@ def test_monitor_unhealthy(sentry_init): assert monitor.downsample_factor == (i + 1 if i < 10 else 10) -def test_transaction_uses_downsampled_rate( - sentry_init, capture_record_lost_event_calls, monkeypatch -): - sentry_init( - traces_sample_rate=1.0, - transport=UnhealthyTestTransport(), - ) - - record_lost_event_calls = capture_record_lost_event_calls() - - monitor = sentry_sdk.get_client().monitor - monitor.interval = 0.1 - - assert monitor.is_healthy() is True - monitor.run() - assert monitor.is_healthy() is False - assert monitor.downsample_factor == 1 - - # make sure we don't sample the transaction - with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=750000): - with sentry_sdk.start_transaction(name="foobar") as transaction: - assert transaction.sampled is False - assert transaction.sample_rate == 0.5 - - assert Counter(record_lost_event_calls) == Counter( - [ - ("backpressure", "transaction", None, 1), - ("backpressure", "span", None, 1), - ] - ) - - def test_segment_uses_downsampled_rate( sentry_init, capture_record_lost_event_calls, monkeypatch ): diff --git a/tests/test_scope.py b/tests/test_scope.py index f05f17b313..2ac3ff61fc 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -827,36 +827,6 @@ def test_with_use_scope_data(): } -def test_nested_scopes_with_tags(sentry_init, capture_envelopes): - sentry_init(traces_sample_rate=1.0) - envelopes = capture_envelopes() - - with sentry_sdk.isolation_scope() as scope1: - scope1.set_tag("isolation_scope1", 1) - - with sentry_sdk.new_scope() as scope2: - scope2.set_tag("current_scope2", 1) - - with sentry_sdk.start_transaction(name="trx") as trx: - trx.set_tag("trx", 1) - - with sentry_sdk.start_span(op="span1") as span1: - span1.set_tag("a", 1) - - with new_scope() as scope3: - scope3.set_tag("current_scope3", 1) - - with sentry_sdk.start_span(op="span2") as span2: - span2.set_tag("b", 1) - - (envelope,) = envelopes - transaction = envelope.items[0].get_transaction_event() - - assert transaction["tags"] == {"isolation_scope1": 1, "current_scope2": 1, "trx": 1} - assert transaction["spans"][0]["tags"] == {"a": 1} - assert transaction["spans"][1]["tags"] == {"b": 1} - - def test_should_send_default_pii_true(sentry_init): sentry_init(send_default_pii=True) @@ -923,17 +893,6 @@ def test_last_event_id(sentry_init): assert Scope.last_event_id() is not None -def test_last_event_id_transaction(sentry_init): - sentry_init(traces_sample_rate=1.0) - - assert Scope.last_event_id() is None - - with sentry_sdk.start_transaction(name="test"): - pass - - assert Scope.last_event_id() is None, "Transaction should not set last_event_id" - - def test_last_event_id_cleared(sentry_init): sentry_init(traces_sample_rate=1.0) @@ -1014,14 +973,14 @@ def test_handle_error_on_token_reset_isolation_scope(error_cls, scope_manager): def test_trace_context_tracing(sentry_init): - sentry_init(traces_sample_rate=1.0) + sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - with sentry_sdk.start_transaction(name="trx") as transaction: - with sentry_sdk.start_span(op="span1"): - with sentry_sdk.start_span(op="span2") as span: + with sentry_sdk.traces.start_span(name="seg") as segment: + with sentry_sdk.traces.start_span(name="span1"): + with sentry_sdk.traces.start_span(name="span2") as span: trace_context = sentry_sdk.get_current_scope().get_trace_context() - assert trace_context["trace_id"] == transaction.trace_id + assert trace_context["trace_id"] == segment.trace_id assert trace_context["span_id"] == span.span_id assert trace_context["parent_span_id"] == span.parent_span_id assert "dynamic_sampling_context" in trace_context diff --git a/tests/test_scrubber.py b/tests/test_scrubber.py index 589ea2cd48..1b301cee86 100644 --- a/tests/test_scrubber.py +++ b/tests/test_scrubber.py @@ -1,11 +1,10 @@ import logging import sys -from sentry_sdk import capture_event, capture_exception, start_span, start_transaction +from sentry_sdk import capture_event, capture_exception from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.scrubber import EventScrubber from sentry_sdk.utils import event_from_exception -from tests.conftest import ApproxDict logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -192,24 +191,6 @@ def test_breadcrumb_extra_scrubbing(sentry_init, capture_events): } -def test_span_data_scrubbing(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - with start_transaction(name="hi"): - with start_span(op="foo", name="bar") as span: - span.set_data("password", "secret") - span.set_data("datafoo", "databar") - - (event,) = events - assert event["spans"][0]["data"] == ApproxDict( - {"password": "[Filtered]", "datafoo": "databar"} - ) - assert event["_meta"]["spans"] == { - "0": {"data": {"password": {"": {"rem": [["!config", "s"]]}}}} - } - - def test_custom_denylist(sentry_init, capture_events): sentry_init( event_scrubber=EventScrubber( From 5354ac8388095bd474b5bd7d5442fbb57ea454b5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 10:31:18 +0200 Subject: [PATCH 08/11] fix --- tests/test_scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_scope.py b/tests/test_scope.py index 2ac3ff61fc..ee7643a77f 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -982,7 +982,7 @@ def test_trace_context_tracing(sentry_init): assert trace_context["trace_id"] == segment.trace_id assert trace_context["span_id"] == span.span_id - assert trace_context["parent_span_id"] == span.parent_span_id + assert trace_context["parent_span_id"] == span._parent_span_id assert "dynamic_sampling_context" in trace_context From d4e2e5b116bc624221ecd2a383ece6d78501fddb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 10:39:44 +0200 Subject: [PATCH 09/11] chore: Remove trace template, trace_ignore_status_codes --- MIGRATION_GUIDE.md | 2 + sentry_sdk/client.py | 5 - sentry_sdk/consts.py | 22 - sentry_sdk/tracing.py | 26 - tests/tracing/test_decorator.py | 552 +--------------------- tests/tracing/test_ignore_status_codes.py | 139 ------ 6 files changed, 3 insertions(+), 743 deletions(-) delete mode 100644 tests/tracing/test_ignore_status_codes.py diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 45caaaac79..717b840c3e 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -26,6 +26,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - In the AWS Lambda and GCP integrations, the message of the warning the SDK optionally emits if a function is about to time out has changed. - We changed the way we emit warnings. Deprecations will from now on be always emitted using `warnings.warn()`, while all other warnings will be emitted using `logger.warning()`. - `sentry_sdk.init()` can no longer be used as a context manager. +- The `@trace` decorator doesn't accept a `template` parameter anymore. ### Logging @@ -150,6 +151,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - The SDK won't set any tags on its own anymore. - The `update_current_span` API was removed. - `SanicIntegration` no longer accepts `unsampled_statuses`. +- The `trace_ignore_status_codes` option was removed. ## Deprecated diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index bd0974ea6e..ce184ffd44 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -364,11 +364,6 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False ) - if rv["trace_ignore_status_codes"] and has_span_streaming_enabled(rv): - logger.warning( - "The `trace_ignore_status_codes` parameter is ignored in span streaming mode.", - ) - if rv["ignore_spans"] and not has_span_streaming_enabled(rv): logger.warning( "The `ignore_spans` parameter only works when `trace_lifecycle` is set to `stream`.", diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 8f3996cc28..7e7a25f65d 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -29,7 +29,6 @@ class CompressionAlgo(Enum): if TYPE_CHECKING: from typing import ( - AbstractSet, Any, Callable, Dict, @@ -94,16 +93,6 @@ class CompressionAlgo(Enum): ] -class SPANTEMPLATE(str, Enum): - DEFAULT = "default" - AI_AGENT = "ai_agent" - AI_TOOL = "ai_tool" - AI_CHAT = "ai_chat" - - def __str__(self) -> str: - return self.value - - class SPANNAME: DB_COMMIT = "COMMIT" DB_ROLLBACK = "ROLLBACK" @@ -1344,7 +1333,6 @@ def __init__( add_full_stack: bool = DEFAULT_ADD_FULL_STACK, max_stack_frames: "Optional[int]" = DEFAULT_MAX_STACK_FRAMES, before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, - trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, before_send_span: "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]" = None, org_id: "Optional[str]" = None, @@ -1725,16 +1713,6 @@ 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. - - This option has no effect in span streaming mode (`trace_lifecycle="stream"`). - :param strict_trace_continuation: If set to `True`, the SDK will only continue a trace if the `org_id` of the incoming trace found in the `baggage` header matches the `org_id` of the current Sentry client and only if BOTH are present. diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 895b2a0671..4cf1dafe9d 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -915,32 +915,6 @@ 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. diff --git a/tests/tracing/test_decorator.py b/tests/tracing/test_decorator.py index 85703f2276..954ffe4323 100644 --- a/tests/tracing/test_decorator.py +++ b/tests/tracing/test_decorator.py @@ -4,7 +4,6 @@ import pytest import sentry_sdk -from sentry_sdk.consts import SPANTEMPLATE from sentry_sdk.tracing_utils import create_span_decorator from sentry_sdk.utils import logger from tests.conftest import patch_start_tracing_child @@ -354,6 +353,7 @@ def _some_function_traced_stream(a, b, c): async def test_functions_to_trace_signature_unchanged_async(sentry_init): sentry_init( traces_sample_rate=1.0, + trace_lifecycle="stream", ) async def _some_function(a, b, c): @@ -373,553 +373,3 @@ async def _some_function_traced_stream(a, b, c): assert inspect.getcallargs(_some_function, 1, 2, 3) == inspect.getcallargs( _some_function_traced_stream, 1, 2, 3 ) - - -@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) -def test_span_templates_ai_dicts( - sentry_init, - capture_events, - capture_items, - stream_gen_ai_spans, -): - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def my_tool(arg1, arg2): - return { - "output": "my_tool_result", - "usage": { - "prompt_tokens": 10, - "completion_tokens": 20, - "total_tokens": 30, - }, - } - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_CHAT) - def my_chat(model=None, **kwargs): - return { - "content": "my_chat_result", - "usage": { - "input_tokens": 11, - "output_tokens": 22, - "total_tokens": 33, - }, - "model": f"{model}-v123", - } - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_AGENT) - def my_agent(): - my_tool(1, 2) - my_chat( - model="my-gpt-4o-mini", - prompt="What is the weather in Tokyo?", - system_prompt="You are a helpful assistant that can answer questions about the weather.", - max_tokens=100, - temperature=0.5, - top_p=0.9, - top_k=40, - frequency_penalty=1.0, - presence_penalty=2.0, - ) - - if stream_gen_ai_spans: - sentry_init( - traces_sample_rate=1.0, - stream_gen_ai_spans=stream_gen_ai_spans, - ) - items = capture_items("span") - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent() - - (agent_span, tool_span, chat_span) = (item.payload for item in items) - - assert ( - agent_span["name"] - == "invoke_agent test_decorator.test_span_templates_ai_dicts..my_agent" - ) - assert agent_span["attributes"] == { - "gen_ai.agent.name": "test_decorator.test_span_templates_ai_dicts..my_agent", - "gen_ai.operation.name": "invoke_agent", - "process.runtime.name": mock.ANY, - "process.runtime.version": mock.ANY, - "sentry.environment": "production", - "sentry.op": "gen_ai.invoke_agent", - "sentry.origin": "manual", - "sentry.release": mock.ANY, - "sentry.sdk.name": "sentry.python", - "sentry.sdk.version": mock.ANY, - "sentry.segment.id": mock.ANY, - "sentry.segment.name": "test-transaction", - "server.address": mock.ANY, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - assert ( - tool_span["name"] - == "execute_tool test_decorator.test_span_templates_ai_dicts..my_tool" - ) - assert tool_span["attributes"] == { - "gen_ai.tool.name": "test_decorator.test_span_templates_ai_dicts..my_tool", - "gen_ai.operation.name": "execute_tool", - "gen_ai.usage.input_tokens": 10, - "gen_ai.usage.output_tokens": 20, - "gen_ai.usage.total_tokens": 30, - "process.runtime.name": mock.ANY, - "process.runtime.version": mock.ANY, - "sentry.environment": "production", - "sentry.op": "gen_ai.execute_tool", - "sentry.origin": "manual", - "sentry.release": mock.ANY, - "sentry.sdk.name": "sentry.python", - "sentry.sdk.version": mock.ANY, - "sentry.segment.id": mock.ANY, - "sentry.segment.name": "test-transaction", - "server.address": mock.ANY, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - assert "gen_ai.tool.description" not in tool_span["attributes"] - - assert chat_span["name"] == "chat my-gpt-4o-mini" - assert chat_span["attributes"] == { - "gen_ai.operation.name": "chat", - "gen_ai.request.frequency_penalty": 1.0, - "gen_ai.request.max_tokens": 100, - "gen_ai.request.messages": "[{'role': 'user', 'content': 'What is the weather in Tokyo?'}, {'role': 'system', 'content': 'You are a helpful assistant that can answer questions about the weather.'}]", - "gen_ai.request.model": "my-gpt-4o-mini", - "gen_ai.request.presence_penalty": 2.0, - "gen_ai.request.temperature": 0.5, - "gen_ai.request.top_k": 40, - "gen_ai.request.top_p": 0.9, - "gen_ai.response.model": "my-gpt-4o-mini-v123", - "gen_ai.usage.input_tokens": 11, - "gen_ai.usage.output_tokens": 22, - "gen_ai.usage.total_tokens": 33, - "process.runtime.name": mock.ANY, - "process.runtime.version": mock.ANY, - "sentry.environment": "production", - "sentry.op": "gen_ai.chat", - "sentry.origin": "manual", - "sentry.release": mock.ANY, - "sentry.sdk.name": "sentry.python", - "sentry.sdk.version": mock.ANY, - "sentry.segment.id": mock.ANY, - "sentry.segment.name": "test-transaction", - "server.address": mock.ANY, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - else: - sentry_init( - traces_sample_rate=1.0, - stream_gen_ai_spans=stream_gen_ai_spans, - ) - events = capture_events() - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent() - - (event,) = events - (agent_span, tool_span, chat_span) = event["spans"] - - assert agent_span["op"] == "gen_ai.invoke_agent" - assert ( - agent_span["description"] - == "invoke_agent test_decorator.test_span_templates_ai_dicts..my_agent" - ) - assert agent_span["data"] == { - "gen_ai.agent.name": "test_decorator.test_span_templates_ai_dicts..my_agent", - "gen_ai.operation.name": "invoke_agent", - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - assert tool_span["op"] == "gen_ai.execute_tool" - assert ( - tool_span["description"] - == "execute_tool test_decorator.test_span_templates_ai_dicts..my_tool" - ) - assert tool_span["data"] == { - "gen_ai.tool.name": "test_decorator.test_span_templates_ai_dicts..my_tool", - "gen_ai.operation.name": "execute_tool", - "gen_ai.usage.input_tokens": 10, - "gen_ai.usage.output_tokens": 20, - "gen_ai.usage.total_tokens": 30, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - assert "gen_ai.tool.description" not in tool_span["data"] - - assert chat_span["op"] == "gen_ai.chat" - assert chat_span["description"] == "chat my-gpt-4o-mini" - assert chat_span["data"] == { - "gen_ai.operation.name": "chat", - "gen_ai.request.frequency_penalty": 1.0, - "gen_ai.request.max_tokens": 100, - "gen_ai.request.messages": "[{'role': 'user', 'content': 'What is the weather in Tokyo?'}, {'role': 'system', 'content': 'You are a helpful assistant that can answer questions about the weather.'}]", - "gen_ai.request.model": "my-gpt-4o-mini", - "gen_ai.request.presence_penalty": 2.0, - "gen_ai.request.temperature": 0.5, - "gen_ai.request.top_k": 40, - "gen_ai.request.top_p": 0.9, - "gen_ai.response.model": "my-gpt-4o-mini-v123", - "gen_ai.usage.input_tokens": 11, - "gen_ai.usage.output_tokens": 22, - "gen_ai.usage.total_tokens": 33, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - -@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) -def test_span_templates_ai_objects( - sentry_init, - capture_events, - capture_items, - stream_gen_ai_spans, -): - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def my_tool(arg1, arg2): - """This is a tool function.""" - mock_usage = mock.Mock() - mock_usage.prompt_tokens = 10 - mock_usage.completion_tokens = 20 - mock_usage.total_tokens = 30 - - mock_result = mock.Mock() - mock_result.output = "my_tool_result" - mock_result.usage = mock_usage - - return mock_result - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_CHAT) - def my_chat(model=None, **kwargs): - mock_result = mock.Mock() - mock_result.content = "my_chat_result" - mock_result.usage = mock.Mock( - input_tokens=11, - output_tokens=22, - total_tokens=33, - ) - mock_result.model = f"{model}-v123" - - return mock_result - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_AGENT) - def my_agent(): - my_tool(1, 2) - my_chat( - model="my-gpt-4o-mini", - prompt="What is the weather in Tokyo?", - system_prompt="You are a helpful assistant that can answer questions about the weather.", - max_tokens=100, - temperature=0.5, - top_p=0.9, - top_k=40, - frequency_penalty=1.0, - presence_penalty=2.0, - ) - - sentry_init( - traces_sample_rate=1.0, - stream_gen_ai_spans=stream_gen_ai_spans, - ) - - if stream_gen_ai_spans: - items = capture_items("span") - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent() - - (agent_span, tool_span, chat_span) = (item.payload for item in items) - - assert ( - agent_span["name"] - == "invoke_agent test_decorator.test_span_templates_ai_objects..my_agent" - ) - assert agent_span["attributes"] == { - "gen_ai.agent.name": "test_decorator.test_span_templates_ai_objects..my_agent", - "gen_ai.operation.name": "invoke_agent", - "process.runtime.name": mock.ANY, - "process.runtime.version": mock.ANY, - "sentry.environment": "production", - "sentry.op": "gen_ai.invoke_agent", - "sentry.origin": "manual", - "sentry.release": mock.ANY, - "sentry.sdk.name": "sentry.python", - "sentry.sdk.version": mock.ANY, - "sentry.segment.id": mock.ANY, - "sentry.segment.name": "test-transaction", - "server.address": mock.ANY, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - assert ( - tool_span["name"] - == "execute_tool test_decorator.test_span_templates_ai_objects..my_tool" - ) - assert tool_span["attributes"] == { - "gen_ai.tool.name": "test_decorator.test_span_templates_ai_objects..my_tool", - "gen_ai.tool.description": "This is a tool function.", - "gen_ai.operation.name": "execute_tool", - "gen_ai.usage.input_tokens": 10, - "gen_ai.usage.output_tokens": 20, - "gen_ai.usage.total_tokens": 30, - "process.runtime.name": mock.ANY, - "process.runtime.version": mock.ANY, - "sentry.environment": "production", - "sentry.op": "gen_ai.execute_tool", - "sentry.origin": "manual", - "sentry.release": mock.ANY, - "sentry.sdk.name": "sentry.python", - "sentry.sdk.version": mock.ANY, - "sentry.segment.id": mock.ANY, - "sentry.segment.name": "test-transaction", - "server.address": mock.ANY, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - assert chat_span["name"] == "chat my-gpt-4o-mini" - assert chat_span["attributes"] == { - "gen_ai.operation.name": "chat", - "gen_ai.request.frequency_penalty": 1.0, - "gen_ai.request.max_tokens": 100, - "gen_ai.request.messages": "[{'role': 'user', 'content': 'What is the weather in Tokyo?'}, {'role': 'system', 'content': 'You are a helpful assistant that can answer questions about the weather.'}]", - "gen_ai.request.model": "my-gpt-4o-mini", - "gen_ai.request.presence_penalty": 2.0, - "gen_ai.request.temperature": 0.5, - "gen_ai.request.top_k": 40, - "gen_ai.request.top_p": 0.9, - "gen_ai.response.model": "my-gpt-4o-mini-v123", - "gen_ai.usage.input_tokens": 11, - "gen_ai.usage.output_tokens": 22, - "gen_ai.usage.total_tokens": 33, - "process.runtime.name": mock.ANY, - "process.runtime.version": mock.ANY, - "sentry.environment": "production", - "sentry.op": "gen_ai.chat", - "sentry.origin": "manual", - "sentry.release": mock.ANY, - "sentry.sdk.name": "sentry.python", - "sentry.sdk.version": mock.ANY, - "sentry.segment.id": mock.ANY, - "sentry.segment.name": "test-transaction", - "server.address": mock.ANY, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - else: - events = capture_events() - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent() - - (event,) = events - (agent_span, tool_span, chat_span) = event["spans"] - - assert agent_span["op"] == "gen_ai.invoke_agent" - assert ( - agent_span["description"] - == "invoke_agent test_decorator.test_span_templates_ai_objects..my_agent" - ) - assert agent_span["data"] == { - "gen_ai.agent.name": "test_decorator.test_span_templates_ai_objects..my_agent", - "gen_ai.operation.name": "invoke_agent", - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - assert tool_span["op"] == "gen_ai.execute_tool" - assert ( - tool_span["description"] - == "execute_tool test_decorator.test_span_templates_ai_objects..my_tool" - ) - assert tool_span["data"] == { - "gen_ai.tool.name": "test_decorator.test_span_templates_ai_objects..my_tool", - "gen_ai.tool.description": "This is a tool function.", - "gen_ai.operation.name": "execute_tool", - "gen_ai.usage.input_tokens": 10, - "gen_ai.usage.output_tokens": 20, - "gen_ai.usage.total_tokens": 30, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - assert chat_span["op"] == "gen_ai.chat" - assert chat_span["description"] == "chat my-gpt-4o-mini" - assert chat_span["data"] == { - "gen_ai.operation.name": "chat", - "gen_ai.request.frequency_penalty": 1.0, - "gen_ai.request.max_tokens": 100, - "gen_ai.request.messages": "[{'role': 'user', 'content': 'What is the weather in Tokyo?'}, {'role': 'system', 'content': 'You are a helpful assistant that can answer questions about the weather.'}]", - "gen_ai.request.model": "my-gpt-4o-mini", - "gen_ai.request.presence_penalty": 2.0, - "gen_ai.request.temperature": 0.5, - "gen_ai.request.top_k": 40, - "gen_ai.request.top_p": 0.9, - "gen_ai.response.model": "my-gpt-4o-mini-v123", - "gen_ai.usage.input_tokens": 11, - "gen_ai.usage.output_tokens": 22, - "gen_ai.usage.total_tokens": 33, - "thread.id": mock.ANY, - "thread.name": mock.ANY, - } - - -@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) -@pytest.mark.parametrize("send_default_pii", [True, False]) -def test_span_templates_ai_pii( - sentry_init, - capture_events, - capture_items, - send_default_pii, - stream_gen_ai_spans, -): - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def my_tool(arg1, arg2, **kwargs): - """This is a tool function.""" - return "tool_output" - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_CHAT) - def my_chat(model=None, **kwargs): - return "chat_output" - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_AGENT) - def my_agent(*args, **kwargs): - my_tool(1, 2, tool_arg1="3", tool_arg2="4") - my_chat( - model="my-gpt-4o-mini", - prompt="What is the weather in Tokyo?", - system_prompt="You are a helpful assistant that can answer questions about the weather.", - max_tokens=100, - temperature=0.5, - top_p=0.9, - top_k=40, - frequency_penalty=1.0, - presence_penalty=2.0, - ) - return "agent_output" - - sentry_init( - traces_sample_rate=1.0, - send_default_pii=send_default_pii, - stream_gen_ai_spans=stream_gen_ai_spans, - ) - - if stream_gen_ai_spans: - items = capture_items("span") - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent(22, 33, arg1=44, arg2=55) - - (_, tool_span, chat_span) = (item.payload for item in items) - tool_data = tool_span["attributes"] - chat_data = chat_span["attributes"] - else: - events = capture_events() - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent(22, 33, arg1=44, arg2=55) - - (event,) = events - (_, tool_span, chat_span) = event["spans"] - tool_data = tool_span["data"] - chat_data = chat_span["data"] - - if send_default_pii: - assert ( - tool_data["gen_ai.tool.input"] - == "{'args': (1, 2), 'kwargs': {'tool_arg1': '3', 'tool_arg2': '4'}}" - ) - assert tool_data["gen_ai.tool.output"] == "'tool_output'" - else: - assert "gen_ai.tool.input" not in tool_data - assert "gen_ai.tool.output" not in tool_data - - # Without `data_collection`, prompts are recorded regardless of `send_default_pii`. - assert chat_data["gen_ai.request.messages"] == ( - "[{'role': 'user', 'content': 'What is the weather in Tokyo?'}, " - "{'role': 'system', 'content': 'You are a helpful assistant that can answer " - "questions about the weather.'}]" - ) - - -@pytest.mark.parametrize("stream_gen_ai_spans", [True, False]) -@pytest.mark.parametrize("collect_outputs", [True, False]) -@pytest.mark.parametrize("collect_inputs", [True, False]) -def test_span_templates_ai_data_collection( - sentry_init, - capture_events, - capture_items, - collect_inputs, - collect_outputs, - stream_gen_ai_spans, -): - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def my_tool(arg1, arg2, **kwargs): - """This is a tool function.""" - return "tool_output" - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_CHAT) - def my_chat(model=None, **kwargs): - return "chat_output" - - @sentry_sdk.trace(template=SPANTEMPLATE.AI_AGENT) - def my_agent(*args, **kwargs): - my_tool(1, 2, tool_arg1="3", tool_arg2="4") - my_chat( - model="my-gpt-4o-mini", - prompt="What is the weather in Tokyo?", - system_prompt="You are a helpful assistant.", - ) - return "agent_output" - - sentry_init( - traces_sample_rate=1.0, - stream_gen_ai_spans=stream_gen_ai_spans, - _experiments={ - "data_collection": { - "gen_ai": {"inputs": collect_inputs, "outputs": collect_outputs} - } - }, - ) - - if stream_gen_ai_spans: - items = capture_items("span") - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent(22, 33, arg1=44, arg2=55) - - (_, tool_span, chat_span) = (item.payload for item in items) - tool_data = tool_span["attributes"] - chat_data = chat_span["attributes"] - else: - events = capture_events() - - with sentry_sdk.start_transaction(name="test-transaction"): - my_agent(22, 33, arg1=44, arg2=55) - - (event,) = events - (_, tool_span, chat_span) = event["spans"] - tool_data = tool_span["data"] - chat_data = chat_span["data"] - - if collect_inputs: - assert ( - tool_data["gen_ai.tool.input"] - == "{'args': (1, 2), 'kwargs': {'tool_arg1': '3', 'tool_arg2': '4'}}" - ) - assert chat_data["gen_ai.request.messages"] == ( - "[{'role': 'user', 'content': 'What is the weather in Tokyo?'}, " - "{'role': 'system', 'content': 'You are a helpful assistant.'}]" - ) - else: - assert "gen_ai.tool.input" not in tool_data - assert "gen_ai.request.messages" not in chat_data - - if collect_outputs: - assert tool_data["gen_ai.tool.output"] == "'tool_output'" - else: - assert "gen_ai.tool.output" not in tool_data diff --git a/tests/tracing/test_ignore_status_codes.py b/tests/tracing/test_ignore_status_codes.py deleted file mode 100644 index fdad1a1e46..0000000000 --- a/tests/tracing/test_ignore_status_codes.py +++ /dev/null @@ -1,139 +0,0 @@ -from collections import Counter - -import pytest - -import sentry_sdk -from sentry_sdk import start_span, start_transaction - - -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), - ] - ) From 00dd1510a15ca85cf3d7ed21d6559206c582e782 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 10:49:29 +0200 Subject: [PATCH 10/11] template --- sentry_sdk/tracing.py | 20 +-- sentry_sdk/tracing_utils.py | 244 +----------------------------------- 2 files changed, 7 insertions(+), 257 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 4cf1dafe9d..e101217fc3 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, cast import sentry_sdk -from sentry_sdk.consts import SPANDATA, SPANSTATUS, SPANTEMPLATE +from sentry_sdk.consts import SPANDATA, SPANSTATUS from sentry_sdk.profiler.continuous_profiler import get_profiler_id from sentry_sdk.utils import ( capture_internal_exceptions, @@ -1222,7 +1222,6 @@ def trace( op: "Optional[str]" = None, name: "Optional[str]" = None, attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, ) -> "Callable[[Callable[P, R]], Callable[P, R]]": # Handles: @trace() and @trace(op="custom") pass @@ -1239,7 +1238,6 @@ def trace( op: "Optional[str]" = None, name: "Optional[str]" = None, attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, ) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": """ Decorator to start a child span around a function call. @@ -1269,13 +1267,6 @@ def trace( attributes provide additional context about the span's execution. :type attributes: dict[str, Any] or None - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` - :returns: When used as ``@trace``, returns the decorated function. When used as ``@trace(...)`` with parameters, returns a decorator function. :rtype: Callable or decorator function @@ -1283,7 +1274,7 @@ def trace( Example:: import sentry_sdk - from sentry_sdk.consts import OP, SPANTEMPLATE + from sentry_sdk.consts import OP # Simple usage with default values @sentry_sdk.trace @@ -1300,12 +1291,6 @@ def process_data(): def make_db_query(sql): # Function implementation pass - - # With a custom template - @sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL) - def calculate_interest_rate(amount, rate, years): - # Function implementation - pass """ from sentry_sdk.tracing_utils import create_span_decorator @@ -1313,7 +1298,6 @@ def calculate_interest_rate(amount, rate, years): op=op, name=name, attributes=attributes, - template=template, ) if func: diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 20257b070b..0fcfb4d473 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -13,7 +13,7 @@ from urllib.parse import quote, unquote import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANTEMPLATE +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.data_collection import ( _apply_data_collection_filtering_to_query_string, ) @@ -30,7 +30,6 @@ logger, match_regex_list, qualname_from_function, - safe_repr, to_string, try_convert, ) @@ -1019,7 +1018,6 @@ def create_span_decorator( op: "Optional[Union[str, OP]]" = None, name: "Optional[str]" = None, attributes: "Optional[dict[str, Any]]" = None, - template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT, ) -> "Any": """ Create a span decorator that can wrap both sync and async functions. @@ -1030,12 +1028,6 @@ def create_span_decorator( :type name: str or None :param attributes: Additional attributes to set on the span. :type attributes: dict or None - :param template: The type of span to create. This determines what kind of - span instrumentation and data collection will be applied. Use predefined - constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`. - The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most - use cases. - :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE` """ def span_decorator(f: "Any") -> "Any": @@ -1061,25 +1053,18 @@ async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": ) return await f(*args, **kwargs) - span_op = op or _get_span_op(template) + span_op = op or OP.FUNCTION function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - collect_inputs = _should_collect_gen_ai("inputs") - collect_outputs = _should_collect_gen_ai("outputs") + span_name = function_name with current_span.start_child( op=span_op, name=span_name, ) as span: span.update_data(attributes or {}) - _set_input_attributes( - span, template, collect_inputs, function_name, f, args, kwargs - ) result = await f(*args, **kwargs) - _set_output_attributes(span, template, collect_outputs, result) - return result try: @@ -1105,25 +1090,18 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": ) return f(*args, **kwargs) - span_op = op or _get_span_op(template) + span_op = op or OP.FUNCTION function_name = name or qualname_from_function(f) or "" - span_name = _get_span_name(template, function_name, kwargs) - collect_inputs = _should_collect_gen_ai("inputs") - collect_outputs = _should_collect_gen_ai("outputs") + span_name = function_name with current_span.start_child( op=span_op, name=span_name, ) as span: span.update_data(attributes or {}) - _set_input_attributes( - span, template, collect_inputs, function_name, f, args, kwargs - ) result = f(*args, **kwargs) - _set_output_attributes(span, template, collect_outputs, result) - return result try: @@ -1282,54 +1260,6 @@ def _get_value(source: "Any", key: str) -> "Optional[Any]": return value -def _get_span_name( - template: "Union[str, SPANTEMPLATE]", - name: str, - kwargs: "Optional[dict[str, Any]]" = None, -) -> str: - """ - Get the name of the span based on the template and the name. - """ - span_name = name - - if template == SPANTEMPLATE.AI_CHAT: - model = None - if kwargs: - for key in ("model", "model_name"): - if kwargs.get(key) and isinstance(kwargs[key], str): - model = kwargs[key] - break - - span_name = f"chat {model}" if model else "chat" - - elif template == SPANTEMPLATE.AI_AGENT: - span_name = f"invoke_agent {name}" - - elif template == SPANTEMPLATE.AI_TOOL: - span_name = f"execute_tool {name}" - - return span_name - - -def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str: - """ - Get the operation of the span based on the template. - """ - mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = { - SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT, - SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT, - SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL, - } - op = mapping.get(template, OP.FUNCTION) - - return str(op) - - -_AI_TEMPLATES = frozenset( - {SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_CHAT, SPANTEMPLATE.AI_TOOL} -) - - def _should_collect_gen_ai(kind: 'Literal["inputs", "outputs"]') -> bool: client = sentry_sdk.get_client() if has_data_collection_enabled(client.options): @@ -1338,69 +1268,6 @@ def _should_collect_gen_ai(kind: 'Literal["inputs", "outputs"]') -> bool: return client.should_send_default_pii() -def _get_input_attributes( - template: "Union[str, SPANTEMPLATE]", - collect_inputs: bool, - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> "dict[str, Any]": - """ - Get input attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in _AI_TEMPLATES: - mapping = { - "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str), - "agent": (SPANDATA.GEN_AI_AGENT_NAME, str), - "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str), - "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int), - "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float), - "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float), - "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float), - "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float), - "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int), - } - - def _set_from_key(key: str, value: "Any") -> None: - if key in mapping: - (attribute, data_type) = mapping[key] - if value is not None and isinstance(value, data_type): - attributes[attribute] = value - - # Pre-data collection, prompts were always recorded here, so they stay - # ungated until `send_default_pii` is removed. - collect_messages = True - if has_data_collection_enabled(sentry_sdk.get_client().options): - collect_messages = collect_inputs - - roles = {"prompt": "user", "system_prompt": "system"} - - for key, value in list(kwargs.items()): - if key in roles: - if collect_messages and isinstance(value, str): - attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append( - {"role": roles[key], "content": value} - ) - continue - - _set_from_key(key, value) - - if template == SPANTEMPLATE.AI_TOOL and collect_inputs: - attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr( - {"args": args, "kwargs": kwargs} - ) - - # Coerce to string - if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes: - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr( - attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] - ) - - return attributes - - def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": """ Get usage attributes. @@ -1429,107 +1296,6 @@ def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None: return attributes -def _get_output_attributes( - template: "Union[str, SPANTEMPLATE]", collect_outputs: bool, result: "Any" -) -> "dict[str, Any]": - """ - Get output attributes for the given span template. - """ - attributes: "dict[str, Any]" = {} - - if template in _AI_TEMPLATES: - with capture_internal_exceptions(): - # Usage from result, result.usage, and result.metadata.usage - usage_candidates = [result] - - usage = _get_value(result, "usage") - usage_candidates.append(usage) - - meta = _get_value(result, "metadata") - usage = _get_value(meta, "usage") - usage_candidates.append(usage) - - for usage_candidate in usage_candidates: - if usage_candidate is not None: - attributes.update(_get_usage_attributes(usage_candidate)) - - # Response model - model_name = _get_value(result, "model") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - model_name = _get_value(result, "model_name") - if model_name is not None and isinstance(model_name, str): - attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name - - # Tool output - if template == SPANTEMPLATE.AI_TOOL and collect_outputs: - attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result) - - return attributes - - -def _set_input_attributes( - span: "Span", - template: "Union[str, SPANTEMPLATE]", - collect_inputs: bool, - name: str, - f: "Any", - args: "tuple[Any, ...]", - kwargs: "dict[str, Any]", -) -> None: - """ - Set span input attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param collect_inputs: Whether gen_ai inputs may be collected. - :param f: The wrapped function. - :param args: The arguments to the wrapped function. - :param kwargs: The keyword arguments to the wrapped function. - """ - attributes: "dict[str, Any]" = {} - - if template == SPANTEMPLATE.AI_AGENT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent", - SPANDATA.GEN_AI_AGENT_NAME: name, - } - elif template == SPANTEMPLATE.AI_CHAT: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "chat", - } - elif template == SPANTEMPLATE.AI_TOOL: - attributes = { - SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool", - SPANDATA.GEN_AI_TOOL_NAME: name, - } - - docstring = f.__doc__ - if docstring is not None: - attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring - - attributes.update(_get_input_attributes(template, collect_inputs, args, kwargs)) - span.update_data(attributes or {}) - - -def _set_output_attributes( - span: "Span", - template: "Union[str, SPANTEMPLATE]", - collect_outputs: bool, - result: "Any", -) -> None: - """ - Set span output attributes based on the given span template. - - :param span: The span to set attributes on. - :param template: The template to use to set attributes on the span. - :param collect_outputs: Whether gen_ai outputs may be collected. - :param result: The result of the wrapped function. - """ - span.update_data(_get_output_attributes(template, collect_outputs, result) or {}) - - def _should_continue_trace(baggage: "Optional[Baggage]") -> bool: """ Check if we should continue the incoming trace according to the strict_trace_continuation spec. From b358be7af817004e4c2fafc3a92cc0f4b1cc148f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 10 Sep 2026 11:11:45 +0200 Subject: [PATCH 11/11] . --- sentry_sdk/tracing_utils.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 0fcfb4d473..95ff3944e4 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -41,7 +41,6 @@ Dict, Generator, Iterator, - Literal, Optional, Tuple, Union, @@ -1260,14 +1259,6 @@ def _get_value(source: "Any", key: str) -> "Optional[Any]": return value -def _should_collect_gen_ai(kind: 'Literal["inputs", "outputs"]') -> bool: - client = sentry_sdk.get_client() - if has_data_collection_enabled(client.options): - return bool(client.options["data_collection"]["gen_ai"][kind]) - - return client.should_send_default_pii() - - def _get_usage_attributes(usage: "Any") -> "dict[str, Any]": """ Get usage attributes.