From 86f7d016e208a3033e1b534eea6cce43db6c82a7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 16:25:55 +0200 Subject: [PATCH 01/28] chore: Migrate even more tests --- tests/conftest.py | 31 +--- tests/integrations/fastmcp/test_fastmcp.py | 24 +--- tests/integrations/mcp/test_mcp.py | 21 +-- tests/test_api.py | 6 +- tests/test_client.py | 86 ----------- tests/test_envelope.py | 41 +----- tests/test_transport.py | 160 +++++++-------------- tests/tracing/test_decorator.py | 15 -- tests/tracing/test_span_streaming.py | 134 ----------------- 9 files changed, 66 insertions(+), 452 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d57099cef0..e4140fdd5a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -395,7 +395,7 @@ def inner(): def append_event(envelope): for item in envelope: - if item.headers.get("type") in ("event", "transaction"): + if item.headers.get("type") == "event": events.append(item.payload.json) return old_capture_envelope(envelope) @@ -995,35 +995,6 @@ def inner(app, method: str, params, request_id: str): return inner -@pytest.fixture() -def select_mcp_transactions(): - def inner(events): - return [ - event - for event in events - if event["type"] == "transaction" - and event["contexts"]["trace"]["op"] == "mcp.server" - ] - - return inner - - -@pytest.fixture() -def select_transactions_with_mcp_spans(): - def inner(events, method_name): - return [ - transaction - for transaction in events - if transaction.get("type") == "transaction" - and any( - span["data"].get("mcp.method.name") == method_name - for span in transaction.get("spans", []) - ) - ] - - return inner - - @pytest.fixture() def json_rpc_sse(): class StreamingASGITransport(ASGITransport): diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index 6d59f2353c..da5debf1f8 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -347,13 +347,7 @@ def add_numbers(a: int, b: int) -> dict: [(True, True), (True, False), (False, True), (False, False)], ) async def test_fastmcp_tool_async( - sentry_init, - capture_items, - FastMCP, - send_default_pii, - include_prompts, - json_rpc, - select_transactions_with_mcp_spans, + sentry_init, capture_items, FastMCP, send_default_pii, include_prompts, json_rpc ): """Test that FastMCP async tool handlers create proper spans""" sentry_init( @@ -674,13 +668,7 @@ def read_file(path: str): ) @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.asyncio -async def test_fastmcp_resource_async( - sentry_init, - capture_items, - FastMCP, - json_rpc, - select_transactions_with_mcp_spans, -): +async def test_fastmcp_resource_async(sentry_init, capture_items, FastMCP, json_rpc): """Test that FastMCP async resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], @@ -882,13 +870,7 @@ def sse_tool(value: str) -> dict: @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) -def test_fastmcp_http_transport( - sentry_init, - capture_items, - FastMCP, - json_rpc, - select_transactions_with_mcp_spans, -): +def test_fastmcp_http_transport(sentry_init, capture_items, FastMCP, json_rpc): """Test that FastMCP correctly detects HTTP transport""" sentry_init( integrations=[MCPIntegration()], diff --git a/tests/integrations/mcp/test_mcp.py b/tests/integrations/mcp/test_mcp.py index 27b494c667..8e0c9ca3e7 100644 --- a/tests/integrations/mcp/test_mcp.py +++ b/tests/integrations/mcp/test_mcp.py @@ -383,12 +383,7 @@ async def test_tool(tool_name, arguments): [(True, True), (True, False), (False, True), (False, False)], ) async def test_tool_handler_streamable_http( - sentry_init, - capture_items, - send_default_pii, - include_prompts, - json_rpc, - select_transactions_with_mcp_spans, + sentry_init, capture_items, send_default_pii, include_prompts, json_rpc ): """Test that async tool handlers create proper spans""" sentry_init( @@ -688,12 +683,7 @@ async def test_prompt(name, arguments): [(True, True), (True, False), (False, True), (False, False)], ) async def test_prompt_handler_streamable_http( - sentry_init, - capture_items, - send_default_pii, - include_prompts, - json_rpc, - select_transactions_with_mcp_spans, + sentry_init, capture_items, send_default_pii, include_prompts, json_rpc ): """Test that async prompt handlers create proper spans""" sentry_init( @@ -890,12 +880,7 @@ async def test_resource(uri): @pytest.mark.asyncio -async def test_resource_handler_streamable_http( - sentry_init, - capture_items, - json_rpc, - select_transactions_with_mcp_spans, -): +async def test_resource_handler_streamable_http(sentry_init, capture_items, json_rpc): """Test that async resource handlers create proper spans""" sentry_init( integrations=[MCPIntegration()], diff --git a/tests/test_api.py b/tests/test_api.py index e4d3569172..ce57b2d224 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -21,10 +21,10 @@ def test_get_current_span(): fake_scope = mock.MagicMock() - fake_scope.span = mock.MagicMock() - assert get_current_span(fake_scope) == fake_scope.span + fake_scope.streamed_span = mock.MagicMock() + assert get_current_span(fake_scope) == fake_scope.streamed_span - fake_scope.span = None + fake_scope.streamed_span = None assert get_current_span(fake_scope) is None diff --git a/tests/test_client.py b/tests/test_client.py index 4c434ca57e..73b1d9fdc6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1375,92 +1375,6 @@ def run(self, sentry_init, capture_record_lost_event_calls): assert Counter(record_lost_event_calls) == self.expected_record_lost_event_calls -@pytest.mark.parametrize( - "test_config", - ( - TestSpanClientReports(span_count=10), # No spans dropped - TestSpanClientReports(span_count=0).with_before_send( - lambda e, _: None, - drops_transactions=1, - drops_spans=1, - ), - TestSpanClientReports(span_count=10).with_before_send( - lambda e, _: None, - drops_transactions=1, - drops_spans=11, - ), - TestSpanClientReports(span_count=10).with_before_send( - TestSpanClientReports.span_dropper(3), - drops_spans=3, - ), - TestSpanClientReports(span_count=10).with_before_send( - TestSpanClientReports.span_dropper(10), - drops_spans=10, - ), - TestSpanClientReports(span_count=10).with_event_processor( - lambda e, _: None, - drops_transactions=1, - drops_spans=11, - ), - TestSpanClientReports(span_count=10).with_event_processor( - TestSpanClientReports.span_dropper(3), - drops_spans=3, - ), - TestSpanClientReports(span_count=10).with_event_processor( - TestSpanClientReports.span_dropper(10), - drops_spans=10, - ), - TestSpanClientReports(span_count=10) - .with_event_processor( - TestSpanClientReports.span_dropper(3), - drops_spans=3, - ) - .with_before_send( - TestSpanClientReports.span_dropper(5), - drops_spans=5, - ), - TestSpanClientReports(10) - .with_event_processor( - TestSpanClientReports.span_dropper(3), - drops_spans=3, - ) - .with_before_send( - lambda e, _: None, - drops_transactions=1, - drops_spans=8, # 3 of the 11 (incl. transaction) spans already dropped - ), - ), -) -def test_dropped_transaction(sentry_init, capture_record_lost_event_calls, test_config): - test_config.run(sentry_init, capture_record_lost_event_calls) - - -def test_ignore_spans_warns_without_streaming(sentry_init): - with mock.patch("sentry_sdk.client.logger") as mock_logger: - sentry_init(ignore_spans=["/health"], trace_lifecycle="static") - mock_logger.warning.assert_any_call( - "The `ignore_spans` parameter only works when `trace_lifecycle` is set to `stream`.", - ) - - -@pytest.mark.parametrize( - "options", - [ - {"ignore_spans": ["/health"], "trace_lifecycle": "stream"}, - {"ignore_spans": ["/health"], "_experiments": {"trace_lifecycle": "stream"}}, - {}, - ], -) -def test_ignore_spans_does_not_warn(sentry_init, options): - with mock.patch("sentry_sdk.client.logger") as mock_logger: - sentry_init(**options) - - ignore_spans_warnings = [ - c for c in mock_logger.warning.call_args_list if "ignore_spans" in str(c) - ] - assert ignore_spans_warnings == [] - - def make_options_transport_cls(): """Make an options transport class that captures the options passed to it.""" # We need a unique class for each test so that the options are not diff --git a/tests/test_envelope.py b/tests/test_envelope.py index 15856c4bd0..7252195b5a 100644 --- a/tests/test_envelope.py +++ b/tests/test_envelope.py @@ -4,20 +4,14 @@ from sentry_sdk.session import Session -def generate_transaction_item(): +def generate_event_item(): return { "event_id": "15210411201320122115110420122013", - "type": "transaction", - "transaction": "/interactions/other-dogs/new-dog", - "start_timestamp": 1353568872.11122131, - "timestamp": 1356942672.09040815, + "message": "hello", "contexts": { "trace": { "trace_id": "12312012123120121231201212312012", "span_id": "0415201309082013", - "parent_span_id": None, - "description": "", - "op": "greeting.sniff", "dynamic_sampling_context": { "trace_id": "12312012123120121231201212312012", "sample_rate": "1.0", @@ -28,17 +22,6 @@ def generate_transaction_item(): }, } }, - "spans": [ - { - "description": "", - "op": "greeting.sniff", - "parent_span_id": None, - "span_id": "0415201309082013", - "start_timestamp": 1353568872.11122131, - "timestamp": 1356942672.09040815, - "trace_id": "12312012123120121231201212312012", - } - ], } @@ -51,22 +34,6 @@ def test_add_and_get_basic_event(): assert envelope.get_event() == {"message": "Hello, World!"} -def test_add_and_get_transaction_event(): - envelope = Envelope() - - transaction_item = generate_transaction_item() - transaction_item.update({"event_id": "a" * 32}) - envelope.add_transaction(transaction_item) - - # typically it should not be possible to be able to add a second transaction; - # but we do it anyways - another_transaction_item = generate_transaction_item() - envelope.add_transaction(another_transaction_item) - - # should only fetch the first inserted transaction event - assert envelope.get_transaction_event() == transaction_item - - def test_add_and_get_session(): envelope = Envelope() @@ -87,12 +54,11 @@ def test_envelope_headers(sentry_init, capture_envelopes, monkeypatch): sentry_init( dsn="https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012", - traces_sample_rate=1.0, trace_lifecycle="stream", ) envelopes = capture_envelopes() - capture_event(generate_transaction_item()) + capture_event(generate_event_item()) assert len(envelopes) == 1 @@ -246,7 +212,6 @@ def test_envelope_item_data_category_mapping(): """Test that envelope items map to correct data categories for rate limiting.""" test_cases = [ ("event", "error"), - ("transaction", "transaction"), ("log", "log_item"), ("session", "session"), ("attachment", "attachment"), diff --git a/tests/test_transport.py b/tests/test_transport.py index aa45dde76d..f29d6a4b65 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -31,8 +31,8 @@ Client, add_breadcrumb, capture_message, - get_isolation_scope, isolation_scope, + new_scope, ) from sentry_sdk._compat import PY37, PY38 from sentry_sdk.envelope import Envelope, Item, PayloadRef, parse_json @@ -476,24 +476,17 @@ def test_envelope_too_large_response(capturing_server, make_client): capturing_server.respond_with(code=413) client.capture_event({"type": "error"}) - client.capture_event({"type": "transaction"}) client.flush() - # Error, transaction, and client report payloads - assert len(capturing_server.captured) == 3 - report = parse_json(capturing_server.captured[2].envelope.items[0].get_bytes()) + # Error and client report payloads + assert len(capturing_server.captured) == 2 + report = parse_json(capturing_server.captured[-1].envelope.items[0].get_bytes()) - # Client reports for error, transaction and included span - assert len(report["discarded_events"]) == 3 + # Client report for error + assert len(report["discarded_events"]) == 1 assert {"reason": "send_error", "category": "error", "quantity": 1} in report[ "discarded_events" ] - assert {"reason": "send_error", "category": "span", "quantity": 1} in report[ - "discarded_events" - ] - assert {"reason": "send_error", "category": "transaction", "quantity": 1} in report[ - "discarded_events" - ] capturing_server.clear_captured() @@ -502,7 +495,7 @@ def test_simple_rate_limits(capturing_server, make_client): client = make_client() capturing_server.respond_with(code=429, headers={"Retry-After": "4"}) - client.capture_event({"type": "transaction"}) + client.capture_event({"type": "error"}) client.flush() assert len(capturing_server.captured) == 1 @@ -511,7 +504,7 @@ def test_simple_rate_limits(capturing_server, make_client): assert set(client.transport._disabled_until) == set([None]) - client.capture_event({"type": "transaction"}) + client.capture_event({"type": "error"}) client.capture_event({"type": "event"}) client.flush() @@ -535,33 +528,40 @@ def record_lost_event(reason, data_category=None, item=None): capturing_server.respond_with( code=response_code, - headers={"X-Sentry-Rate-Limits": "4711:transaction:organization"}, + headers={"X-Sentry-Rate-Limits": "4711:attachment:organization"}, ) - client.capture_event({"type": "transaction"}) + with new_scope() as scope: + scope.add_attachment(bytes=b"Hello", filename="hello.txt") + client.capture_event({"type": "error"}, scope=scope) client.flush() assert len(capturing_server.captured) == 1 assert capturing_server.captured[0].path == "/api/132/envelope/" capturing_server.clear_captured() - assert set(client.transport._disabled_until) == set(["transaction"]) + assert set(client.transport._disabled_until) == set(["attachment"]) - client.capture_event({"type": "transaction"}) - client.capture_event({"type": "transaction"}) + with new_scope() as scope: + scope.add_attachment(bytes=b"Hello", filename="hello.txt") + client.capture_event({"type": "error"}, scope=scope) + with new_scope() as scope: + scope.add_attachment(bytes=b"Hello", filename="hello.txt") + client.capture_event({"type": "error"}, scope=scope) client.flush() - assert not capturing_server.captured + # events go through but attachments are dropped + assert len(capturing_server.captured) == 2 - client.capture_event({"type": "event"}) + client.capture_event({"type": "error"}) client.flush() - assert len(capturing_server.captured) == 1 - assert capturing_server.captured[0].path == "/api/132/envelope/" + assert len(capturing_server.captured) == 3 + assert capturing_server.captured[2].path == "/api/132/envelope/" assert captured_outcomes == [ - ("ratelimit_backoff", "transaction"), - ("ratelimit_backoff", "transaction"), + ("ratelimit_backoff", "attachment"), + ("ratelimit_backoff", "attachment"), ] @@ -574,7 +574,7 @@ def test_data_category_limits_reporting( capturing_server.respond_with( code=response_code, headers={ - "X-Sentry-Rate-Limits": "4711:transaction:organization, 4711:attachment:organization" + "X-Sentry-Rate-Limits": "4711:error:organization, 4711:attachment:organization" }, ) @@ -591,17 +591,23 @@ def intercepting_fetch(*args, **kwargs): # get rid of threading making things hard to track monkeypatch.setattr(client.transport._worker, "submit", lambda x: x() or True) - client.capture_event({"type": "transaction"}) + with new_scope() as scope: + scope.add_attachment(bytes=b"Hello World", filename="hello.txt") + client.capture_event({"type": "error"}, scope=scope) client.flush() assert len(capturing_server.captured) == 1 assert capturing_server.captured[0].path == "/api/132/envelope/" capturing_server.clear_captured() - assert set(client.transport._disabled_until) == set(["attachment", "transaction"]) + assert set(client.transport._disabled_until) == set(["attachment", "error"]) - client.capture_event({"type": "transaction"}) - client.capture_event({"type": "transaction"}) + with new_scope() as scope: + scope.add_attachment(bytes=b"Hello World", filename="hello.txt") + client.capture_event({"type": "error"}, scope=scope) + with new_scope() as scope: + scope.add_attachment(bytes=b"Hello World", filename="hello.txt") + client.capture_event({"type": "error"}, scope=scope) capturing_server.clear_captured() # flush out the events but don't flush the client reports @@ -609,67 +615,45 @@ def intercepting_fetch(*args, **kwargs): client.transport._last_client_report_sent = 0 outcomes_enabled = True - scope = get_isolation_scope() - scope.add_attachment(bytes=b"Hello World", filename="hello.txt") - client.capture_event({"type": "error"}, scope=scope) + client.capture_event({"type": "error"}) client.flush() - # this goes out with an extra envelope because it's flushed after the last item - # that is normally in the queue. This is quite funny in a way because it means - # that the envelope that caused its own over quota report (an error with an - # attachment) will include its outcome since it's pending. + # the client report is flushed alongside the event (which is also dropped + # because error is rate-limited) assert len(capturing_server.captured) == 1 envelope = capturing_server.captured[0].envelope - assert envelope.items[0].type == "event" - assert envelope.items[1].type == "client_report" - report = parse_json(envelope.items[1].get_bytes()) + assert envelope.items[0].type == "client_report" + report = parse_json(envelope.items[0].get_bytes()) discarded_events = report["discarded_events"] - assert len(discarded_events) == 3 - assert { - "category": "transaction", - "reason": "ratelimit_backoff", - "quantity": 2, - } in discarded_events + assert len(discarded_events) == 2 assert { - "category": "span", + "category": "error", "reason": "ratelimit_backoff", - "quantity": 2, + "quantity": 3, } in discarded_events assert { "category": "attachment", "reason": "ratelimit_backoff", - "quantity": 11, + "quantity": 22, } in discarded_events capturing_server.clear_captured() - # here we sent a normal event - client.capture_event({"type": "transaction"}) - client.capture_event({"type": "error", "release": "foo"}) + # send more events (error is still rate-limited) + client.capture_event({"type": "error"}) client.flush() - assert len(capturing_server.captured) == 2 - - assert len(capturing_server.captured[0].envelope.items) == 1 - event = capturing_server.captured[0].envelope.items[0].get_event() - assert event["type"] == "error" - assert event["release"] == "foo" - - envelope = capturing_server.captured[1].envelope + assert len(capturing_server.captured) == 1 + envelope = capturing_server.captured[0].envelope assert envelope.items[0].type == "client_report" report = parse_json(envelope.items[0].get_bytes()) discarded_events = report["discarded_events"] - assert len(discarded_events) == 2 + assert len(discarded_events) == 1 assert { - "category": "transaction", - "reason": "ratelimit_backoff", - "quantity": 1, - } in discarded_events - assert { - "category": "span", + "category": "error", "reason": "ratelimit_backoff", "quantity": 1, } in discarded_events @@ -827,14 +811,11 @@ def test_log_item_limits(capturing_server, response_code, item, make_client): assert set(client.transport._disabled_until) == {"log_item"} client.transport.capture_envelope(envelope) - client.capture_event({"type": "transaction"}) client.flush() - assert len(capturing_server.captured) == 2 + assert len(capturing_server.captured) == 1 envelope = capturing_server.captured[0].envelope - assert envelope.items[0].type == "transaction" - envelope = capturing_server.captured[1].envelope assert envelope.items[0].type == "client_report" report = parse_json(envelope.items[0].get_bytes()) @@ -876,41 +857,6 @@ def test_record_lost_event_quantity(capturing_server, make_client, quantity): ] -@pytest.mark.parametrize("span_count", (0, 1, 2, 10)) -def test_record_lost_event_transaction_item(capturing_server, make_client, span_count): - client = make_client() - transport = client.transport - - envelope = mock_transaction_envelope(span_count) - (transaction_item,) = envelope.items - - transport.record_lost_event(reason="test", item=transaction_item) - client.flush() - - (captured,) = capturing_server.captured # Should only be one envelope - envelope = captured.envelope - (item,) = envelope.items # Envelope should only have one item - - assert item.type == "client_report" - - report = parse_json(item.get_bytes()) - discarded_events = report["discarded_events"] - - assert len(discarded_events) == 2 - - assert { - "category": "transaction", - "reason": "test", - "quantity": 1, - } in discarded_events - - assert { - "category": "span", - "reason": "test", - "quantity": span_count + 1, - } in discarded_events - - @skip_under_gevent @pytest.mark.asyncio @pytest.mark.parametrize( diff --git a/tests/tracing/test_decorator.py b/tests/tracing/test_decorator.py index 21287e39d5..eac6af2700 100644 --- a/tests/tracing/test_decorator.py +++ b/tests/tracing/test_decorator.py @@ -257,18 +257,10 @@ def test_functions_to_trace_signature_unchanged_sync(sentry_init): def _some_function(a, b, c): pass - @sentry_sdk.trace - def _some_function_traced(a, b, c): - pass - @sentry_sdk.traces.trace def _some_function_traced_stream(a, b, c): pass - assert inspect.getcallargs(_some_function, 1, 2, 3) == inspect.getcallargs( - _some_function_traced, 1, 2, 3 - ) - assert inspect.getcallargs(_some_function, 1, 2, 3) == inspect.getcallargs( _some_function_traced_stream, 1, 2, 3 ) @@ -284,17 +276,10 @@ async def test_functions_to_trace_signature_unchanged_async(sentry_init): async def _some_function(a, b, c): pass - @sentry_sdk.trace - async def _some_function_traced(a, b, c): - pass - @sentry_sdk.traces.trace async def _some_function_traced_stream(a, b, c): pass - assert inspect.getcallargs(_some_function, 1, 2, 3) == inspect.getcallargs( - _some_function_traced, 1, 2, 3 - ) assert inspect.getcallargs(_some_function, 1, 2, 3) == inspect.getcallargs( _some_function_traced_stream, 1, 2, 3 ) diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 638c3a4e83..295c963b1a 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -12,7 +12,6 @@ SpanStatus, StreamedSpan, ) -from sentry_sdk.tracing_utils import has_span_streaming_enabled minimum_python_38 = pytest.mark.skipif( sys.version_info < (3, 8), reason="Asyncio tests need Python >= 3.8" @@ -383,25 +382,6 @@ def before_send_span(span, hint): assert "mutated" not in span["attributes"] -def test_before_send_span_warns_without_span_streaming(sentry_init): - from unittest import mock - - def before_send_span(span, hint): - return span - - with mock.patch("sentry_sdk.client.logger") as mock_logger: - sentry_init( - traces_sample_rate=1.0, - before_send_span=before_send_span, - ) - - warnings = [ - c for c in mock_logger.warning.call_args_list if "before_send_span" in str(c) - ] - assert len(warnings) == 1 - assert "trace_lifecycle" in str(warnings[0]) - - def test_span_attributes(sentry_init, capture_items): sentry_init( traces_sample_rate=1.0, @@ -1773,52 +1753,6 @@ def test_default_attributes(sentry_init, capture_envelopes): } -@pytest.mark.parametrize( - ("options", "expected"), - [ - ({"trace_lifecycle": "stream"}, True), - ({"_experiments": {"trace_lifecycle": "stream"}}, True), - ( - { - "trace_lifecycle": "stream", - "_experiments": {"trace_lifecycle": "static"}, - }, - True, - ), - ( - { - "trace_lifecycle": "static", - "_experiments": {"trace_lifecycle": "stream"}, - }, - False, - ), - ({"trace_lifecycle": "static"}, False), - ({"_experiments": {"trace_lifecycle": "static"}}, False), - ({}, False), - ({"_experiments": {}}, False), - ({"_experiments": None}, False), - (None, False), - ], -) -def test_has_span_streaming_enabled(options, expected): - assert has_span_streaming_enabled(options) is expected - - -def test_trace_lifecycle_top_level_enables_streaming(sentry_init, capture_items): - sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - - items = capture_items("span") - - with sentry_sdk.traces.start_span(name="segment") as segment: - assert isinstance(segment, StreamedSpan) - - sentry_sdk.get_client().flush() - spans = [item.payload for item in items] - - assert len(spans) == 1 - assert spans[0]["name"] == "segment" - - @pytest.mark.parametrize( ("ignore_spans", "name", "attributes", "ignored"), IGNORE_SPANS_CASES ) @@ -1850,71 +1784,3 @@ def test_ignore_spans_top_level( assert len(spans) == 1 (span,) = spans assert span["name"] == name - - -def test_ignore_spans_top_level_with_trace_lifecycle_in_experiments( - sentry_init, capture_items -): - sentry_init( - traces_sample_rate=1.0, - ignore_spans=["ignored"], - _experiments={"trace_lifecycle": "stream"}, - ) - - items = capture_items("span") - - with sentry_sdk.traces.start_span(name="ignored") as ignored_span: - assert ignored_span.sampled is False - assert isinstance(ignored_span, NoOpStreamedSpan) - - with sentry_sdk.traces.start_span(name="not ignored") as span: - assert span.sampled is True - - sentry_sdk.get_client().flush() - spans = [item.payload for item in items] - - assert len(spans) == 1 - (span,) = spans - assert span["name"] == "not ignored" - - -@pytest.mark.parametrize( - ("options", "streaming_enabled"), - [ - ( - { - "trace_lifecycle": "stream", - "_experiments": {"trace_lifecycle": "static"}, - }, - True, - ), - ( - { - "trace_lifecycle": "static", - "_experiments": {"trace_lifecycle": "stream"}, - }, - False, - ), - ], -) -def test_top_level_trace_lifecycle_takes_precedence_over_experiments( - sentry_init, capture_items, options, streaming_enabled -): - sentry_init(traces_sample_rate=1.0, **options) - - items = capture_items("span") - - with sentry_sdk.traces.start_span(name="segment") as segment: - if streaming_enabled: - assert isinstance(segment, StreamedSpan) - else: - assert isinstance(segment, NoOpStreamedSpan) - - sentry_sdk.get_client().flush() - spans = [item.payload for item in items] - - if streaming_enabled: - assert len(spans) == 1 - assert spans[0]["name"] == "segment" - else: - assert len(spans) == 0 From 6e713bbd9ad2014c0494796faa8284040e152a2f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 16:29:29 +0200 Subject: [PATCH 02/28] more --- tests/conftest.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e4140fdd5a..01f3bf6d8b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,11 +7,9 @@ import threading import warnings from collections import namedtuple -from contextlib import contextmanager from dataclasses import dataclass from http.server import BaseHTTPRequestHandler, HTTPServer from threading import Thread -from unittest import mock from urllib.parse import parse_qs, urlparse try: @@ -85,7 +83,7 @@ if TYPE_CHECKING: from collections.abc import Iterator - from typing import Any, Callable, MutableMapping, Optional + from typing import Any, Callable, MutableMapping try: from httpx import ( @@ -501,7 +499,7 @@ def inner(): old_capture_envelope = test_client.transport.capture_envelope def append(envelope): - event = envelope.get_event() or envelope.get_transaction_event() + event = envelope.get_event() if event is not None: events_w.write(json.dumps(event).encode("utf-8")) events_w.write(b"\n") @@ -1738,24 +1736,6 @@ def werkzeug_set_cookie(client, servername, key, value): client.set_cookie(key, value) -@contextmanager -def patch_start_tracing_child( - fake_transaction_is_none: bool = False, -) -> "Iterator[Optional[mock.MagicMock]]": - if not fake_transaction_is_none: - fake_transaction = mock.MagicMock() - fake_start_child = mock.MagicMock() - fake_transaction.start_child = fake_start_child - else: - fake_transaction = None - fake_start_child = None - - with mock.patch( - "sentry_sdk.tracing_utils.get_current_span", return_value=fake_transaction - ): - yield fake_start_child - - class ApproxDict(dict): def __eq__(self, other): # For an ApproxDict to equal another dict, the other dict just needs to contain From 23304770d6e66b3b1e44ccffd2be85b5f829d005 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 16:34:47 +0200 Subject: [PATCH 03/28] not yet --- tests/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index ce57b2d224..e4d3569172 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -21,10 +21,10 @@ def test_get_current_span(): fake_scope = mock.MagicMock() - fake_scope.streamed_span = mock.MagicMock() - assert get_current_span(fake_scope) == fake_scope.streamed_span + fake_scope.span = mock.MagicMock() + assert get_current_span(fake_scope) == fake_scope.span - fake_scope.streamed_span = None + fake_scope.span = None assert get_current_span(fake_scope) is None From 8e9398367e360d634abc9d45c30f7cd48a22e6b9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 11:55:48 +0200 Subject: [PATCH 04/28] ref: Rewire top-level tracing API to span streaming --- sentry_sdk/api.py | 70 ++++++----------------------------------------- 1 file changed, 8 insertions(+), 62 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 7446b8613b..bf9a4231e6 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,11 +1,10 @@ import inspect from typing import TYPE_CHECKING -from sentry_sdk import Client, tracing_utils +from sentry_sdk import Client, traces from sentry_sdk._init_implementation import init from sentry_sdk.crons import monitor from sentry_sdk.scope import Scope, isolation_scope, new_scope -from sentry_sdk.tracing import NoOpSpan, Transaction, trace if TYPE_CHECKING: from collections.abc import Mapping @@ -19,8 +18,6 @@ overload, ) - from typing_extensions import Unpack - from sentry_sdk._types import ( Breadcrumb, BreadcrumbHint, @@ -28,10 +25,9 @@ ExcInfo, Hint, LogLevelStr, - SamplingContext, ) from sentry_sdk.client import BaseClient - from sentry_sdk.tracing import Span, TransactionKwargs + from sentry_sdk.traces import StreamedSpan T = TypeVar("T") F = TypeVar("F", bound=Callable[..., Any]) @@ -73,8 +69,6 @@ def overload(x: "T") -> "T": "set_tags", "set_user", "start_span", - "start_transaction", - "trace", "monitor", "start_session", "end_session", @@ -275,57 +269,17 @@ async def flush_async( @scopemethod def start_span( **kwargs: "Any", -) -> "Span": - return get_current_scope().start_span(**kwargs) - - -@scopemethod -def start_transaction( - transaction: "Optional[Transaction]" = None, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", -) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction on the current scope. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - return get_current_scope().start_transaction( - transaction, custom_sampling_context, **kwargs - ) +) -> "StreamedSpan": + return traces.start_span(**kwargs) def get_current_span( scope: "Optional[Scope]" = None, -) -> "Optional[Span]": +) -> "Optional[StreamedSpan]": """ Returns the currently active span if there is one running, otherwise `None` """ - return tracing_utils.get_current_span(scope) + return traces.get_current_span(scope) def get_traceparent() -> "Optional[str]": @@ -346,19 +300,11 @@ def get_baggage() -> "Optional[str]": return None -def continue_trace( - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", -) -> "Transaction": +def continue_trace(incoming: "Dict[str, Any]") -> None: """ Sets the propagation context from environment or headers and returns a transaction. """ - return get_isolation_scope().continue_trace( - environ_or_headers, op, name, source, origin - ) + return traces.continue_trace(incoming) @scopemethod From 51e4b4e34109673eeed1a0aef49f3048124ff528 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 11:57:23 +0200 Subject: [PATCH 05/28] formatting --- sentry_sdk/api.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index bf9a4231e6..79d941701f 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -267,15 +267,11 @@ async def flush_async( @scopemethod -def start_span( - **kwargs: "Any", -) -> "StreamedSpan": +def start_span(**kwargs: "Any") -> "StreamedSpan": return traces.start_span(**kwargs) -def get_current_span( - scope: "Optional[Scope]" = None, -) -> "Optional[StreamedSpan]": +def get_current_span(scope: "Optional[Scope]" = None) -> "Optional[StreamedSpan]": """ Returns the currently active span if there is one running, otherwise `None` """ From 9d22ba3cac945675d59574df23bf5603cea911d7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 12:02:21 +0200 Subject: [PATCH 06/28] more instances --- docs/api.rst | 1 - sentry_sdk/__init__.py | 2 +- sentry_sdk/api.py | 10 ++- sentry_sdk/scope.py | 155 ----------------------------------------- sentry_sdk/traces.py | 37 +--------- sentry_sdk/utils.py | 2 +- 6 files changed, 14 insertions(+), 193 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index b2925ef33f..199e54eb61 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,7 +41,6 @@ Performance Monitoring .. autofunction:: sentry_sdk.api.continue_trace .. autofunction:: sentry_sdk.api.get_current_span .. autofunction:: sentry_sdk.api.start_span -.. autofunction:: sentry_sdk.api.start_transaction Distributed Tracing diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 28e79202d8..417220ecc1 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -22,6 +22,7 @@ "capture_exception", "capture_message", "continue_trace", + "new_trace", "flush", "flush_async", "get_baggage", @@ -45,7 +46,6 @@ "set_tags", "set_user", "start_span", - "start_transaction", "trace", "monitor", "logger", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 79d941701f..e9a870aa76 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -46,6 +46,7 @@ def overload(x: "T") -> "T": "capture_exception", "capture_message", "continue_trace", + "new_trace", "flush", "flush_async", "get_baggage", @@ -298,11 +299,18 @@ def get_baggage() -> "Optional[str]": def continue_trace(incoming: "Dict[str, Any]") -> None: """ - Sets the propagation context from environment or headers and returns a transaction. + Sets the propagation context from environment or headers. """ return traces.continue_trace(incoming) +def new_trace() -> None: + """ + Resets the propagation context, forcing a new trace. + """ + return traces.new_trace() + + @scopemethod def start_session( session_mode: str = "application", diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 76ec2cda2a..f6ee43a8be 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -20,11 +20,6 @@ SPANDATA, ) from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer -from sentry_sdk.profiler.continuous_profiler import ( - get_profiler_id, - try_autostart_continuous_profiler, - try_profile_lifecycle_trace_start, -) from sentry_sdk.session import Session from sentry_sdk.traces import ( _DEFAULT_PARENT_SPAN, @@ -75,8 +70,6 @@ Union, ) - from typing_extensions import Unpack - import sentry_sdk from sentry_sdk._types import ( Attributes, @@ -91,10 +84,8 @@ Log, LogLevelStr, Metric, - SamplingContext, Type, ) - from sentry_sdk.tracing import TransactionKwargs P = ParamSpec("P") R = TypeVar("R") @@ -1041,152 +1032,6 @@ def add_breadcrumb( self._breadcrumbs.popleft() self._n_breadcrumbs_truncated += 1 - def start_transaction( - self, - transaction: "Optional[Transaction]" = None, - custom_sampling_context: "Optional[SamplingContext]" = None, - **kwargs: "Unpack[TransactionKwargs]", - ) -> "Union[Transaction, NoOpSpan]": - """ - Start and return a transaction. - - Start an existing transaction if given, otherwise create and start a new - transaction with kwargs. - - This is the entry point to manual tracing instrumentation. - - A tree structure can be built by adding child spans to the transaction, - and child spans to other spans. To start a new child span within the - transaction or any span, call the respective `.start_child()` method. - - Every child span must be finished before the transaction is finished, - otherwise the unfinished spans are discarded. - - When used as context managers, spans and transactions are automatically - finished at the end of the `with` block. If not using context managers, - call the `.finish()` method. - - When the transaction is finished, it will be sent to Sentry with all its - finished child spans. - - :param transaction: The transaction to start. If omitted, we create and - start a new transaction. - :param custom_sampling_context: The transaction's custom sampling context. - :param kwargs: Optional keyword arguments to be passed to the Transaction - constructor. See :py:class:`sentry_sdk.tracing.Transaction` for - available arguments. - """ - client = self.get_client() - if has_span_streaming_enabled(client.options): - deprecation_warning( - "Scope.start_transaction is not available in streaming mode.", - ) - return NoOpSpan() - - kwargs.setdefault("scope", self) - - try_autostart_continuous_profiler() - - custom_sampling_context = custom_sampling_context or {} - - # kwargs at this point has type TransactionKwargs, since we have removed - # the client and custom_sampling_context from it. - transaction_kwargs: "TransactionKwargs" = kwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_kwargs) - - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision - sampling_context = { - "transaction_context": transaction.to_json(), - "parent_sampled": transaction.parent_sampled, - } - sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) - - # update the sample rate in the dsc - if transaction.sample_rate is not None: - propagation_context = self.get_active_propagation_context() - baggage = propagation_context.baggage - - if baggage is not None: - baggage.sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction._baggage: - transaction._baggage.sentry_items["sample_rate"] = str( - transaction.sample_rate - ) - - if transaction.sampled: - transaction._continuous_profile = try_profile_lifecycle_trace_start() - - # Typically, the profiler is set when the transaction is created. But when - # using the auto lifecycle, the profiler isn't running when the first - # transaction is started. So make sure we update the profiler id on it. - if transaction._continuous_profile is not None: - transaction.set_profiler_id(get_profiler_id()) - - # we don't bother to keep spans if we already know we're not going to - # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) - - return transaction - - def start_span(self, **kwargs: "Any") -> "Span": - """ - Start a span whose parent is the currently active span or transaction, if any. - - The return value is a :py:class:`sentry_sdk.tracing.Span` instance, - typically used as a context manager to start and stop timing in a `with` - block. - - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - """ - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - deprecation_warning( - "Scope.start_span is not available in streaming mode.", - ) - return NoOpSpan() - - if kwargs.get("description") is not None: - deprecation_warning( - "The `description` parameter is deprecated. Please use `name` instead.", - ) - - with new_scope(): - kwargs.setdefault("scope", self) - - client = self.get_client() - - # get current span or transaction - span = self.span or self.get_isolation_scope().span - if isinstance(span, StreamedSpan): - # make mypy happy - return NoOpSpan() - - if span is None: - # New spans get the `trace_id` from the scope - if "trace_id" not in kwargs: - propagation_context = self.get_active_propagation_context() - kwargs["trace_id"] = propagation_context.trace_id - - span = Span(**kwargs) - else: - # Children take `trace_id`` from the parent span. - span = span.start_child(**kwargs) - - return span - def start_streamed_span( self, name: str, diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 32e7b812b1..af3b8f8a98 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -1,23 +1,3 @@ -""" -The API in this file is only meant to be used in span streaming mode. It should -not be mixed with the legacy tracing API (sentry_sdk.start_transaction, -sentry_sdk.start_span, etc.). - -You can enable span streaming mode via: - -``` -import sentry_sdk - -sentry_sdk.init( - trace_lifecycle="stream", -) -``` - -See -https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide/ -for how to migrate to span streaming. -""" - import sys import uuid from datetime import datetime, timedelta, timezone @@ -120,7 +100,7 @@ def start_span( active: bool = True, ) -> "StreamedSpan": """ - Start a span in streaming mode. + Start a span. The span's parent, unless provided explicitly via the `parent_span` argument, will be the current active span, if any. If there is none, this span will @@ -173,17 +153,6 @@ def start_span( :return: The span that has been started. :rtype: StreamedSpan """ - from sentry_sdk.tracing_utils import has_span_streaming_enabled - - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - logger.warning( - "Using span streaming API in non-span-streaming mode. Use " - "sentry_sdk.start_transaction() and sentry_sdk.start_span() " - "instead.", - ) - return NoOpStreamedSpan() - return sentry_sdk.get_current_scope().start_streamed_span( name, attributes, parent_span, active ) @@ -191,7 +160,7 @@ def start_span( def continue_trace(incoming: "dict[str, Any]") -> None: """ - Continue a trace from headers or environment variables in streaming mode. + Continue a trace from headers or environment variables. This function sets the propagation context on the scope. Any span started in the updated scope will belong under the trace extracted from the @@ -215,7 +184,7 @@ def continue_trace(incoming: "dict[str, Any]") -> None: def new_trace() -> None: """ - Resets the propagation context, forcing a new trace, in streaming mode. + Resets the propagation context, forcing a new trace. This function sets the propagation context on the scope. Any span started in the updated scope will start its own trace. diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 0381af78ba..c500f55574 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1807,7 +1807,7 @@ def ensure_integration_enabled( ```python @ensure_integration_enabled(MyIntegration, my_function) def patch_my_function(): - with sentry_sdk.start_transaction(...): + with sentry_sdk.traces.start_span(...): return my_function() ``` """ From 7a6a7c6e2e70edb2e8e818912e63c57244e89761 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 12:07:27 +0200 Subject: [PATCH 07/28] . --- sentry_sdk/scope.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index f6ee43a8be..720f012d27 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1032,14 +1032,13 @@ def add_breadcrumb( self._breadcrumbs.popleft() self._n_breadcrumbs_truncated += 1 - def start_streamed_span( + def start_span( self, name: str, attributes: "Optional[Attributes]", parent_span: "Optional[StreamedSpan]", active: bool, ) -> "StreamedSpan": - # TODO: rename to start_span once we drop the old API if isinstance(parent_span, NoOpStreamedSpan): # parent_span is only set if the user explicitly set it logger.debug( From 188e485468ad22079429b94954f261de3ab6d006 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 12:17:10 +0200 Subject: [PATCH 08/28] . --- sentry_sdk/__init__.py | 1 - sentry_sdk/traces.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 417220ecc1..c19fc9e925 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -46,7 +46,6 @@ "set_tags", "set_user", "start_span", - "trace", "monitor", "logger", "metrics", diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index af3b8f8a98..5998455c39 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -153,7 +153,7 @@ def start_span( :return: The span that has been started. :rtype: StreamedSpan """ - return sentry_sdk.get_current_scope().start_streamed_span( + return sentry_sdk.get_current_scope().start_span( name, attributes, parent_span, active ) From 306c2514b100b8936e93562914232b6d9b105ca7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 12:27:31 +0200 Subject: [PATCH 09/28] . --- tests/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index e4d3569172..ce57b2d224 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -21,10 +21,10 @@ def test_get_current_span(): fake_scope = mock.MagicMock() - fake_scope.span = mock.MagicMock() - assert get_current_span(fake_scope) == fake_scope.span + fake_scope.streamed_span = mock.MagicMock() + assert get_current_span(fake_scope) == fake_scope.streamed_span - fake_scope.span = None + fake_scope.streamed_span = None assert get_current_span(fake_scope) is None From 3482ec8f57ac903bfdbdb7ca645d2fed5024d1e5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 12:32:22 +0200 Subject: [PATCH 10/28] apidoc --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index 199e54eb61..a790054f4f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -39,6 +39,7 @@ Performance Monitoring .. autofunction:: sentry_sdk.api.trace .. autofunction:: sentry_sdk.api.continue_trace +.. autofunction:: sentry_sdk.api.new_trace .. autofunction:: sentry_sdk.api.get_current_span .. autofunction:: sentry_sdk.api.start_span From 51a30f78485fecc52ae4d9c88566c7001a637ec0 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 12:36:06 +0200 Subject: [PATCH 11/28] return trace --- sentry_sdk/__init__.py | 1 + sentry_sdk/api.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index c19fc9e925..417220ecc1 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -46,6 +46,7 @@ "set_tags", "set_user", "start_span", + "trace", "monitor", "logger", "metrics", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index e9a870aa76..3d2baef89c 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -5,6 +5,7 @@ from sentry_sdk._init_implementation import init from sentry_sdk.crons import monitor from sentry_sdk.scope import Scope, isolation_scope, new_scope +from sentry_sdk.traces import trace if TYPE_CHECKING: from collections.abc import Mapping @@ -70,6 +71,7 @@ def overload(x: "T") -> "T": "set_tags", "set_user", "start_span", + "trace", "monitor", "start_session", "end_session", From fd3e215b875bbc7289c59cfb21dbe815e19f8ac1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 12:52:53 +0200 Subject: [PATCH 12/28] remove span, transaction --- sentry_sdk/client.py | 31 +- sentry_sdk/scope.py | 93 +-- sentry_sdk/tracing.py | 1226 +---------------------------------- sentry_sdk/tracing_utils.py | 155 +---- 4 files changed, 21 insertions(+), 1484 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ce184ffd44..ae6b385c5a 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -35,10 +35,7 @@ from sentry_sdk.scrubber import EventScrubber from sentry_sdk.serializer import serialize from sentry_sdk.sessions import SessionFlusher -from sentry_sdk.traces import SpanStatus, StreamedSpan -from sentry_sdk.traces import trace as streaming_trace -from sentry_sdk.tracing import trace as legacy_trace -from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.traces import SpanStatus, StreamedSpan, trace from sentry_sdk.transport import ( AsyncHttpTransport, HttpTransportCore, @@ -364,16 +361,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["ignore_spans"] and not has_span_streaming_enabled(rv): - logger.warning( - "The `ignore_spans` parameter only works when `trace_lifecycle` is set to `stream`.", - ) - - if rv["before_send_span"] and not has_span_streaming_enabled(rv): - logger.warning( - "The `before_send_span` parameter only works when `trace_lifecycle` is set to `stream`.", - ) - return rv @@ -507,12 +494,6 @@ def _setup_instrumentation( """ Instruments the functions given in the list `functions_to_trace` with a trace decorator. """ - trace = ( - streaming_trace - if has_span_streaming_enabled(self.options) - else legacy_trace - ) - for function in functions_to_trace: class_name = None function_qualname = function["qualified_name"] @@ -635,12 +616,10 @@ def _record_lost_event( record_lost_func=_record_lost_event, ) - self.span_batcher = None - if has_span_streaming_enabled(self.options): - self.span_batcher = SpanBatcher( - capture_func=_capture_envelope, - record_lost_func=_record_lost_event, - ) + self.span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) max_request_body_size = ("always", "never", "small", "medium") if self.options["max_request_body_size"] not in max_request_body_size: diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 720f012d27..fc008ea1cf 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -9,7 +9,7 @@ from enum import Enum from functools import wraps from itertools import chain -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import sentry_sdk from sentry_sdk._types import AnnotatedValue @@ -29,15 +29,11 @@ from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, - NoOpSpan, - Span, - Transaction, ) from sentry_sdk.tracing_utils import ( Baggage, PropagationContext, _make_sampling_decision, - has_span_streaming_enabled, has_tracing_enabled, is_ignored_span, ) @@ -576,12 +572,9 @@ def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]": if not has_tracing_enabled(client.options): return self.get_active_propagation_context().to_traceparent() - span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return traceparent from there - if span_streaming and self.streamed_span is not None: + if self.streamed_span is not None: return self.streamed_span._to_traceparent() - elif not span_streaming and self.span is not None: - return self.span._to_traceparent() # else return traceparent from the propagation context return self.get_active_propagation_context().to_traceparent() @@ -596,12 +589,9 @@ def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]": if not has_tracing_enabled(client.options): return self.get_active_propagation_context().get_baggage() - span_streaming = has_span_streaming_enabled(client.options) # If we have an active span, return baggage from there - if span_streaming and self.streamed_span is not None: + if self.streamed_span is not None: return self.streamed_span._to_baggage() - elif not span_streaming and self.span is not None: - return self.span._to_baggage() # else return baggage from the propagation context return self.get_active_propagation_context().get_baggage() @@ -610,11 +600,7 @@ def get_trace_context(self) -> "Dict[str, Any]": """ Returns the Sentry "trace" context from the Propagation Context. """ - if ( - has_tracing_enabled(self.get_client().options) - and self._span is not None - and not isinstance(self._span, NoOpSpan) - ): + if has_tracing_enabled(self.get_client().options) and self._span is not None: return self._span._get_trace_context() # if we are tracing externally (otel), those values take precedence @@ -655,16 +641,9 @@ def iter_trace_propagation_headers( """ client = self.get_client() - span = kwargs.pop("span", None) - if not span: - span_streaming = has_span_streaming_enabled(client.options) - span = self.streamed_span if span_streaming else self.span + span = kwargs.pop("span", None) or self.streamed_span - if ( - has_tracing_enabled(client.options) - and span is not None - and not isinstance(span, NoOpSpan) - ): + if has_tracing_enabled(client.options) and span is not None: for header in span._iter_headers(): yield header elif has_external_propagation_context(): @@ -713,7 +692,7 @@ def clear(self) -> None: self.clear_breadcrumbs() self._should_capture: bool = True - self._span: "Optional[Union[Span, StreamedSpan]]" = None + self._span: "Optional[StreamedSpan]" = None self._session: "Optional[Session]" = None self._force_auto_session_tracking: "Optional[bool]" = None @@ -821,23 +800,6 @@ def set_user(self, value: "Optional[Dict[str, Any]]") -> None: if session is not None: session.update(user=value) - @property - def span(self) -> "Optional[Span]": - """Get/set current tracing span or transaction.""" - return self._span if isinstance(self._span, Span) else None - - @span.setter - def span(self, span: "Optional[Span]") -> None: - self._span = span - # XXX: this differs from the implementation in JS, there Scope.setSpan - # does not set Scope._transactionName. - if isinstance(span, Transaction): - transaction = span - if transaction.name: - self._transaction = transaction.name - if transaction.source: - self._transaction_info["source"] = transaction.source - @property def streamed_span(self) -> "Optional[StreamedSpan]": """Get/set current tracing span.""" @@ -1152,39 +1114,6 @@ def _update_sample_rate(self, sample_rate: float) -> None: if baggage is not None and baggage.sentry_items.get("sample_rate"): baggage.sentry_items["sample_rate"] = str(sample_rate) - def continue_trace( - self, - environ_or_headers: "Dict[str, Any]", - op: "Optional[str]" = None, - name: "Optional[str]" = None, - source: "Optional[str]" = None, - origin: str = "manual", - ) -> "Transaction": - """ - Sets the propagation context from environment or headers and returns a transaction. - """ - self.generate_propagation_context(environ_or_headers) - - # generate_propagation_context ensures that the propagation_context is not None. - propagation_context = cast(PropagationContext, self._propagation_context) - - optional_kwargs = {} - if name: - optional_kwargs["name"] = name - if source: - optional_kwargs["source"] = source - - return Transaction( - op=op, - origin=origin, - baggage=propagation_context.baggage, - parent_sampled=propagation_context.parent_sampled, - trace_id=propagation_context.trace_id, - parent_span_id=propagation_context.parent_span_id, - same_process_as_parent=False, - **optional_kwargs, - ) - def capture_event( self, event: "Event", @@ -1258,11 +1187,9 @@ def _capture_span(self, span: "Optional[StreamedSpan]") -> None: if span is None: return - client = self.get_client() - if not has_span_streaming_enabled(client.options): - return - merged_scope = self._merge_scopes() + + client = self.get_client() client._capture_span(span, scope=merged_scope) def capture_message( @@ -1670,7 +1597,7 @@ def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> N # isn't one if telemetry.get("span_id") is None: if self._span is not None and not isinstance( - self._span, (NoOpStreamedSpan, NoOpSpan) + self._span, NoOpStreamedSpan ): telemetry["span_id"] = self._span.span_id else: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index e101217fc3..045e64160f 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,117 +1,19 @@ -import uuid -from datetime import datetime, timedelta, timezone from enum import Enum -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING -import sentry_sdk -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, - deprecation_warning, - get_current_thread_meta, - is_valid_sample_rate, - logger, - nanosecond_time, - should_be_treated_as_error, -) +from sentry_sdk.consts import SPANSTATUS if TYPE_CHECKING: - from collections.abc import Callable, Mapping, MutableMapping from typing import ( - Any, - Dict, - Iterator, - List, - Optional, ParamSpec, - Tuple, TypeVar, - Union, - overload, ) - from typing_extensions import TypedDict, Unpack + from typing_extensions import TypedDict P = ParamSpec("P") R = TypeVar("R") - from sentry_sdk._types import ( - Event, - SamplingContext, - ) - from sentry_sdk.profiler.continuous_profiler import ContinuousProfile - - class SpanKwargs(TypedDict, total=False): - trace_id: str - """ - The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - """ - - span_id: str - """The span ID of this span. If omitted, a new span ID will be generated.""" - - parent_span_id: str - """The span ID of the parent span, if applicable.""" - - same_process_as_parent: bool - """Whether this span is in the same process as the parent span.""" - - sampled: bool - """ - Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - """ - - op: str - """ - The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - """ - - description: str - """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" - - status: str - """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" - - containing_transaction: "Optional[Transaction]" - """The transaction that this span belongs to.""" - - start_timestamp: "Optional[Union[datetime, float]]" - """ - The timestamp when the span started. If omitted, the current time - will be used. - """ - - scope: "sentry_sdk.Scope" - """The scope to use for this span. If not provided, we use the current scope.""" - - origin: str - """ - The origin of the span. - See https://develop.sentry.dev/sdk/performance/trace-origin/ - Default "manual". - """ - - name: str - """A string describing what operation is being performed within the span/transaction.""" - - class TransactionKwargs(SpanKwargs, total=False): - source: str - """ - A string describing the source of the transaction name. This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. - Default "custom". - """ - - parent_sampled: bool - """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" - - baggage: "Baggage" - """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" - ProfileContext = TypedDict( "ProfileContext", { @@ -193,1126 +95,4 @@ def get_span_status_from_http_code(http_status_code: int) -> str: return SPANSTATUS.UNKNOWN_ERROR -class _SpanRecorder: - """Limits the number of spans recorded in a transaction.""" - - __slots__ = ("maxlen", "spans", "dropped_spans") - - def __init__(self, maxlen: int) -> None: - # FIXME: this is `maxlen - 1` only to preserve historical behavior - # enforced by tests. - # Either this should be changed to `maxlen` or the JS SDK implementation - # should be changed to match a consistent interpretation of what maxlen - # limits: either transaction+spans or only child spans. - self.maxlen = maxlen - 1 - self.spans: "List[Span]" = [] - self.dropped_spans: int = 0 - - def add(self, span: "Span") -> None: - if len(self.spans) > self.maxlen: - span._span_recorder = None - self.dropped_spans += 1 - else: - self.spans.append(span) - - -class Span: - """A span holds timing information of a block of code. - Spans can have multiple child spans thus forming a span tree. - - :param trace_id: The trace ID of the root span. If this new span is to be the root span, - omit this parameter, and a new trace ID will be generated. - :param span_id: The span ID of this span. If omitted, a new span ID will be generated. - :param parent_span_id: The span ID of the parent span, if applicable. - :param same_process_as_parent: Whether this span is in the same process as the parent span. - :param sampled: Whether the span should be sampled. Overrides the default sampling decision - for this span when provided. - :param op: The span's operation. A list of recommended values is available here: - https://develop.sentry.dev/sdk/performance/span-operations/ - :param description: A description of what operation is being performed within the span. - - .. deprecated:: 2.15.0 - Please use the `name` parameter, instead. - :param name: A string describing what operation is being performed within the span. - - .. deprecated:: 2.0.0 - Please use the `scope` parameter, instead. - :param status: The span's status. Possible values are listed at - https://develop.sentry.dev/sdk/event-payloads/span/ - :param containing_transaction: The transaction that this span belongs to. - :param start_timestamp: The timestamp when the span started. If omitted, the current time - will be used. - :param scope: The scope to use for this span. If not provided, we use the current scope. - """ - - __slots__ = ( - "_trace_id", - "_span_id", - "parent_span_id", - "same_process_as_parent", - "sampled", - "op", - "description", - "start_timestamp", - "_start_timestamp_monotonic_ns", - "status", - "timestamp", - "_tags", - "_data", - "_span_recorder", - "_context_manager_state", - "_containing_transaction", - "scope", - "origin", - "name", - "_flags", - "_flags_capacity", - ) - - def __init__( - self, - trace_id: "Optional[str]" = None, - span_id: "Optional[str]" = None, - parent_span_id: "Optional[str]" = None, - same_process_as_parent: bool = True, - sampled: "Optional[bool]" = None, - op: "Optional[str]" = None, - description: "Optional[str]" = None, - status: "Optional[str]" = None, - containing_transaction: "Optional[Transaction]" = None, - start_timestamp: "Optional[Union[datetime, float]]" = None, - scope: "Optional[sentry_sdk.Scope]" = None, - origin: str = "manual", - name: "Optional[str]" = None, - ) -> None: - self._trace_id = trace_id - self._span_id = span_id - self.parent_span_id = parent_span_id - self.same_process_as_parent = same_process_as_parent - self.sampled = sampled - self.op = op - self.description = name or description - self.status = status - self.scope = scope - self.origin = origin - self._tags: "MutableMapping[str, str]" = {} - self._data: "Dict[str, Any]" = {} - self._containing_transaction = containing_transaction - self._flags: "Dict[str, bool]" = {} - self._flags_capacity = 10 - - if start_timestamp is None: - start_timestamp = datetime.now(timezone.utc) - elif isinstance(start_timestamp, float): - start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) - self.start_timestamp = start_timestamp - try: - # profiling depends on this value and requires that - # it is measured in nanoseconds - self._start_timestamp_monotonic_ns = nanosecond_time() - except AttributeError: - pass - - #: End timestamp of span - self.timestamp: "Optional[datetime]" = None - - self._span_recorder: "Optional[_SpanRecorder]" = None - - self.update_active_thread() - self.set_profiler_id(get_profiler_id()) - - # TODO this should really live on the Transaction class rather than the Span - # class - def init_span_recorder(self, maxlen: int) -> None: - if self._span_recorder is None: - self._span_recorder = _SpanRecorder(maxlen) - - @property - def trace_id(self) -> str: - if not self._trace_id: - self._trace_id = uuid.uuid4().hex - - return self._trace_id - - @trace_id.setter - def trace_id(self, value: str) -> None: - self._trace_id = value - - @property - def span_id(self) -> str: - if not self._span_id: - self._span_id = uuid.uuid4().hex[16:] - - return self._span_id - - @span_id.setter - def span_id(self, value: str) -> None: - self._span_id = value - - def __repr__(self) -> str: - return ( - "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.op, - self.description, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.origin, - ) - ) - - def __enter__(self) -> "Span": - if has_span_streaming_enabled(sentry_sdk.get_client().options): - self._context_manager_state = None # early return sentinel - return self - - scope = self.scope or sentry_sdk.get_current_scope() - old_span = scope.span - scope.span = self - self._context_manager_state = (scope, old_span) - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if ( - hasattr(self, "_context_manager_state") - and self._context_manager_state is None - ): - del self._context_manager_state - return - - if value is not None and should_be_treated_as_error(ty, value): - self.set_status(SPANSTATUS.INTERNAL_ERROR) - - with capture_internal_exceptions(): - scope, old_span = cast( - "Tuple[sentry_sdk.Scope, Optional[Span]]", - self._context_manager_state, - ) - del self._context_manager_state - self.finish(scope) - scope.span = old_span - - @property - def containing_transaction(self) -> "Optional[Transaction]": - """The ``Transaction`` that this span belongs to. - The ``Transaction`` is the root of the span tree, - so one could also think of this ``Transaction`` as the "root span".""" - - # this is a getter rather than a regular attribute so that transactions - # can return `self` here instead (as a way to prevent them circularly - # referencing themselves) - return self._containing_transaction - - def start_child(self, **kwargs: "Any") -> "Span": - """ - Start a sub-span from the current span or transaction. - - Takes the same arguments as the initializer of :py:class:`Span`. The - trace id, sampling decision, transaction pointer, and span recorder are - inherited from the current span/transaction. - """ - if kwargs.get("description") is not None: - deprecation_warning( - "The `description` parameter is deprecated. Please use `name` instead.", - ) - - kwargs.setdefault("sampled", self.sampled) - - child = Span( - trace_id=self.trace_id, - parent_span_id=self.span_id, - containing_transaction=self.containing_transaction, - **kwargs, - ) - - span_recorder = ( - self.containing_transaction and self.containing_transaction._span_recorder - ) - if span_recorder: - span_recorder.add(child) - - return child - - @classmethod - def continue_from_environ( - cls, - environ: "Mapping[str, str]", - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a Transaction with the given params, then add in data pulled from - the ``sentry-trace`` and ``baggage`` headers from the environ (if any) - before returning the Transaction. - - This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` - in that it assumes header names in the form ``HTTP_HEADER_NAME`` - - such as you would get from a WSGI/ASGI environ - - rather than the form ``header-name``. - - :param environ: The ASGI/WSGI environ to pull information from. - """ - return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) - - @classmethod - def continue_from_headers( - cls, - headers: "Mapping[str, str]", - *, - _sample_rand: "Optional[str]" = None, - **kwargs: "Any", - ) -> "Transaction": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a transaction with the given params (including any data pulled from - the ``sentry-trace`` and ``baggage`` headers). - - :param headers: The dictionary with the HTTP headers to pull information from. - :param _sample_rand: If provided, we override the sample_rand value from the - incoming headers with this value. (internal use only) - """ - logger.warning("Deprecated: use sentry_sdk.continue_trace instead.") - - # TODO-neel move away from this kwargs stuff, it's confusing and opaque - # make more explicit - baggage = Baggage.from_incoming_header( - headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand - ) - kwargs.update({BAGGAGE_HEADER_NAME: baggage}) - - sentrytrace_kwargs = extract_sentrytrace_data( - headers.get(SENTRY_TRACE_HEADER_NAME) - ) - - if sentrytrace_kwargs is not None: - kwargs.update(sentrytrace_kwargs) - - # If there's an incoming sentry-trace but no incoming baggage header, - # for instance in traces coming from older SDKs, - # baggage will be empty and immutable and won't be populated as head SDK. - baggage.freeze() - - transaction = Transaction(**kwargs) - transaction.same_process_as_parent = False - - return transaction - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - """ - Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. - If the span's containing transaction doesn't yet have a ``baggage`` value, - this will cause one to be generated and stored. - """ - if not self.containing_transaction: - # Do not propagate headers if there is no containing transaction. Otherwise, this - # span ends up being the root span of a new trace, and since it does not get sent - # to Sentry, the trace will be missing a root transaction. The dynamic sampling - # context will also be missing, breaking dynamic sampling & traces. - return - - yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() - - baggage = self.containing_transaction.get_baggage().serialize() - if baggage: - yield BAGGAGE_HEADER_NAME, baggage - - @classmethod - def from_traceparent( - cls, - traceparent: "Optional[str]", - **kwargs: "Any", - ) -> "Optional[Transaction]": - """ - DEPRECATED: Use :py:meth:`sentry_sdk.continue_trace`. - - Create a ``Transaction`` with the given params, then add in data pulled from - the given ``sentry-trace`` header value before returning the ``Transaction``. - """ - if not traceparent: - return None - - return cls.continue_from_headers( - {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs - ) - - def to_traceparent(self) -> str: - if self.sampled is True: - sampled = "1" - elif self.sampled is False: - sampled = "0" - else: - sampled = None - - traceparent = "%s-%s" % (self.trace_id, self.span_id) - if sampled is not None: - traceparent += "-%s" % (sampled,) - - return traceparent - - def to_baggage(self) -> "Optional[Baggage]": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with this ``Span``, if any. (Taken from the root of the span tree.) - """ - if self.containing_transaction: - return self.containing_transaction.get_baggage() - return None - - def set_tag(self, key: str, value: "Any") -> None: - self._tags[key] = value - - def set_data(self, key: str, value: "Any") -> None: - self._data[key] = value - - def update_data(self, data: "Dict[str, Any]") -> None: - self._data.update(data) - - def set_flag(self, flag: str, result: bool) -> None: - if len(self._flags) < self._flags_capacity: - self._flags[flag] = result - - def set_status(self, value: str) -> None: - self.status = value - - def set_thread( - self, thread_id: "Optional[int]", thread_name: "Optional[str]" - ) -> None: - if thread_id is not None: - self.set_data(SPANDATA.THREAD_ID, str(thread_id)) - - if thread_name is not None: - self.set_data(SPANDATA.THREAD_NAME, thread_name) - - def set_profiler_id(self, profiler_id: "Optional[str]") -> None: - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) - - def set_http_status(self, http_status: int) -> None: - self.set_tag( - "http.status_code", str(http_status) - ) # TODO-neel remove in major, we keep this for backwards compatibility - self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) - self.set_status(get_span_status_from_http_code(http_status)) - - def is_success(self) -> bool: - return self.status == "ok" - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - """ - Sets the end timestamp of the span. - - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. - - :param scope: The scope to use for this transaction. - If not provided, the current scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: Always ``None``. The type is ``Optional[str]`` to match - the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. - """ - if self.timestamp is not None: - # This span is already finished, ignore. - return None - - try: - if end_timestamp: - if isinstance(end_timestamp, float): - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - self.timestamp = end_timestamp - else: - elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns - self.timestamp = self.start_timestamp + timedelta( - microseconds=elapsed / 1000 - ) - except AttributeError: - self.timestamp = datetime.now(timezone.utc) - - scope = scope or sentry_sdk.get_current_scope() - - # Copy conversation_id from scope to span data if this is an AI span - conversation_id = scope.get_conversation_id() - if conversation_id: - has_ai_op = SPANDATA.GEN_AI_OPERATION_NAME in self._data - is_ai_span_op = self.op is not None and ( - self.op.startswith("ai.") or self.op.startswith("gen_ai.") - ) - if has_ai_op or is_ai_span_op: - self.set_data("gen_ai.conversation.id", conversation_id) - - return None - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the span.""" - - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "same_process_as_parent": self.same_process_as_parent, - "op": self.op, - "description": self.description, - "start_timestamp": self.start_timestamp, - "timestamp": self.timestamp, - "origin": self.origin, - } - - if self.status: - rv["status"] = self.status - # TODO-neel remove redundant tag in major - self._tags["status"] = self.status - - tags = self._tags - if tags: - rv["tags"] = tags - - data = {} - data.update(self._flags) - data.update(self._data) - if data: - rv["data"] = data - - return rv - - def get_trace_context(self) -> "Any": - rv: "Dict[str, Any]" = { - "trace_id": self.trace_id, - "span_id": self.span_id, - "parent_span_id": self.parent_span_id, - "op": self.op, - "description": self.description, - "origin": self.origin, - } - if self.status: - rv["status"] = self.status - - if self.containing_transaction: - rv["dynamic_sampling_context"] = ( - self.containing_transaction.get_baggage().dynamic_sampling_context() - ) - - data = {} - - thread_id = self._data.get(SPANDATA.THREAD_ID) - if thread_id is not None: - data["thread.id"] = thread_id - - thread_name = self._data.get(SPANDATA.THREAD_NAME) - if thread_name is not None: - data["thread.name"] = thread_name - - if data: - rv["data"] = data - - return rv - - def get_profile_context(self) -> "Optional[ProfileContext]": - profiler_id = self._data.get(SPANDATA.PROFILER_ID) - if profiler_id is None: - return None - - return { - "profiler_id": profiler_id, - } - - def update_active_thread(self) -> None: - thread_id, thread_name = get_current_thread_meta() - self.set_thread(thread_id, thread_name) - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -class Transaction(Span): - """The Transaction is the root element that holds all the spans - for Sentry performance instrumentation. - - :param name: Identifier of the transaction. - Will show up in the Sentry UI. - :param parent_sampled: Whether the parent transaction was sampled. - If True this transaction will be kept, if False it will be discarded. - :param baggage: The W3C baggage header value. - (see https://www.w3.org/TR/baggage/) - :param source: A string describing the source of the transaction name. - This will be used to determine the transaction's type. - See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations - for more information. Default "custom". - :param kwargs: Additional arguments to be passed to the Span constructor. - See :py:class:`sentry_sdk.tracing.Span` for available arguments. - """ - - __slots__ = ( - "name", - "source", - "parent_sampled", - # used to create baggage value for head SDKs in dynamic sampling - "sample_rate", - "_contexts", - "_continuous_profile", - "_baggage", - "_sample_rand", - ) - - def __init__( # type: ignore[misc] - self, - name: str = "", - parent_sampled: "Optional[bool]" = None, - baggage: "Optional[Baggage]" = None, - source: str = TransactionSource.CUSTOM, - **kwargs: "Unpack[SpanKwargs]", - ) -> None: - super().__init__(**kwargs) - - self.name = name - self.source = source - self.sample_rate: "Optional[float]" = None - self.parent_sampled = parent_sampled - self._contexts: "Dict[str, Any]" = {} - self._continuous_profile: "Optional[ContinuousProfile]" = None - self._baggage = baggage - - baggage_sample_rand = ( - None if self._baggage is None else self._baggage._sample_rand() - ) - if baggage_sample_rand is not None: - self._sample_rand = baggage_sample_rand - else: - self._sample_rand = _generate_sample_rand(self.trace_id) - - def __repr__(self) -> str: - return ( - "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" - % ( - self.__class__.__name__, - self.name, - self.op, - self.trace_id, - self.span_id, - self.parent_span_id, - self.sampled, - self.source, - self.origin, - ) - ) - - def _possibly_started(self) -> bool: - """Returns whether the transaction might have been started. - - If this returns False, we know that the transaction was not started - with sentry_sdk.start_transaction, and therefore the transaction will - be discarded. - """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False - - def __enter__(self) -> "Transaction": - if not self._possibly_started(): - logger.debug( - "Transaction was entered without being started with sentry_sdk.start_transaction." - "The transaction will not be sent to Sentry. To fix, start the transaction by" - "passing it to sentry_sdk.start_transaction." - ) - - super().__enter__() - - return self - - def __exit__( - self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]" - ) -> None: - if self._continuous_profile is not None: - self._continuous_profile.stop() - - super().__exit__(ty, value, tb) - - @property - def containing_transaction(self) -> "Transaction": - """The root element of the span tree. - In the case of a transaction it is the transaction itself. - """ - - # Transactions (as spans) belong to themselves (as transactions). This - # is a getter rather than a regular attribute to avoid having a circular - # reference. - return self - - def _get_log_representation(self) -> str: - return "{op}transaction <{name}>".format( - op=("<" + self.op + "> " if self.op else ""), name=self.name - ) - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - """Finishes the transaction and sends it to Sentry. - All finished spans in the transaction will also be sent to Sentry. - - :param scope: The Scope to use for this transaction. - If not provided, the current Scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: The event ID if the transaction was sent to Sentry, - otherwise None. - """ - if self.timestamp is not None: - # This transaction is already finished, ignore. - return None - - scope = scope or self.scope or sentry_sdk.get_current_scope() - client = sentry_sdk.get_client() - - if not client.is_active(): - # We have no active client and therefore nowhere to send this transaction. - return None - - if self._span_recorder is None: - # Explicit check against False needed because self.sampled might be None - if self.sampled is False: - logger.debug("Discarding transaction because sampled = False") - else: - logger.debug( - "Discarding transaction because it was not started with sentry_sdk.start_transaction" - ) - - # This is not entirely accurate because discards here are not - # exclusively based on sample rate but also traces sampler, but - # we handle this the same here. - if client.transport and has_tracing_enabled(client.options): - if client.monitor and client.monitor.downsample_factor > 0: - reason = "backpressure" - else: - reason = "sample_rate" - - client.transport.record_lost_event(reason, data_category="transaction") - - # Only one span (the transaction itself) is discarded, since we did not record any spans here. - client.transport.record_lost_event(reason, data_category="span") - return None - - if not self.name: - logger.warning( - "Transaction has no name, falling back to ``." - ) - self.name = "" - - super().finish(scope, end_timestamp) - - if not self.sampled: - # At this point a `sampled = None` should have already been resolved - # to a concrete decision. - if self.sampled is None: - logger.warning("Discarding transaction without sampling decision.") - - return None - - finished_spans = [] - has_gen_ai_span = False - if client.options.get("stream_gen_ai_spans", True): - for span in self._span_recorder.spans: - if span.timestamp is None: - continue - - if isinstance(span.op, str) and span.op.startswith("gen_ai."): - has_gen_ai_span = True - - finished_spans.append(span.to_json()) - else: - finished_spans = [ - span.to_json() - for span in self._span_recorder.spans - if span.timestamp is not None - ] - - len_diff = len(self._span_recorder.spans) - len(finished_spans) - dropped_spans = len_diff + self._span_recorder.dropped_spans - - # we do this to break the circular reference of transaction -> span - # recorder -> span -> containing transaction (which is where we started) - # before either the spans or the transaction goes out of scope and has - # to be garbage collected - self._span_recorder = None - - contexts = {} - contexts.update(self._contexts) - contexts.update({"trace": self.get_trace_context()}) - profile_context = self.get_profile_context() - if profile_context is not None: - contexts.update({"profile": profile_context}) - - event: "Event" = { - "type": "transaction", - "transaction": self.name, - "transaction_info": {"source": self.source}, - "contexts": contexts, - "tags": self._tags, - "timestamp": self.timestamp, - "start_timestamp": self.start_timestamp, - "spans": finished_spans, - } - - if dropped_spans > 0: - event["_dropped_spans"] = dropped_spans - - if has_gen_ai_span: - event["_has_gen_ai_span"] = True - - return scope.capture_event(event) - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - """Sets a context. Transactions can have multiple contexts - and they should follow the format described in the "Contexts Interface" - documentation. - - :param key: The name of the context. - :param value: The information about the context. - """ - self._contexts[key] = value - - def set_http_status(self, http_status: int) -> None: - """Sets the status of the Transaction according to the given HTTP status. - - :param http_status: The HTTP status code.""" - super().set_http_status(http_status) - self.set_context("response", {"status_code": http_status}) - - def to_json(self) -> "Dict[str, Any]": - """Returns a JSON-compatible representation of the transaction.""" - rv = super().to_json() - - rv["name"] = self.name - rv["source"] = self.source - rv["sampled"] = self.sampled - - return rv - - def get_trace_context(self) -> "Any": - trace_context = super().get_trace_context() - - if self._data: - trace_context["data"] = self._data - - return trace_context - - def get_baggage(self) -> "Baggage": - """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` - associated with the Transaction. - - The first time a new baggage with Sentry items is made, - it will be frozen.""" - if not self._baggage or self._baggage.mutable: - self._baggage = Baggage.populate_from_transaction(self) - - return self._baggage - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - """ - Sets the transaction's sampling decision, according to the following - precedence rules: - - 1. If a sampling decision is passed to `start_transaction` - (`start_transaction(name: "my transaction", sampled: True)`), that - decision will be used, regardless of anything else - - 2. If `traces_sampler` is defined, its decision will be used. It can - choose to keep or ignore any parent sampling decision, or use the - sampling context data to make its own decision or to choose a sample - rate for the transaction. - - 3. If `traces_sampler` is not defined, but there's a parent sampling - decision, the parent sampling decision will be used. - - 4. If `traces_sampler` is not defined and there's no parent sampling - decision, `traces_sample_rate` will be used. - """ - client = sentry_sdk.get_client() - - transaction_description = self._get_log_representation() - - # nothing to do if tracing is disabled - if not has_tracing_enabled(client.options): - self.sampled = False - return - - # if the user has forced a sampling decision by passing a `sampled` - # value when starting the transaction, go with that - if self.sampled is not None: - self.sample_rate = float(self.sampled) - return - - # we would have bailed already if neither `traces_sampler` nor - # `traces_sample_rate` were defined, so one of these should work; prefer - # the hook if so - if callable(client.options.get("traces_sampler")): - try: - sample_rate = client.options["traces_sampler"](sampling_context) - except Exception: - logger.warning( - "[Tracing] traces_sampler raised; falling back to parent sample rate or traces_sample_rate", - exc_info=True, - ) - sample_rate = ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - else: - sample_rate = ( - sampling_context["parent_sampled"] - if sampling_context["parent_sampled"] is not None - else client.options["traces_sample_rate"] - ) - - # Since this is coming from the user (or from a function provided by the - # user), who knows what we might get. (The only valid values are - # booleans or numbers between 0 and 1.) - if not is_valid_sample_rate(sample_rate, source="Tracing"): - logger.warning( - "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( - transaction_description=transaction_description, - ) - ) - self.sampled = False - return - - self.sample_rate = float(sample_rate) - - if client.monitor: - self.sample_rate /= 2**client.monitor.downsample_factor - - # if the function returned 0 (or false), or if `traces_sample_rate` is - # 0, it's a sign the transaction should be dropped - if not self.sample_rate: - logger.debug( - "[Tracing] Discarding {transaction_description} because {reason}".format( - transaction_description=transaction_description, - reason=( - "traces_sampler returned 0 or False, or is using a fallback sample rate that is 0 or False" - if callable(client.options.get("traces_sampler")) - else "traces_sample_rate is set to 0" - ), - ) - ) - self.sampled = False - return - - # Now we roll the dice. - self.sampled = self._sample_rand < self.sample_rate - - if self.sampled: - logger.debug( - "[Tracing] Starting {transaction_description}".format( - transaction_description=transaction_description, - ) - ) - else: - logger.debug( - "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( - transaction_description=transaction_description, - sample_rate=self.sample_rate, - ) - ) - - # Private aliases matching StreamedSpan's private API - _get_baggage = get_baggage - _get_trace_context = get_trace_context - - -class NoOpSpan(Span): - def __repr__(self) -> str: - return "<%s>" % self.__class__.__name__ - - @property - def containing_transaction(self) -> "Optional[Transaction]": - return None - - def start_child(self, **kwargs: "Any") -> "NoOpSpan": - return NoOpSpan() - - def to_traceparent(self) -> str: - return "" - - def to_baggage(self) -> "Optional[Baggage]": - return None - - def get_baggage(self) -> "Optional[Baggage]": - return None - - def iter_headers(self) -> "Iterator[Tuple[str, str]]": - return iter(()) - - def set_tag(self, key: str, value: "Any") -> None: - pass - - def set_data(self, key: str, value: "Any") -> None: - pass - - def update_data(self, data: "Dict[str, Any]") -> None: - pass - - def set_status(self, value: str) -> None: - pass - - def set_http_status(self, http_status: int) -> None: - pass - - def is_success(self) -> bool: - return True - - def to_json(self) -> "Dict[str, Any]": - return {} - - def get_trace_context(self) -> "Any": - return {} - - def get_profile_context(self) -> "Any": - return {} - - def finish( - self, - scope: "Optional[sentry_sdk.Scope]" = None, - end_timestamp: "Optional[Union[float, datetime]]" = None, - ) -> "Optional[str]": - pass - - def set_context(self, key: str, value: "dict[str, Any]") -> None: - pass - - def init_span_recorder(self, maxlen: int) -> None: - pass - - def _set_initial_sampling_decision( - self, sampling_context: "SamplingContext" - ) -> None: - pass - - # Private aliases matching StreamedSpan's private API - _to_traceparent = to_traceparent - _to_baggage = to_baggage - _get_baggage = get_baggage - _iter_headers = iter_headers - _get_trace_context = get_trace_context - - -if TYPE_CHECKING: - - @overload - def trace( - func: None = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, - ) -> "Callable[[Callable[P, R]], Callable[P, R]]": - # Handles: @trace() and @trace(op="custom") - pass - - @overload - def trace(func: "Callable[P, R]") -> "Callable[P, R]": - # Handles: @trace - pass - - -def trace( - func: "Optional[Callable[P, R]]" = None, - *, - op: "Optional[str]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]": - """ - Decorator to start a child span around a function call. - - This decorator automatically creates a new span when the decorated function - is called, and finishes the span when the function returns or raises an exception. - - :param func: The function to trace. When used as a decorator without parentheses, - this is the function being decorated. When used with parameters (e.g., - ``@trace(op="custom")``, this should be None. - :type func: Callable or None - - :param op: The operation name for the span. This is a high-level description - of what the span represents (e.g., "http.client", "db.query"). - You can use predefined constants from :py:class:`sentry_sdk.consts.OP` - or provide your own string. If not provided, a default operation will - be assigned based on the template. - :type op: str or None - - :param name: The human-readable name/description for the span. If not provided, - defaults to the function name. This provides more specific details about - what the span represents (e.g., "GET /api/users", "process_user_data"). - :type name: str or None - - :param attributes: A dictionary of key-value pairs to add as attributes to the span. - Attribute values must be strings, integers, floats, or booleans. These - attributes provide additional context about the span's execution. - :type attributes: dict[str, Any] or None - - :returns: When used as ``@trace``, returns the decorated function. When used as - ``@trace(...)`` with parameters, returns a decorator function. - :rtype: Callable or decorator function - - Example:: - - import sentry_sdk - from sentry_sdk.consts import OP - - # Simple usage with default values - @sentry_sdk.trace - def process_data(): - # Function implementation - pass - - # With custom parameters - @sentry_sdk.trace( - op=OP.DB_QUERY, - name="Get user data", - attributes={"postgres": True} - ) - def make_db_query(sql): - # Function implementation - pass - """ - from sentry_sdk.tracing_utils import create_span_decorator - - decorator = create_span_decorator( - op=op, - name=name, - attributes=attributes, - ) - - if func: - return decorator(func) - else: - return decorator - - # Circular imports - -from sentry_sdk.tracing_utils import ( - Baggage, - EnvironHeaders, - _generate_sample_rand, - extract_sentrytrace_data, - has_span_streaming_enabled, - has_tracing_enabled, -) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 63938bb1cd..cf627a76dd 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -22,7 +22,6 @@ _is_in_project_root, _module_in_list, capture_internal_exceptions, - deprecation_warning, filename_for_module, has_data_collection_enabled, is_sentry_url, @@ -112,20 +111,6 @@ def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool: ) -def has_span_streaming_enabled(options: "Optional[dict[str, Any]]") -> bool: - if options is None: - return False - - is_enabled_in_experiment_config = (options.get("_experiments") or {}).get( - "trace_lifecycle" - ) == "stream" - - if options.get("trace_lifecycle") is not None: - return options.get("trace_lifecycle") == "stream" - - return is_enabled_in_experiment_config - - @contextlib.contextmanager def record_sql_queries( cursor: "Any", @@ -180,7 +165,7 @@ def record_sql_queries( if query is not None: additional_attributes["db.query.text"] = query - with sentry_sdk.traces.start_span( + with sentry_sdk.start_span( name="" if query is None else query, attributes={ "sentry.origin": span_origin, @@ -990,109 +975,6 @@ def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]" def create_span_decorator( - op: "Optional[Union[str, OP]]" = None, - name: "Optional[str]" = None, - attributes: "Optional[dict[str, Any]]" = None, -) -> "Any": - """ - Create a span decorator that can wrap both sync and async functions. - - :param op: The operation type for the span. - :type op: str or :py:class:`sentry_sdk.consts.OP` or None - :param name: The name of the span. - :type name: str or None - :param attributes: Additional attributes to set on the span. - :type attributes: dict or None - """ - - def span_decorator(f: "Any") -> "Any": - """ - Decorator to create a span for the given function. - """ - - @functools.wraps(f) - async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return await f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - deprecation_warning( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - ) - return await f(*args, **kwargs) - - span_op = op or OP.FUNCTION - function_name = name or qualname_from_function(f) or "" - span_name = function_name - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - - result = await f(*args, **kwargs) - - return result - - try: - async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - @functools.wraps(f) - def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - current_span = get_current_span() - - if current_span is None: - logger.debug( - "Cannot create a child span for %s. " - "Please start a Sentry transaction before calling this function.", - qualname_from_function(f), - ) - return f(*args, **kwargs) - - if isinstance(current_span, StreamedSpan): - deprecation_warning( - "Use the @sentry_sdk.traces.trace decorator in span streaming mode.", - ) - return f(*args, **kwargs) - - span_op = op or OP.FUNCTION - function_name = name or qualname_from_function(f) or "" - span_name = function_name - - with current_span.start_child( - op=span_op, - name=span_name, - ) as span: - span.update_data(attributes or {}) - - result = f(*args, **kwargs) - - return result - - try: - sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined] - except Exception: - pass - - if inspect.iscoroutinefunction(f): - return async_wrapper - else: - return sync_wrapper - - return span_decorator - - -def create_streaming_span_decorator( name: "Optional[str]" = None, attributes: "Optional[dict[str, Any]]" = None, active: bool = True, @@ -1111,16 +993,9 @@ def span_decorator(f: "Any") -> "Any": @functools.wraps(f) async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - logger.warning( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - ) - span_name = name or qualname_from_function(f) or "" - with start_streaming_span( + with sentry_sdk.start_span( name=span_name, attributes=new_attributes, active=active ): result = await f(*args, **kwargs) @@ -1133,16 +1008,9 @@ async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any": @functools.wraps(f) def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - if client.is_active() and not has_span_streaming_enabled(client.options): - logger.warning( - "Using span streaming API in non-span-streaming mode. Use " - "@sentry_sdk.trace instead.", - ) - span_name = name or qualname_from_function(f) or "" - with start_streaming_span( + with sentry_sdk.start_span( name=span_name, attributes=new_attributes, active=active ): return f(*args, **kwargs) @@ -1160,17 +1028,6 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any": return span_decorator -def get_current_span( - scope: "Optional[sentry_sdk.Scope]" = None, -) -> "Optional[Span]": - """ - Returns the currently active span if there is one running, otherwise `None` - """ - scope = scope or sentry_sdk.get_current_scope() - current_span = scope.span - return current_span - - def _generate_sample_rand( trace_id: "Optional[str]", *, @@ -1494,14 +1351,8 @@ def _matches(rule: "Any", value: "Any") -> bool: LOW_QUALITY_SEGMENT_SOURCES, StreamedSpan, ) -from sentry_sdk.traces import ( - start_span as start_streaming_span, -) from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span From 39ab5a0eb96e715a736d375d065c52701b81f9ed Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 13:03:02 +0200 Subject: [PATCH 13/28] remove compat funcs --- sentry_sdk/ai/monitoring.py | 3 +- sentry_sdk/traces.py | 4 +-- sentry_sdk/tracing_utils.py | 55 +------------------------------------ 3 files changed, 4 insertions(+), 58 deletions(-) diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index dfadc290db..6ce400cba4 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -24,8 +24,7 @@ def record_token_usage( if input_tokens_cached is not None: span.set_attribute( - SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, - input_tokens_cached, + SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED, input_tokens_cached ) if input_tokens_cache_write is not None: diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 5998455c39..fa71c31d44 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -828,10 +828,10 @@ def make_db_query(sql): pass """ from sentry_sdk.tracing_utils import ( - create_streaming_span_decorator, + create_span_decorator, ) - decorator = create_streaming_span_decorator( + decorator = create_span_decorator( name=name, attributes=attributes, active=active, diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index cf627a76dd..756f934dc3 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -366,9 +366,7 @@ def add_query_source( ) -def add_http_request_source( - span: "sentry_sdk.traces.StreamedSpan", -) -> None: +def add_http_request_source(span: "sentry_sdk.traces.StreamedSpan") -> None: """ Adds OTel compatible source code information to a span for an outgoing HTTP request """ @@ -761,56 +759,6 @@ def populate_from_propagation_context( return Baggage(sentry_items, third_party_items, mutable) - @classmethod - def populate_from_transaction( - cls, transaction: "sentry_sdk.tracing.Transaction" - ) -> "Baggage": - """ - Populate fresh baggage entry with sentry_items and make it immutable - if this is the head SDK which originates traces. - """ - client = sentry_sdk.get_client() - sentry_items: "Dict[str, str]" = {} - - if not client.is_active(): - return Baggage(sentry_items) - - options = client.options or {} - - sentry_items["trace_id"] = transaction.trace_id - sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231 - - if options.get("environment"): - sentry_items["environment"] = options["environment"] - - if options.get("release"): - sentry_items["release"] = options["release"] - - if client.parsed_dsn: - sentry_items["public_key"] = client.parsed_dsn.public_key - if client.parsed_dsn.org_id: - sentry_items["org_id"] = client.parsed_dsn.org_id - - if ( - transaction.name - and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES - ): - sentry_items["transaction"] = transaction.name - - if transaction.sample_rate is not None: - sentry_items["sample_rate"] = str(transaction.sample_rate) - - if transaction.sampled is not None: - sentry_items["sampled"] = "true" if transaction.sampled else "false" - - # there's an existing baggage but it was mutable, - # which is why we are creating this new baggage. - # However, if by chance the user put some sentry items in there, give them precedence. - if transaction._baggage and transaction._baggage.sentry_items: - sentry_items.update(transaction._baggage.sentry_items) - - return Baggage(sentry_items, mutable=False) - @classmethod def populate_from_segment(cls, segment: "StreamedSpan") -> "Baggage": """ @@ -1353,6 +1301,5 @@ def _matches(rule: "Any", value: "Any") -> bool: ) from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, - LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) From 3d7349ddab29ee886ff3d93c65cc85759fb29998 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 13:06:48 +0200 Subject: [PATCH 14/28] apidoc fix --- docs/apidocs.rst | 4 ++-- sentry_sdk/_types.py | 1 - sentry_sdk/consts.py | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/apidocs.rst b/docs/apidocs.rst index 4991b8ccd6..dd16a59233 100644 --- a/docs/apidocs.rst +++ b/docs/apidocs.rst @@ -23,10 +23,10 @@ API Docs .. autoclass:: sentry_sdk.HttpTransport :members: -.. autoclass:: sentry_sdk.tracing.Transaction +.. autoclass:: sentry_sdk.traces.StreamedSpan :members: -.. autoclass:: sentry_sdk.tracing.Span +.. autoclass:: sentry_sdk.traces.NoOpStreamedSpan :members: .. autoclass:: sentry_sdk.session.Session diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 8cd412213e..98dff9e88c 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -383,7 +383,6 @@ class DataCollection(TypedDict): EventProcessor = Callable[[Event, Hint], Optional[Event]] ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]] BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]] - TransactionProcessor = Callable[[Event, Hint], Optional[Event]] LogProcessor = Callable[[Log, Hint], Optional[Log]] TracesSampler = Callable[[SamplingContext], Union[float, int, bool]] diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 7e7a25f65d..71930e3299 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -55,7 +55,6 @@ class CompressionAlgo(Enum): Metric, SpanJSON, TracesSampler, - TransactionProcessor, ) # Experiments are feature flags to enable and disable certain unstable SDK @@ -1310,7 +1309,6 @@ def __init__( send_client_reports: bool = True, _experiments: "Experiments" = {}, # noqa: B006 proxy_headers: "Optional[Dict[str, str]]" = None, - before_send_transaction: "Optional[TransactionProcessor]" = None, project_root: "Optional[str]" = None, include_local_variables: "Optional[bool]" = True, include_source_context: "Optional[bool]" = True, From 998101716185eca0c60b7f1a66943e6e05ffebf1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 13:16:04 +0200 Subject: [PATCH 15/28] transactions events --- sentry_sdk/client.py | 131 ++-------------------------------------- sentry_sdk/consts.py | 5 -- sentry_sdk/envelope.py | 17 +----- sentry_sdk/scope.py | 7 +-- sentry_sdk/transport.py | 9 +-- 5 files changed, 11 insertions(+), 158 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index ae6b385c5a..336ee53cc2 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -27,7 +27,7 @@ _map_from_send_default_pii, _resolve_data_collection, ) -from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.envelope import Envelope, Item from sentry_sdk.integrations import setup_integrations from sentry_sdk.integrations.dedupe import DedupeIntegration from sentry_sdk.monitor import Monitor @@ -706,10 +706,7 @@ def _prepare_event( if event.get("timestamp") is None: event["timestamp"] = datetime.now(timezone.utc) - is_transaction = event.get("type") == "transaction" - if scope is not None: - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) event_ = scope.apply_to_event(event, hint, self.options) # one of the event/error processors returned None @@ -717,51 +714,14 @@ def _prepare_event( if self.transport: self.transport.record_lost_event( "event_processor", - data_category=("transaction" if is_transaction else "error"), + data_category="error", ) - if is_transaction: - self.transport.record_lost_event( - "event_processor", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) return None event = event_ - spans_delta = spans_before - len( - cast(List[Dict[str, object]], event.get("spans", [])) - ) - span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0) - - if is_transaction and self.transport is not None: - if spans_delta > 0: - self.transport.record_lost_event( - "event_processor", data_category="span", quantity=spans_delta - ) - if span_recorder_dropped_spans > 0: - self.transport.record_lost_event( - "buffer_overflow", - data_category="span", - quantity=span_recorder_dropped_spans, - ) - - dropped_spans: int = span_recorder_dropped_spans + spans_delta - if dropped_spans > 0: - previous_total_spans = spans_before + dropped_spans - if scope._n_breadcrumbs_truncated > 0: - breadcrumbs = event.get("breadcrumbs", {}) - values = ( - breadcrumbs.get("values", []) - if not isinstance(breadcrumbs, AnnotatedValue) - else [] - ) - previous_total_breadcrumbs = ( - len(values) + scope._n_breadcrumbs_truncated - ) if ( - not is_transaction - and self.options["attach_stacktrace"] + self.options["attach_stacktrace"] and "exception" not in event and "stacktrace" not in event and "threads" not in event @@ -846,11 +806,7 @@ def _prepare_event( ) before_send = self.options["before_send"] - if ( - before_send is not None - and event is not None - and event.get("type") != "transaction" - ): + if before_send is not None and event is not None: new_event = None with capture_internal_exceptions(): new_event = before_send(event, hint or {}) @@ -870,36 +826,6 @@ def _prepare_event( event = new_event - before_send_transaction = self.options["before_send_transaction"] - if ( - before_send_transaction is not None - and event is not None - and event.get("type") == "transaction" - ): - new_event = None - spans_before = len(cast(List[Dict[str, object]], event.get("spans", []))) - with capture_internal_exceptions(): - new_event = before_send_transaction(event, hint or {}) - if new_event is None: - logger.info("before send transaction dropped event") - if self.transport: - self.transport.record_lost_event( - reason="before_send", data_category="transaction" - ) - self.transport.record_lost_event( - reason="before_send", - data_category="span", - quantity=spans_before + 1, # +1 for the transaction itself - ) - else: - spans_delta = spans_before - len(new_event.get("spans", [])) - if spans_delta > 0 and self.transport is not None: - self.transport.record_lost_event( - reason="before_send", data_category="span", quantity=spans_delta - ) - - event = new_event - return event def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool: @@ -929,11 +855,6 @@ def _should_capture( hint: "Hint", scope: "Optional[Scope]" = None, ) -> bool: - # Transactions are sampled independent of error events. - is_transaction = event.get("type") == "transaction" - if is_transaction: - return True - ignoring_prevents_recursion = scope is not None and not scope._should_capture if ignoring_prevents_recursion: return False @@ -1053,7 +974,6 @@ def capture_event( if event_id is None: event["event_id"] = event_id = uuid.uuid4().hex - span_recorder_has_gen_ai_span = event.pop("_has_gen_ai_span", False) event_opt = self._prepare_event(event, hint, scope) if event_opt is None: return None @@ -1064,14 +984,9 @@ def capture_event( if session: self._update_session_from_event(session, event) - is_transaction = event_opt.get("type") == "transaction" is_checkin = event_opt.get("type") == "check_in" - if ( - not is_transaction - and not is_checkin - and not self._should_sample_error(event, hint) - ): + if not is_checkin and not self._should_sample_error(event, hint): return None attachments = hint.get("attachments") @@ -1089,41 +1004,7 @@ def capture_event( envelope = Envelope(headers=headers) - if is_transaction and not span_recorder_has_gen_ai_span: - envelope.add_transaction(event_opt) - elif is_transaction: - split_spans = _split_gen_ai_spans(event_opt) - if split_spans is None or not split_spans[1]: - envelope.add_transaction(event_opt) - else: - non_gen_ai_spans, gen_ai_spans = split_spans - - event_opt["spans"] = non_gen_ai_spans - envelope.add_transaction(event_opt) - - converted_gen_ai_spans = [ - _serialized_v1_span_to_serialized_v2_span(span, event_opt) - for span in gen_ai_spans - if isinstance(span, dict) - ] - - envelope.add_item( - Item( - type=SpanBatcher.TYPE, - content_type=SpanBatcher.CONTENT_TYPE, - headers={ - "item_count": len(converted_gen_ai_spans), - }, - payload=PayloadRef( - json={ - "version": 2, - "items": converted_gen_ai_spans, - }, - ), - ) - ) - - elif is_checkin: + if is_checkin: envelope.add_checkin(event_opt) else: envelope.add_event(event_opt) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 71930e3299..b1c714d6e4 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1546,11 +1546,6 @@ def __init__( By the time `before_send` is executed, all scope data has already been applied to the event. Further modification of the scope won't have any effect. - :param before_send_transaction: This function is called with an SDK-specific transaction event object, and can - return a modified transaction event object, or `null` to skip reporting the event. - - One way this might be used is for manual PII stripping before sending. - :param before_breadcrumb: This function is called with an SDK-specific breadcrumb object before the breadcrumb is added to the scope. diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index c4e3a4d42d..9557a238d6 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -23,7 +23,7 @@ class Envelope: """ Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular, - each envelope may have at most one Item with type "event" or "transaction" (but not both). + each envelope may have at most one Item with type "event". """ def __init__( @@ -53,12 +53,6 @@ def add_event( ) -> None: self.add_item(Item(payload=PayloadRef(json=event), type="event")) - def add_transaction( - self, - transaction: "Event", - ) -> None: - self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction")) - def add_profile_chunk( self, profile_chunk: "Any", @@ -241,8 +235,6 @@ def data_category(self) -> "EventDataCategory": return "session" elif ty == "attachment": return "attachment" - elif ty == "transaction": - return "transaction" elif ty == "span": return "span" elif ty == "event": @@ -271,11 +263,6 @@ def get_event(self) -> "Optional[Event]": return self.payload.json return None - def get_transaction_event(self) -> "Optional[Event]": - if self.type == "transaction" and self.payload.json is not None: - return self.payload.json - return None - def serialize_into( self, f: "Any", @@ -310,7 +297,7 @@ def deserialize_from( # if no length was specified we need to read up to the end of line # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope) payload = f.readline().rstrip(b"\n") - if headers.get("type") in ("event", "transaction"): + if headers.get("type") == "event": rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload))) else: rv = cls(headers=headers, payload=payload) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index fc008ea1cf..05dbce206f 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1146,7 +1146,7 @@ def capture_event( event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope) - if event_id is not None and event.get("type") != "transaction": + if event_id is not None: self.get_isolation_scope()._last_event_id = event_id return event_id @@ -1539,7 +1539,6 @@ def apply_to_event( ) -> "Optional[Event]": """Applies the information contained on the scope to the given event.""" ty = event.get("type") - is_transaction = ty == "transaction" is_check_in = ty == "check_in" # put all attachments into the hint. This lets callbacks play around @@ -1547,7 +1546,7 @@ def apply_to_event( # create the envelope. attachments_to_send = hint.get("attachments") or [] for attachment in self._attachments: - if not is_transaction or attachment.add_to_transactions: + if attachment.add_to_transactions: attachments_to_send.append(attachment) hint["attachments"] = attachments_to_send @@ -1567,8 +1566,6 @@ def apply_to_event( self._apply_transaction_info_to_event(event, hint, options) self._apply_tags_to_event(event, hint, options) self._apply_extra_to_event(event, hint, options) - - if not is_transaction and not is_check_in: self._apply_breadcrumbs_to_event(event, hint, options) self._apply_flags_to_event(event, hint, options) diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 83af5c9509..9f4a6170e3 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -159,13 +159,6 @@ def record_lost_event( If an item is provided, the data category and quantity are extracted from the item, and the values passed for data_category and quantity are ignored. - - When recording a lost transaction via data_category="transaction", - the calling code should also record the lost spans via this method. - When recording lost spans, `quantity` should be set to the number - of contained spans, plus one for the transaction itself. When - passing an Item containing a transaction via the `item` parameter, - this method automatically records the lost spans. """ return None @@ -453,7 +446,7 @@ def _prepare_envelope( new_items = [] for item in envelope.items: if self._check_disabled(item.data_category): - if item.data_category in ("transaction", "error", "default", "statsd"): + if item.data_category in ("error", "default", "statsd"): self.on_dropped_event("self_rate_limits") self.record_lost_event("ratelimit_backoff", item=item) else: From ed330a8ca546ba3a680b1b3b13910d2992c4b766 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 13:16:50 +0200 Subject: [PATCH 16/28] more transaction events --- sentry_sdk/envelope.py | 7 ------- sentry_sdk/transport.py | 14 ++------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/envelope.py b/sentry_sdk/envelope.py index 9557a238d6..85a5a4c18f 100644 --- a/sentry_sdk/envelope.py +++ b/sentry_sdk/envelope.py @@ -98,13 +98,6 @@ def get_event(self) -> "Optional[Event]": return event return None - def get_transaction_event(self) -> "Optional[Event]": - for item in self.items: - event = item.get_transaction_event() - if event is not None: - return event - return None - def __iter__(self) -> "Iterator[Item]": return iter(self.items) diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 9f4a6170e3..42b795f681 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -36,7 +36,7 @@ except ImportError: ASYNC_TRANSPORT_AVAILABLE = False -from typing import TYPE_CHECKING, Dict, List, cast +from typing import TYPE_CHECKING, Dict, List import certifi import urllib3 @@ -264,17 +264,7 @@ def record_lost_event( data_category = item.data_category quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below). - if data_category == "transaction": - # Also record the lost spans - event = item.get_transaction_event() or {} - - # +1 for the transaction itself - span_count = ( - len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1 - ) - self.record_lost_event(reason, "span", quantity=span_count) - - elif data_category == "log_item" and item: + if data_category == "log_item" and item: # Also record size of lost logs in bytes bytes_size = len(item.get_bytes()) self.record_lost_event(reason, "log_byte", quantity=bytes_size) From cfd5920242fcd076edfcf310f9db2f892f403407 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 15:33:03 +0200 Subject: [PATCH 17/28] . --- sentry_sdk/scope.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 05dbce206f..23e5c9aca6 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1546,8 +1546,7 @@ def apply_to_event( # create the envelope. attachments_to_send = hint.get("attachments") or [] for attachment in self._attachments: - if attachment.add_to_transactions: - attachments_to_send.append(attachment) + attachments_to_send.append(attachment) hint["attachments"] = attachments_to_send self._apply_contexts_to_event(event, hint, options) From 452e47b73fe94c8452c5a9d336f6a47c89ad61f2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 15:37:18 +0200 Subject: [PATCH 18/28] previous_total_spans --- sentry_sdk/client.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 336ee53cc2..548d2cc0d0 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -700,7 +700,6 @@ def _prepare_event( hint: "Hint", scope: "Optional[Scope]", ) -> "Optional[Event]": - previous_total_spans: "Optional[int]" = None previous_total_breadcrumbs: "Optional[int]" = None if event.get("timestamp") is None: @@ -720,6 +719,17 @@ def _prepare_event( event = event_ + if scope._n_breadcrumbs_truncated > 0: + breadcrumbs = event.get("breadcrumbs", {}) + values = ( + breadcrumbs.get("values", []) + if not isinstance(breadcrumbs, AnnotatedValue) + else [] + ) + previous_total_breadcrumbs = ( + len(values) + scope._n_breadcrumbs_truncated + ) + if ( self.options["attach_stacktrace"] and "exception" not in event @@ -782,10 +792,7 @@ def _prepare_event( span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES], {"len": scope._gen_ai_original_message_count[span_id]}, ) - if previous_total_spans is not None: - event["spans"] = AnnotatedValue( - event.get("spans", []), {"len": previous_total_spans} - ) + if previous_total_breadcrumbs is not None: event["breadcrumbs"] = AnnotatedValue( event.get("breadcrumbs", {"values": []}), From 626e6e0c73bbac9d7c025f1c55299b8141c5590b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 16:45:47 +0200 Subject: [PATCH 19/28] more helpers --- tests/test_client.py | 99 ----------------------------------------- tests/test_transport.py | 14 ------ 2 files changed, 113 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 73b1d9fdc6..624fc7f713 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,7 +4,6 @@ import subprocess import sys import time -from collections import Counter, defaultdict from collections.abc import Mapping from textwrap import dedent from unittest import mock @@ -1277,104 +1276,6 @@ def test_uwsgi_warnings(sentry_init, opt, missing_flags): mock_logger.warning.assert_not_called() -class TestSpanClientReports: - """ - Tests for client reports related to spans. - """ - - __test__ = False - - @staticmethod - def span_dropper(spans_to_drop): - """ - Returns a function that can be used to drop spans from an event. - """ - - def drop_spans(event, _): - event["spans"] = event["spans"][spans_to_drop:] - return event - - return drop_spans - - @staticmethod - def mock_transaction_event(span_count): - """ - Returns a mock transaction event with the given number of spans. - """ - - return defaultdict( - mock.MagicMock, - type="transaction", - spans=[mock.MagicMock() for _ in range(span_count)], - ) - - def __init__(self, span_count): - """Configures a test case with the number of spans dropped and whether the transaction was dropped.""" - self.span_count = span_count - self.expected_record_lost_event_calls = Counter() - self.before_send = lambda event, _: event - self.event_processor = lambda event, _: event - - def _update_resulting_calls(self, reason, drops_transactions=0, drops_spans=0): - """ - Updates the expected calls with the given resulting calls. - """ - if drops_transactions > 0: - self.expected_record_lost_event_calls[ - (reason, "transaction", None, drops_transactions) - ] += 1 - - if drops_spans > 0: - self.expected_record_lost_event_calls[ - (reason, "span", None, drops_spans) - ] += 1 - - def with_before_send( - self, - before_send, - *, - drops_transactions=0, - drops_spans=0, - ): - self.before_send = before_send - self._update_resulting_calls( - "before_send", - drops_transactions, - drops_spans, - ) - - return self - - def with_event_processor( - self, - event_processor, - *, - drops_transactions=0, - drops_spans=0, - ): - self.event_processor = event_processor - self._update_resulting_calls( - "event_processor", - drops_transactions, - drops_spans, - ) - - return self - - def run(self, sentry_init, capture_record_lost_event_calls): - """Runs the test case with the configured parameters.""" - sentry_init(before_send_transaction=self.before_send) - record_lost_event_calls = capture_record_lost_event_calls() - - with sentry_sdk.isolation_scope() as scope: - scope.add_event_processor(self.event_processor) - event = self.mock_transaction_event(self.span_count) - sentry_sdk.get_client().capture_event(event, scope=scope) - - # We use counters to ensure that the calls are made the expected number of times, disregarding order. - assert Counter(record_lost_event_calls) == self.expected_record_lost_event_calls - - def make_options_transport_cls(): """Make an options transport class that captures the options passed to it.""" # We need a unique class for each test so that the options are not diff --git a/tests/test_transport.py b/tests/test_transport.py index f29d6a4b65..e02473875d 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -4,7 +4,6 @@ import pickle import socket import sys -from collections import defaultdict from datetime import datetime, timedelta, timezone from unittest import mock @@ -94,19 +93,6 @@ def inner(**kwargs): return inner -def mock_transaction_envelope(span_count: int) -> "Envelope": - event = defaultdict( - mock.MagicMock, - type="transaction", - spans=[mock.MagicMock() for _ in range(span_count)], - ) - - envelope = Envelope() - envelope.add_transaction(event) - - return envelope - - # The compression-relevant dimensions (level x algo x http2) are fully # crossed; debug, flush method and pickling are rotated through the cases # so every value of every dimension is still exercised. From f6a1dc822c1f411913ffd1362db91c8614e89d85 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 09:52:33 +0200 Subject: [PATCH 20/28] bad merge --- tests/test_transport.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index 3784a976a0..a21ff3b1e2 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -541,14 +541,13 @@ def record_lost_event(reason, data_category=None, item=None): client.transport.capture_envelope(session_envelope) client.flush() - # events go through but attachments are dropped - assert len(capturing_server.captured) == 2 + assert not capturing_server.captured - client.capture_event({"type": "error"}) + client.capture_event({"type": "event"}) client.flush() - assert len(capturing_server.captured) == 3 - assert capturing_server.captured[2].path == "/api/132/envelope/" + assert len(capturing_server.captured) == 1 + assert capturing_server.captured[0].path == "/api/132/envelope/" assert captured_outcomes == [ ("ratelimit_backoff", "session"), From e177eaf3797ab12f0e8cc4911a5d0c2fc2b16466 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 10:21:39 +0200 Subject: [PATCH 21/28] chore: Remove more old transaction stuff --- sentry_sdk/client.py | 207 +------------------------------------------ sentry_sdk/scope.py | 71 ++------------- 2 files changed, 7 insertions(+), 271 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 548d2cc0d0..b66c29a651 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -1,11 +1,8 @@ -import json import os -import platform import random import socket -import sys import uuid -from collections.abc import Iterable, Mapping +from collections.abc import Mapping from contextvars import ContextVar from datetime import datetime, timezone from importlib import import_module @@ -19,7 +16,6 @@ DEFAULT_MAX_VALUE_LENGTH, DEFAULT_OPTIONS, SPANDATA, - SPANSTATUS, VERSION, ClientConstructor, ) @@ -35,7 +31,7 @@ from sentry_sdk.scrubber import EventScrubber from sentry_sdk.serializer import serialize from sentry_sdk.sessions import SessionFlusher -from sentry_sdk.traces import SpanStatus, StreamedSpan, trace +from sentry_sdk.traces import StreamedSpan, trace from sentry_sdk.transport import ( AsyncHttpTransport, HttpTransportCore, @@ -45,7 +41,6 @@ AnnotatedValue, capture_internal_exceptions, current_stacktrace, - datetime_from_isoformat, env_to_bool, format_timestamp, get_before_send_log, @@ -71,7 +66,6 @@ Log, Metric, SDKInfo, - SerializedAttributeValue, ) from sentry_sdk.integrations import Integration from sentry_sdk.scope import Scope @@ -93,203 +87,6 @@ } -def _serialized_v1_attribute_to_serialized_v2_attribute( - attribute_value: "Any", -) -> "Optional[SerializedAttributeValue]": - if isinstance(attribute_value, bool): - return { - "value": attribute_value, - "type": "boolean", - } - - if isinstance(attribute_value, int): - return { - "value": attribute_value, - "type": "integer", - } - - if isinstance(attribute_value, float): - return { - "value": attribute_value, - "type": "double", - } - - if isinstance(attribute_value, str): - return { - "value": attribute_value, - "type": "string", - } - - if isinstance(attribute_value, list): - if not attribute_value: - return {"value": [], "type": "array"} - - ty = type(attribute_value[0]) - if ty in (int, str, bool, float) and all( - type(v) is ty for v in attribute_value - ): - return { - "value": attribute_value, - "type": "array", - } - - # Types returned when the serializer for V1 span attributes recurses into some container types. - if isinstance(attribute_value, (dict, list)): - return { - "value": json.dumps(attribute_value), - "type": "string", - } - - return None - - -def _serialized_v1_span_to_serialized_v2_span( - span: "dict[str, Any]", event: "Event" -) -> "dict[str, Any]": - # See SpanBatcher._to_transport_format() for analogous population of all entries except "attributes". - res: "dict[str, Any]" = { - "status": SpanStatus.OK.value, - "is_segment": False, - } - - if "trace_id" in span: - res["trace_id"] = span["trace_id"] - - if "span_id" in span: - res["span_id"] = span["span_id"] - - if "description" in span: - description = span["description"] - - if description is None and "op" in span: - description = span["op"] - - res["name"] = description - - if "start_timestamp" in span: - start_timestamp = None - try: - start_timestamp = datetime_from_isoformat(span["start_timestamp"]) - except Exception: - pass - - if start_timestamp is not None: - res["start_timestamp"] = start_timestamp.timestamp() - - if "timestamp" in span: - end_timestamp = None - try: - end_timestamp = datetime_from_isoformat(span["timestamp"]) - except Exception: - pass - - if end_timestamp is not None: - res["end_timestamp"] = end_timestamp.timestamp() - - if "parent_span_id" in span: - res["parent_span_id"] = span["parent_span_id"] - - if "status" in span and span["status"] != SPANSTATUS.OK: - res["status"] = "error" - - attributes: "Dict[str, Any]" = {} - - if "op" in span: - attributes["sentry.op"] = span["op"] - if "origin" in span: - attributes["sentry.origin"] = span["origin"] - - span_data = span.get("data") - if isinstance(span_data, dict): - attributes.update(span_data) - - span_tags = span.get("tags") - if isinstance(span_tags, dict): - attributes.update(span_tags) - - # See Scope._apply_user_attributes_to_telemetry() for user attributes. - user = event.get("user") - if isinstance(user, dict): - if "id" in user: - attributes["user.id"] = user["id"] - if "username" in user: - attributes["user.name"] = user["username"] - if "email" in user: - attributes["user.email"] = user["email"] - - # See Scope.set_global_attributes() for release, environment, and SDK metadata. - if "release" in event: - attributes["sentry.release"] = event["release"] - if "environment" in event: - attributes["sentry.environment"] = event["environment"] - if "server_name" in event: - attributes["server.address"] = event["server_name"] - if "transaction" in event: - attributes["sentry.segment.name"] = event["transaction"] - - trace_context = event.get("contexts", {}).get("trace", {}) - if "span_id" in trace_context: - attributes["sentry.segment.id"] = trace_context["span_id"] - - sdk_info = event.get("sdk") - if isinstance(sdk_info, dict): - if "name" in sdk_info: - attributes["sentry.sdk.name"] = sdk_info["name"] - if "version" in sdk_info: - attributes["sentry.sdk.version"] = sdk_info["version"] - - attributes["process.runtime.name"] = platform.python_implementation() - attributes["process.runtime.version"] = ( - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - ) - - if not attributes: - return res - - res["attributes"] = {} - for key, value in attributes.items(): - converted_value = _serialized_v1_attribute_to_serialized_v2_attribute(value) - if converted_value is None: - continue - - res["attributes"][key] = converted_value - - # Remove redundant attribute, as status is stored in the status field. - if "status" in res["attributes"]: - del res["attributes"]["status"] - - return res - - -def _split_gen_ai_spans( - event_opt: "Event", -) -> "Optional[tuple[List[Dict[str, object]], List[Dict[str, object]]]]": - if "spans" not in event_opt: - return None - - spans: "Any" = event_opt["spans"] - if isinstance(spans, AnnotatedValue): - spans = spans.value - - if not isinstance(spans, Iterable): - return None - - non_gen_ai_spans = [] - gen_ai_spans = [] - for span in spans: - if not isinstance(span, dict): - non_gen_ai_spans.append(span) - continue - - span_op = span.get("op") - if isinstance(span_op, str) and span_op.startswith("gen_ai."): - gen_ai_spans.append(span) - else: - non_gen_ai_spans.append(span) - - return non_gen_ai_spans, gen_ai_spans - - def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": if args and (isinstance(args[0], (bytes, str)) or args[0] is None): dsn: "Optional[str]" = args[0] diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 23e5c9aca6..a19d0333d9 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -41,7 +41,6 @@ capture_internal_exception, capture_internal_exceptions, datetime_from_isoformat, - deprecation_warning, disable_capture_event, event_from_exception, exc_info_from_error, @@ -719,75 +718,15 @@ def fingerprint(self, value: "Optional[List[str]]") -> None: """When set this overrides the default fingerprint.""" self._fingerprint = value - @property - def transaction(self) -> "Any": - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 - """Return the transaction (root span) in the scope, if any.""" - - # there is no span/transaction on the scope - if self._span is None: - return None - - if isinstance(self._span, StreamedSpan): - deprecation_warning( - "Scope.transaction is not available in streaming mode.", - ) - return None - - # there is an orphan span on the scope - if self._span.containing_transaction is None: - return None - - # there is either a transaction (which is its own containing - # transaction) or a non-orphan span on the scope - return self._span.containing_transaction - - @transaction.setter - def transaction(self, value: "Any") -> None: - # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 - """When set this forces a specific transaction name to be set. - - Deprecated: use set_transaction_name instead.""" - - # XXX: the docstring above is misleading. The implementation of - # apply_to_event prefers an existing value of event.transaction over - # anything set in the scope. - # XXX: note that with the introduction of the Scope.transaction getter, - # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. - # Without breaking version compatibility, we could make the setter set a - # transaction name or transaction (self._span) depending on the type of - # the value argument. - - logger.warning( - "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." - ) - self._transaction = value - if self._span: - if isinstance(self._span, StreamedSpan): - deprecation_warning( - "Scope.transaction is not available in streaming mode.", - ) - return None - - if self._span.containing_transaction: - self._span.containing_transaction.name = value - def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None: """Set the transaction name and optionally the transaction source.""" self._transaction = name if self._span: - if isinstance(self._span, StreamedSpan): - self._span._segment.name = name - if source: - self._span._segment.set_attribute( - "sentry.segment.name.source", getattr(source, "value", source) - ) - - elif self._span.containing_transaction: - self._span.containing_transaction.name = name - if source: - self._span.containing_transaction.source = source + self._span._segment.name = name + if source: + self._span._segment.set_attribute( + "sentry.segment.name.source", getattr(source, "value", source) + ) if source: self._transaction_info["source"] = source From 64d58a02de43cc03e31de0059c36800b5136b6a5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 11:21:03 +0200 Subject: [PATCH 22/28] arq --- sentry_sdk/integrations/arq.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/sentry_sdk/integrations/arq.py b/sentry_sdk/integrations/arq.py index fa03233bf5..ba8c7bea90 100644 --- a/sentry_sdk/integrations/arq.py +++ b/sentry_sdk/integrations/arq.py @@ -1,7 +1,7 @@ import sys import sentry_sdk -from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.integrations.logging import ignore_logger_for_events from sentry_sdk.scope import should_send_default_pii @@ -125,15 +125,6 @@ async def _sentry_run_job(self: "Worker", job_id: str, score: int) -> None: def _capture_exception(exc_info: "ExcInfo") -> None: - scope = sentry_sdk.get_current_scope() - - if scope.transaction is not None: - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: - scope.transaction.set_status(SPANSTATUS.ABORTED) - return - - scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR) - if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS: return @@ -150,11 +141,6 @@ def _make_event_processor( ) -> "EventProcessor": def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]": with capture_internal_exceptions(): - scope = sentry_sdk.get_current_scope() - if scope.transaction is not None: - scope.transaction.name = ctx["job_name"] - event["transaction"] = ctx["job_name"] - tags = event.setdefault("tags", {}) tags["arq_task_id"] = ctx["job_id"] tags["arq_task_retry"] = ctx["job_try"] > 1 From 187600491adad7d41f9e2af62dd87502a60dd54e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 11:24:26 +0200 Subject: [PATCH 23/28] django --- sentry_sdk/integrations/django/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 565d0e88ef..8e59a64415 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -425,11 +425,13 @@ def _set_transaction_name_and_source( if hasattr(urlconf, "handler404"): handler = urlconf.handler404 if isinstance(handler, str): - scope.transaction = handler + scope.set_transaction_name(handler) else: - scope.transaction = transaction_from_function( + name = transaction_from_function( getattr(handler, "view_class", handler) ) + if name: + scope.set_transaction_name(name) except Exception: pass From 73595036f3a35ef7f550cd1017ba5b809562fd51 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 11:35:34 +0200 Subject: [PATCH 24/28] . --- MIGRATION_GUIDE.md | 1 + sentry_sdk/_types.py | 1 - sentry_sdk/api.py | 5 +---- sentry_sdk/attachments.py | 6 +----- sentry_sdk/consts.py | 21 ++++++++------------- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 6dbc607475..28f50548a6 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -154,6 +154,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - The `update_current_span` API was removed. - `SanicIntegration` no longer accepts `unsampled_statuses`. - The `trace_ignore_status_codes` option was removed. +- `add_attachment` no longer accepts an `add_to_transactions` argument. ## Deprecated diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 98dff9e88c..4634a88156 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -394,7 +394,6 @@ class DataCollection(TypedDict): "default", "error", "crash", - "transaction", "security", "attachment", "session", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 3d2baef89c..bae0526442 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -174,11 +174,8 @@ def add_attachment( filename: "Optional[str]" = None, path: "Optional[str]" = None, content_type: "Optional[str]" = None, - add_to_transactions: bool = False, ) -> None: - return get_isolation_scope().add_attachment( - bytes, filename, path, content_type, add_to_transactions - ) + return get_isolation_scope().add_attachment(bytes, filename, path, content_type) @scopemethod diff --git a/sentry_sdk/attachments.py b/sentry_sdk/attachments.py index 4d69d3acf2..7bcf59e181 100644 --- a/sentry_sdk/attachments.py +++ b/sentry_sdk/attachments.py @@ -13,8 +13,7 @@ class Attachment: This class stores attachments that can be sent along with an event. Attachments are files or other data, e.g. config or log files, that are relevant to an event. Attachments are set on the ``Scope``, and are sent along with - all non-transaction events (or all events including transactions if ``add_to_transactions`` is ``True``) that are - captured within the ``Scope``. + all non-transaction events that are captured within the ``Scope``. To add an attachment to a ``Scope``, use :py:meth:`sentry_sdk.Scope.add_attachment`. The parameters for ``add_attachment`` are the same as the parameters for this class's constructor. @@ -25,7 +24,6 @@ class Attachment: :param path: Path to a file to attach. Must be provided unless ``bytes`` is provided. :param content_type: The content type of the attachment. If not provided, it will be guessed from the ``filename`` parameter, if available, or the ``path`` parameter if ``filename`` is ``None``. - :param add_to_transactions: Whether to add this attachment to transactions. Defaults to ``False``. """ def __init__( @@ -34,7 +32,6 @@ def __init__( filename: "Optional[str]" = None, path: "Optional[str]" = None, content_type: "Optional[str]" = None, - add_to_transactions: bool = False, ) -> None: if bytes is None and path is None: raise TypeError("path or raw bytes required for attachment") @@ -48,7 +45,6 @@ def __init__( self.filename = filename self.path = path self.content_type = content_type - self.add_to_transactions = add_to_transactions def to_envelope_item(self) -> "Item": """Returns an envelope item for this attachment.""" diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index f50f8e2141..e7e019b037 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1615,28 +1615,28 @@ def __init__( If provided, the options will override the default `urllib3` `socket options `_. - :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given transaction + :param traces_sample_rate: A number between `0` and `1`, controlling the percentage chance a given service span will be sent to Sentry. - (`0` represents 0% while `1` represents 100%.) Applies equally to all transactions created in the app. + (`0` represents 0% while `1` represents 100%.) Applies equally to all service spans created in the app. Either this or `traces_sampler` must be defined to enable tracing. If `traces_sample_rate` is `0`, this means that no new traces will be created. However, if you have another service (for example a JS frontend) that makes requests to your service that include trace - information, those traces will be continued and thus transactions will be sent to Sentry. + information, those traces will be continued and thus spans will be sent to Sentry. If you want to disable all tracing you need to set `traces_sample_rate=None`. In this case, no new traces will be started and no incoming traces will be continued. - :param traces_sampler: A function responsible for determining the percentage chance a given transaction will be + :param traces_sampler: A function responsible for determining the percentage chance a given service span will be sent to Sentry. - It will automatically be passed information about the transaction and the context in which it's being + It will automatically be passed information about the service span and the context in which it's being created, and must return a number between `0` (0% chance of being sent) and `1` (100% chance of being sent). - Can also be used for filtering transactions, by returning `0` for those that are unwanted. + Can also be used for filtering service spans, by returning `0` for those that are unwanted. Either this or `traces_sample_rate` must be defined to enable tracing. @@ -1727,17 +1727,12 @@ def __init__( :param before_send_span: An optional function to modify spans before they're sent to Sentry. Modifications to the span's attributes and name will be retained. Unlike ``before_send_log`` - and ``before_send_metric``, spans cannot be dropped by returning None. Only works when - ``trace_lifecycle="stream"`` is enabled. - - :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to - reduce downstream data loss. + and ``before_send_metric``, spans cannot be dropped by returning None. :param trace_lifecycle: Controls how traces are sent. Set to `"stream"` to send spans as they finish, or `"static"` to send a completed trace as a transaction event. - :param ignore_spans: A sequence of span-matching rules. Matching spans are ignored when - `trace_lifecycle="stream"` is enabled. + :param ignore_spans: A sequence of span-matching rules. Matching spans are ignored. :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. From 3d37ea9bb6eba68882bdc7f9e79af094a93f918d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 11:38:54 +0200 Subject: [PATCH 25/28] . --- sentry_sdk/scope.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index a19d0333d9..0fdab7f96b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -868,7 +868,6 @@ def add_attachment( filename: "Optional[str]" = None, path: "Optional[str]" = None, content_type: "Optional[str]" = None, - add_to_transactions: bool = False, ) -> None: """Adds an attachment to future events sent from this scope. @@ -880,7 +879,6 @@ def add_attachment( path=path, filename=filename, content_type=content_type, - add_to_transactions=add_to_transactions, ) ) From 015e539734cf65c4998db83ac8d9fb04d086d941 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 11:41:02 +0200 Subject: [PATCH 26/28] base64??? --- CONTRIBUTING.md | 4 +-- sentry_sdk/utils.py | 38 ------------------------- tests/utils/test_general.py | 55 ------------------------------------- 3 files changed, 2 insertions(+), 95 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b31d918e88..11a1a66ceb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,7 +100,7 @@ uv run ruff format tests sentry_sdk We use `mypy` for typing our codebase. Use the following command to typecheck the source: ``` -uv run --group typing mypy sentry_sdk +uv run --group typing mypy sentry_sdk ``` ##### Install Coding Style Pre-commit Hooks @@ -167,7 +167,7 @@ TESTPATH=tests/integrations/logging/test_logging.py uv run tox -e py3.12-common To select specific tests, you can forward arguments to `pytest` like so ```bash -uv run tox -e py3.12-celery-v5.5.3 -- -k test_transaction_events +uv run tox -e py3.12-celery-v5.5.3 -- -k test_simple_with_performance ``` In general, you use diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index c500f55574..2aed77daaa 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1,4 +1,3 @@ -import base64 import copy import json import linecache @@ -1483,43 +1482,6 @@ def run(self) -> None: raise ServerlessTimeoutWarning("WARNING: Function is about to time out.") -def to_base64(original: str) -> "Optional[str]": - """ - Convert a string to base64, via UTF-8. Returns None on invalid input. - """ - base64_string = None - - try: - utf8_bytes = original.encode("UTF-8") - base64_bytes = base64.b64encode(utf8_bytes) - base64_string = base64_bytes.decode("UTF-8") - except Exception as err: - logger.warning("Unable to encode {orig} to base64:".format(orig=original), err) - - return base64_string - - -def from_base64(base64_string: str) -> "Optional[str]": - """ - Convert a string from base64, via UTF-8. Returns None on invalid input. - """ - utf8_string = None - - try: - only_valid_chars = BASE64_ALPHABET.match(base64_string) - assert only_valid_chars - - base64_bytes = base64_string.encode("UTF-8") - utf8_bytes = base64.b64decode(base64_bytes) - utf8_string = utf8_bytes.decode("UTF-8") - except Exception as err: - logger.warning( - "Unable to decode {b64} from base64:".format(b64=base64_string), err - ) - - return utf8_string - - Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"]) diff --git a/tests/utils/test_general.py b/tests/utils/test_general.py index 219ccd4180..ed0df8b3b5 100644 --- a/tests/utils/test_general.py +++ b/tests/utils/test_general.py @@ -11,12 +11,10 @@ Dsn, exceptions_from_error_tuple, filename_for_module, - from_base64, iter_event_stacktraces, safe_repr, set_in_app_in_frames, strip_string, - to_base64, ) try: @@ -550,59 +548,6 @@ def test_iter_stacktraces(): ) == {1, 2, 3} -@pytest.mark.parametrize( - ("original", "base64_encoded"), - [ - # ascii only - ("Dogs are great!", "RG9ncyBhcmUgZ3JlYXQh"), - # emoji - ("🐶", "8J+Qtg=="), - # non-ascii - ( - "Καλό κορίτσι, Μάιζεϊ!", - "zprOsc67z4wgzrrOv8+Bzq/PhM+DzrksIM6czqzOuc62zrXPiiE=", - ), - # mix of ascii and non-ascii - ( - "Of margir hundar! Ég geri ráð fyrir að ég þurfi stærra rúm.", - "T2YgbWFyZ2lyIGh1bmRhciEgw4lnIGdlcmkgcsOhw7AgZnlyaXIgYcOwIMOpZyDDvnVyZmkgc3TDpnJyYSByw7ptLg==", - ), - ], -) -def test_successful_base64_conversion(original, base64_encoded): - # all unicode characters should be handled correctly - assert to_base64(original) == base64_encoded - assert from_base64(base64_encoded) == original - - # "to" and "from" should be inverses - assert from_base64(to_base64(original)) == original - assert to_base64(from_base64(base64_encoded)) == base64_encoded - - -@pytest.mark.parametrize( - "input", - [ - 1231, # incorrect type - True, # incorrect type - [], # incorrect type - {}, # incorrect type - None, # incorrect type - "yayfordogs", # wrong length - "#dog", # invalid ascii character - "🐶", # non-ascii character - ], -) -def test_failed_base64_conversion(input): - # conversion from base64 should fail if given input of the wrong type or - # input which isn't a valid base64 string - assert from_base64(input) is None - - # any string can be converted to base64, so only type errors will cause - # failures - if not isinstance(input, str): - assert to_base64(input) is None - - @pytest.mark.parametrize( "input,max_length,result", [ From 7766b0a031475a8466a8a541af6356bd115641f5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 11:44:06 +0200 Subject: [PATCH 27/28] more leftovers --- sentry_sdk/consts.py | 26 ------------- sentry_sdk/integrations/bottle.py | 30 -------------- sentry_sdk/integrations/falcon.py | 14 ------- sentry_sdk/tracing.py | 65 ------------------------------- 4 files changed, 135 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index e7e019b037..255cb6bf10 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1171,32 +1171,6 @@ class SPANDATA: """ -class SPANSTATUS: - """ - The status of a Sentry span. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - - ABORTED = "aborted" - ALREADY_EXISTS = "already_exists" - CANCELLED = "cancelled" - DATA_LOSS = "data_loss" - DEADLINE_EXCEEDED = "deadline_exceeded" - FAILED_PRECONDITION = "failed_precondition" - INTERNAL_ERROR = "internal_error" - INVALID_ARGUMENT = "invalid_argument" - NOT_FOUND = "not_found" - OK = "ok" - OUT_OF_RANGE = "out_of_range" - PERMISSION_DENIED = "permission_denied" - RESOURCE_EXHAUSTED = "resource_exhausted" - UNAUTHENTICATED = "unauthenticated" - UNAVAILABLE = "unavailable" - UNIMPLEMENTED = "unimplemented" - UNKNOWN_ERROR = "unknown_error" - - class OP: ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic" CACHE_GET = "cache.get" diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 06e360b06f..fb40c1b110 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -12,7 +12,6 @@ from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE -from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE from sentry_sdk.utils import ( capture_internal_exceptions, ensure_integration_enabled, @@ -199,39 +198,10 @@ def _set_segment_name_and_source(transaction_style: str) -> None: pass -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "Any" -) -> None: - name = "" - - if transaction_style == "url": - try: - name = request.route.rule or "" - except RuntimeError: - pass - - elif transaction_style == "endpoint": - try: - name = ( - request.route.name - or transaction_from_function(request.route.callback) - or "" - ) - except RuntimeError: - pass - - event["transaction"] = name - event["transaction_info"] = { - "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] - } - - def _make_request_event_processor( app: "Bottle", request: "LocalRequest", integration: "BottleIntegration" ) -> "EventProcessor": def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, request) - with capture_internal_exceptions(): BottleRequestExtractor(request).extract_into_event(event) diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index 3a15a77b36..a1a0df9fcd 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -5,7 +5,6 @@ from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.tracing import SOURCE_FOR_STYLE from sentry_sdk.utils import ( capture_internal_exceptions, ensure_integration_enabled, @@ -228,23 +227,10 @@ def _has_http_5xx_status(response: "falcon.Response") -> bool: return response.status.startswith("5") -def _set_transaction_name_and_source( - event: "Event", transaction_style: str, request: "falcon.Request" -) -> None: - name_for_style = { - "uri_template": request.uri_template, - "path": request.path, - } - event["transaction"] = name_for_style[transaction_style] - event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} - - def _make_request_event_processor( req: "falcon.Request", integration: "FalconIntegration" ) -> "EventProcessor": def event_processor(event: "Event", hint: "dict[str, Any]") -> "Event": - _set_transaction_name_and_source(event, integration.transaction_style, req) - with capture_internal_exceptions(): FalconRequestExtractor(req).extract_into_event(event) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 045e64160f..619bb6d000 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,25 +1,4 @@ from enum import Enum -from typing import TYPE_CHECKING - -from sentry_sdk.consts import SPANSTATUS - -if TYPE_CHECKING: - from typing import ( - ParamSpec, - TypeVar, - ) - - from typing_extensions import TypedDict - - P = ParamSpec("P") - R = TypeVar("R") - - ProfileContext = TypedDict( - "ProfileContext", - { - "profiler_id": str, - }, - ) BAGGAGE_HEADER_NAME = "baggage" SENTRY_TRACE_HEADER_NAME = "sentry-trace" @@ -39,11 +18,6 @@ def __str__(self) -> str: return self.value -# These are typically high cardinality and the server hates them -LOW_QUALITY_TRANSACTION_SOURCES = [ - TransactionSource.URL, -] - SOURCE_FOR_STYLE = { "endpoint": TransactionSource.COMPONENT, "function_name": TransactionSource.COMPONENT, @@ -56,43 +30,4 @@ def __str__(self) -> str: "url": TransactionSource.ROUTE, } - -def get_span_status_from_http_code(http_status_code: int) -> str: - """ - Returns the Sentry status corresponding to the given HTTP status code. - - See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context - """ - if http_status_code < 400: - return SPANSTATUS.OK - - elif 400 <= http_status_code < 500: - if http_status_code == 403: - return SPANSTATUS.PERMISSION_DENIED - elif http_status_code == 404: - return SPANSTATUS.NOT_FOUND - elif http_status_code == 429: - return SPANSTATUS.RESOURCE_EXHAUSTED - elif http_status_code == 413: - return SPANSTATUS.FAILED_PRECONDITION - elif http_status_code == 401: - return SPANSTATUS.UNAUTHENTICATED - elif http_status_code == 409: - return SPANSTATUS.ALREADY_EXISTS - else: - return SPANSTATUS.INVALID_ARGUMENT - - elif 500 <= http_status_code < 600: - if http_status_code == 504: - return SPANSTATUS.DEADLINE_EXCEEDED - elif http_status_code == 501: - return SPANSTATUS.UNIMPLEMENTED - elif http_status_code == 503: - return SPANSTATUS.UNAVAILABLE - else: - return SPANSTATUS.INTERNAL_ERROR - - return SPANSTATUS.UNKNOWN_ERROR - - # Circular imports From 34d7a47147acfdf3da01c9f55380a4427c52d876 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 14 Sep 2026 11:53:48 +0200 Subject: [PATCH 28/28] . --- tests/integrations/dramatiq/test_dramatiq.py | 14 +++++++------- tests/tracing/test_integration_tests.py | 4 +--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/integrations/dramatiq/test_dramatiq.py b/tests/integrations/dramatiq/test_dramatiq.py index 1bab8d1dfe..3e53625bc2 100644 --- a/tests/integrations/dramatiq/test_dramatiq.py +++ b/tests/integrations/dramatiq/test_dramatiq.py @@ -6,7 +6,7 @@ from dramatiq.middleware import Middleware, SkipMessage import sentry_sdk -from sentry_sdk.consts import SPANDATA, SPANSTATUS +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.dramatiq import DramatiqIntegration from sentry_sdk.integrations.logging import ignore_logger_for_events @@ -76,7 +76,7 @@ def dummy_actor(x, y): "traces_sample_rate": 1.0, "trace_lifecycle": "stream", }, - SPANSTATUS.INTERNAL_ERROR, + "error", False, ), ( @@ -84,7 +84,7 @@ def dummy_actor(x, y): "traces_sample_rate": 1.0, "trace_lifecycle": "stream", }, - SPANSTATUS.OK, + "ok", False, ), ( @@ -92,7 +92,7 @@ def dummy_actor(x, y): "traces_sample_rate": 1.0, "trace_lifecycle": "stream", }, - SPANSTATUS.INTERNAL_ERROR, + "error", True, ), ( @@ -100,7 +100,7 @@ def dummy_actor(x, y): "traces_sample_rate": 1.0, "trace_lifecycle": "stream", }, - SPANSTATUS.OK, + "ok", True, ), ], @@ -120,7 +120,7 @@ def test_task_transaction( expected_span_status, fail_fast, ): - task_fails = expected_span_status == SPANSTATUS.INTERNAL_ERROR + task_fails = expected_span_status == "error" items = capture_items("event", "span") @@ -130,7 +130,7 @@ def dummy_actor(x, y): dummy_actor.send(1, int(not task_fails)) - if expected_span_status == SPANSTATUS.INTERNAL_ERROR and fail_fast: + if expected_span_status == "error" and fail_fast: with pytest.raises(ZeroDivisionError): broker.join(dummy_actor.queue_name, fail_fast=fail_fast) else: diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index 97f253bcaf..5a5bd99001 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -8,7 +8,6 @@ import sentry_sdk from sentry_sdk import capture_message -from sentry_sdk.consts import SPANSTATUS from sentry_sdk.transport import Transport from tests.conftest import TestTransportWithOptions @@ -368,8 +367,7 @@ def test_non_error_exceptions( sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") items = capture_items() - with sentry_sdk.traces.start_span(name="hi") as segment: - segment.status = SPANSTATUS.OK + with sentry_sdk.traces.start_span(name="hi"): with pytest.raises(exception_cls): with sentry_sdk.traces.start_span(name="foo"): raise exception_cls(exception_value)