From 86f7d016e208a3033e1b534eea6cce43db6c82a7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 16:25:55 +0200 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 96c232d1819eba583b66fc560a269e79f03b44d1 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 16:45:47 +0200 Subject: [PATCH 4/5] 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 90b2ce5941ebd1a5700d45dfd49a11eb5e6f95ba Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 11 Sep 2026 16:49:50 +0200 Subject: [PATCH 5/5] use sessions in the transport tests --- tests/test_transport.py | 110 ++++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/tests/test_transport.py b/tests/test_transport.py index e02473875d..a21ff3b1e2 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -31,7 +31,6 @@ add_breadcrumb, capture_message, isolation_scope, - new_scope, ) from sentry_sdk._compat import PY37, PY38 from sentry_sdk.envelope import Envelope, Item, PayloadRef, parse_json @@ -514,40 +513,45 @@ def record_lost_event(reason, data_category=None, item=None): capturing_server.respond_with( code=response_code, - headers={"X-Sentry-Rate-Limits": "4711:attachment:organization"}, + headers={"X-Sentry-Rate-Limits": "4711:session:organization"}, ) - with new_scope() as scope: - scope.add_attachment(bytes=b"Hello", filename="hello.txt") - client.capture_event({"type": "error"}, scope=scope) + session_envelope = Envelope() + session_envelope.add_item( + Item(payload=PayloadRef(json={"sid": "123"}), type="session") + ) + client.transport.capture_envelope(session_envelope) 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"]) + assert set(client.transport._disabled_until) == set(["session"]) - 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) + session_envelope = Envelope() + session_envelope.add_item( + Item(payload=PayloadRef(json={"sid": "456"}), type="session") + ) + client.transport.capture_envelope(session_envelope) + session_envelope = Envelope() + session_envelope.add_item( + Item(payload=PayloadRef(json={"sid": "789"}), type="session") + ) + 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", "attachment"), - ("ratelimit_backoff", "attachment"), + ("ratelimit_backoff", "session"), + ("ratelimit_backoff", "session"), ] @@ -560,7 +564,7 @@ def test_data_category_limits_reporting( capturing_server.respond_with( code=response_code, headers={ - "X-Sentry-Rate-Limits": "4711:error:organization, 4711:attachment:organization" + "X-Sentry-Rate-Limits": "4711:session:organization, 4711:attachment:organization" }, ) @@ -577,23 +581,29 @@ 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) - with new_scope() as scope: - scope.add_attachment(bytes=b"Hello World", filename="hello.txt") - client.capture_event({"type": "error"}, scope=scope) + session_envelope = Envelope() + session_envelope.add_item( + Item(payload=PayloadRef(json={"sid": "123"}), type="session") + ) + client.transport.capture_envelope(session_envelope) 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", "error"]) + assert set(client.transport._disabled_until) == set(["attachment", "session"]) - 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) + session_envelope = Envelope() + session_envelope.add_item( + Item(payload=PayloadRef(json={"sid": "456"}), type="session") + ) + client.transport.capture_envelope(session_envelope) + session_envelope = Envelope() + session_envelope.add_item( + Item(payload=PayloadRef(json={"sid": "789"}), type="session") + ) + client.transport.capture_envelope(session_envelope) capturing_server.clear_captured() # flush out the events but don't flush the client reports @@ -601,45 +611,59 @@ def intercepting_fetch(*args, **kwargs): client.transport._last_client_report_sent = 0 outcomes_enabled = True - client.capture_event({"type": "error"}) + with isolation_scope() as scope: + scope.add_attachment(bytes=b"Hello World", filename="hello.txt") + client.capture_event({"type": "error"}, scope=scope) client.flush() - # the client report is flushed alongside the event (which is also dropped - # because error is rate-limited) + # the error goes through (not rate-limited), attachment is dropped; + # the client report piggybacks on the error 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()) + assert envelope.items[0].type == "event" + assert envelope.items[1].type == "client_report" + report = parse_json(envelope.items[1].get_bytes()) discarded_events = report["discarded_events"] assert len(discarded_events) == 2 assert { - "category": "error", + "category": "session", "reason": "ratelimit_backoff", - "quantity": 3, + "quantity": 2, } in discarded_events assert { "category": "attachment", "reason": "ratelimit_backoff", - "quantity": 22, + "quantity": 11, } in discarded_events capturing_server.clear_captured() - # send more events (error is still rate-limited) - client.capture_event({"type": "error"}) + # send a session (dropped) and a normal error (goes through) + session_envelope = Envelope() + session_envelope.add_item( + Item(payload=PayloadRef(json={"sid": "000"}), type="session") + ) + client.transport.capture_envelope(session_envelope) + client.capture_event({"type": "error", "release": "foo"}) client.flush() - assert len(capturing_server.captured) == 1 - envelope = capturing_server.captured[0].envelope + 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 envelope.items[0].type == "client_report" report = parse_json(envelope.items[0].get_bytes()) discarded_events = report["discarded_events"] assert len(discarded_events) == 1 assert { - "category": "error", + "category": "session", "reason": "ratelimit_backoff", "quantity": 1, } in discarded_events