From 30649d557458d8f2835101b1662e113a086a0275 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Tue, 2 Jun 2026 14:45:47 -0400 Subject: [PATCH 1/4] feat(bottle): Add span streaming support to Bottle integration Fixes PY-2310 Fixes #6008 --- sentry_sdk/integrations/bottle.py | 73 +++++- tests/integrations/bottle/test_bottle.py | 289 ++++++++++++++++++++++- 2 files changed, 346 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 42b5048941..4a51593f75 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -10,7 +10,9 @@ ) from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.traces import SegmentSource from sentry_sdk.tracing import SOURCE_FOR_STYLE +from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( capture_internal_exceptions, ensure_integration_enabled, @@ -100,7 +102,38 @@ def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": scope.add_event_processor( _make_request_event_processor(self, bottle_request, integration) ) - res = old_handle(self, environ) + + if has_span_streaming_enabled(sentry_sdk.get_client().options): + # Using SegmentSource.COMPONENT as the default since this is the also value for "handler_name" + # within SOURCE_FOR_STYLE. + # See https://github.com/getsentry/sentry-python/blob/c3aab3932f5a7e89ad3aff551a206db710acf0e6/sentry_sdk/tracing.py#L151-L161 + with sentry_sdk.traces.start_span( + name="bottle", + attributes={"sentry.span.source": SegmentSource.COMPONENT}, + ) as span: + res = old_handle(self, environ) + + try: + if integration.transaction_style == "url": + span.name = bottle_request.route.rule or "bottle" + span.set_attribute("sentry.span.source", SegmentSource.URL) + elif integration.transaction_style == "endpoint": + name = ( + bottle_request.route.name + or transaction_from_function( + bottle_request.route.callback + ) + or "bottle" + ) + span.name = name + span.set_attribute( + "sentry.span.source", SegmentSource.COMPONENT + ) + except RuntimeError: + pass + + else: + res = old_handle(self, environ) return res @@ -119,17 +152,33 @@ def patched_make_callback( return prepared_callback def wrapped_callback(*args: object, **kwargs: object) -> "Any": - try: - res = prepared_callback(*args, **kwargs) - except Exception as exception: - _capture_exception(exception, handled=False) - raise exception - - if ( - isinstance(res, HTTPResponse) - and res.status_code in integration.failed_request_status_codes - ): - _capture_exception(res, handled=True) + if has_span_streaming_enabled(sentry_sdk.get_client().options): + with sentry_sdk.traces.start_span(name="bottle"): + try: + res = prepared_callback(*args, **kwargs) + except Exception as exception: + _capture_exception(exception, handled=False) + raise exception + + if ( + isinstance(res, HTTPResponse) + and res.status_code + in integration.failed_request_status_codes + ): + _capture_exception(res, handled=True) + + else: + try: + res = prepared_callback(*args, **kwargs) + except Exception as exception: + _capture_exception(exception, handled=False) + raise exception + + if ( + isinstance(res, HTTPResponse) + and res.status_code in integration.failed_request_status_codes + ): + _capture_exception(res, handled=True) return res diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index dd5e39a20f..b580bcc90c 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -8,6 +8,7 @@ from werkzeug.test import Client from werkzeug.wrappers import Response +import sentry_sdk from sentry_sdk import capture_message from sentry_sdk.integrations.bottle import BottleIntegration from sentry_sdk.integrations.logging import LoggingIntegration @@ -462,23 +463,37 @@ def here(): assert not events +@pytest.mark.parametrize("span_streaming", [True, False]) def test_span_origin( sentry_init, get_client, capture_events, + capture_items, + span_streaming, ): sentry_init( integrations=[BottleIntegration()], traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() client = get_client() client.get("/message") - (_, event) = events - - assert event["contexts"]["trace"]["origin"] == "auto.http.bottle" + if span_streaming: + sentry_sdk.flush() + spans = [item.payload for item in items] + segment = spans[-1] + assert segment["is_segment"] is True + assert segment["attributes"]["sentry.origin"] == "auto.http.bottle" + else: + (_, event) = events + assert event["contexts"]["trace"]["origin"] == "auto.http.bottle" @pytest.mark.parametrize("raise_error", [True, False]) @@ -556,3 +571,269 @@ def handle(): (event,) = events assert event["exception"]["values"][0]["type"] == "ZeroDivisionError" + + +def test_span_streaming_basic(sentry_init, capture_items): + sentry_init( + integrations=[BottleIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + items = capture_items("span") + + app = Bottle() + + @app.route("/message") + def hi(): + return "ok" + + client = Client(app) + client.get("/message") + + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 3 + + callback_span = spans[0] + handle_span = spans[1] + segment = spans[2] + + # Segment span (root, created by WSGI middleware) + assert segment["is_segment"] is True + assert "parent_span_id" not in segment + assert segment["status"] == "ok" + assert segment["attributes"]["sentry.op"] == "http.server" + assert segment["attributes"]["sentry.origin"] == "auto.http.bottle" + assert segment["attributes"]["http.request.method"] == "GET" + assert segment["attributes"]["http.response.status_code"] == 200 + + # Handle span (created by _patched_handle, child of segment) + assert handle_span["is_segment"] is False + assert handle_span["parent_span_id"] == segment["span_id"] + assert handle_span["name"].endswith("hi") + + # Callback span (created by wrapped_callback, child of handle span) + assert callback_span["is_segment"] is False + assert callback_span["parent_span_id"] == handle_span["span_id"] + assert callback_span["name"] == "bottle" + + +@pytest.mark.parametrize( + "url,transaction_style,expected_name,expected_source", + [ + ("/message", "endpoint", "hi", "component"), + ("/message", "url", "/message", "url"), + ("/message/123456", "url", "/message/", "url"), + ("/message-named-route", "endpoint", "hi", "component"), + ], +) +def test_span_streaming_transaction_style( + sentry_init, + capture_items, + url, + transaction_style, + expected_name, + expected_source, +): + sentry_init( + integrations=[BottleIntegration(transaction_style=transaction_style)], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + items = capture_items("span") + + app = Bottle() + + @app.route("/message") + def hi(): + return "ok" + + @app.route("/message/") + def hi_with_id(message_id): + return "ok" + + @app.route("/message-named-route", name="hi") + def named_hi(): + return "ok" + + client = Client(app) + client.get(url) + + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 3 + + callback_span = spans[0] + handle_span = spans[1] + segment = spans[2] + + assert segment["is_segment"] is True + assert handle_span["parent_span_id"] == segment["span_id"] + assert callback_span["parent_span_id"] == handle_span["span_id"] + + assert handle_span["name"].endswith(expected_name) + assert handle_span["attributes"]["sentry.span.source"] == expected_source + + +def test_span_streaming_with_error(sentry_init, capture_items): + sentry_init( + integrations=[BottleIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + items = capture_items("event", "span") + + app = Bottle() + + @app.route("/error") + def error(): + 1 / 0 + + client = Client(app) + try: + client.get("/error") + except ZeroDivisionError: + pass + + sentry_sdk.flush() + + events = [item.payload for item in items if item.type == "event"] + spans = [item.payload for item in items if item.type == "span"] + assert len(events) == 1 + assert len(spans) == 3 + + error_event = events[0] + callback_span = spans[0] + handle_span = spans[1] + segment = spans[2] + + # All share the same trace + assert callback_span["trace_id"] == error_event["contexts"]["trace"]["trace_id"] + assert handle_span["trace_id"] == error_event["contexts"]["trace"]["trace_id"] + assert segment["trace_id"] == error_event["contexts"]["trace"]["trace_id"] + + # Span hierarchy + assert segment["is_segment"] is True + assert "parent_span_id" not in segment + assert handle_span["parent_span_id"] == segment["span_id"] + assert callback_span["parent_span_id"] == handle_span["span_id"] + + # Error event span_id points to the callback span (where the exception was raised) + assert error_event["contexts"]["trace"]["span_id"] == callback_span["span_id"] + + # Span statuses + assert segment["status"] == "error" + assert callback_span["status"] == "error" + + # Bottle mechanism on the error event + assert error_event["exception"]["values"][0]["mechanism"]["type"] == "bottle" + assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False + + +@pytest.mark.parametrize( + "status_code,expected_span_status", + [ + (200, "ok"), + (404, "error"), + (500, "error"), + ], +) +def test_span_streaming_http_error_status( + sentry_init, + capture_items, + status_code, + expected_span_status, +): + sentry_init( + integrations=[BottleIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + items = capture_items("span") + + app = Bottle() + + @app.route("/") + def handle(): + return HTTPResponse(status=status_code, body="response") + + client = Client(app) + client.get("/") + + sentry_sdk.flush() + + spans = [item.payload for item in items] + assert len(spans) == 3 + + callback_span = spans[0] + handle_span = spans[1] + segment = spans[2] + + assert segment["is_segment"] is True + assert handle_span["parent_span_id"] == segment["span_id"] + assert callback_span["parent_span_id"] == handle_span["span_id"] + + assert segment["status"] == expected_span_status + assert segment["attributes"]["http.response.status_code"] == status_code + + +@pytest.mark.parametrize("raise_error", [True, False]) +@pytest.mark.parametrize( + ("integration_kwargs", "status_code", "should_capture"), + ( + ({}, 500, True), + ({}, 400, False), + ({"failed_request_status_codes": set()}, 500, False), + ({"failed_request_status_codes": {404, *range(500, 600)}}, 404, True), + ({"failed_request_status_codes": {404, *range(500, 600)}}, 400, False), + ), +) +def test_span_streaming_failed_request_status_codes( + sentry_init, + capture_items, + integration_kwargs, + status_code, + should_capture, + raise_error, +): + sentry_init( + integrations=[BottleIntegration(**integration_kwargs)], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + items = capture_items("event", "span") + + app = Bottle() + + @app.route("/") + def handle(): + response = HTTPResponse(status=status_code) + if raise_error: + raise response + return response + + client = Client(app, Response) + client.get("/") + + sentry_sdk.flush() + + events = [item.payload for item in items if item.type == "event"] + spans = [item.payload for item in items if item.type == "span"] + assert len(spans) == 3 + + callback_span = spans[0] + handle_span = spans[1] + segment = spans[2] + + assert segment["is_segment"] is True + assert handle_span["parent_span_id"] == segment["span_id"] + assert callback_span["parent_span_id"] == handle_span["span_id"] + + if should_capture: + assert len(events) == 1 + assert events[0]["exception"]["values"][0]["type"] == "HTTPResponse" + assert events[0]["exception"]["values"][0]["mechanism"]["handled"] is True + else: + assert len(events) == 0 From 4d7da81c7bca11d2b4588e4a4e5436a197b5fe04 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Tue, 2 Jun 2026 15:15:59 -0400 Subject: [PATCH 2/4] Switch to using the set_transaction_name function on the current scope --- sentry_sdk/integrations/bottle.py | 51 ++++++++++----------- tests/integrations/bottle/test_bottle.py | 56 +++++++++--------------- 2 files changed, 43 insertions(+), 64 deletions(-) diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 4a51593f75..6b35475408 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -10,8 +10,8 @@ ) from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.traces import SegmentSource -from sentry_sdk.tracing import SOURCE_FOR_STYLE +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.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( capture_internal_exceptions, @@ -104,33 +104,24 @@ def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": ) if has_span_streaming_enabled(sentry_sdk.get_client().options): - # Using SegmentSource.COMPONENT as the default since this is the also value for "handler_name" - # within SOURCE_FOR_STYLE. - # See https://github.com/getsentry/sentry-python/blob/c3aab3932f5a7e89ad3aff551a206db710acf0e6/sentry_sdk/tracing.py#L151-L161 - with sentry_sdk.traces.start_span( - name="bottle", - attributes={"sentry.span.source": SegmentSource.COMPONENT}, - ) as span: - res = old_handle(self, environ) + res = old_handle(self, environ) - try: - if integration.transaction_style == "url": - span.name = bottle_request.route.rule or "bottle" - span.set_attribute("sentry.span.source", SegmentSource.URL) - elif integration.transaction_style == "endpoint": - name = ( - bottle_request.route.name - or transaction_from_function( - bottle_request.route.callback - ) - or "bottle" - ) - span.name = name - span.set_attribute( - "sentry.span.source", SegmentSource.COMPONENT - ) - except RuntimeError: - pass + try: + if integration.transaction_style == "url": + name = bottle_request.route.rule or "bottle" + else: + name = ( + bottle_request.route.name + or transaction_from_function(bottle_request.route.callback) + or "bottle" + ) + + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style], + ) + except RuntimeError: + pass else: res = old_handle(self, environ) @@ -234,7 +225,9 @@ def _set_transaction_name_and_source( pass event["transaction"] = name - event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]} + event["transaction_info"] = { + "source": TRANSACTION_SOURCE_FOR_STYLE[transaction_style] + } def _make_request_event_processor( diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index b580bcc90c..d5af2e48c4 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -593,11 +593,10 @@ def hi(): sentry_sdk.flush() spans = [item.payload for item in items] - assert len(spans) == 3 + assert len(spans) == 2 callback_span = spans[0] - handle_span = spans[1] - segment = spans[2] + segment = spans[1] # Segment span (root, created by WSGI middleware) assert segment["is_segment"] is True @@ -607,15 +606,11 @@ def hi(): assert segment["attributes"]["sentry.origin"] == "auto.http.bottle" assert segment["attributes"]["http.request.method"] == "GET" assert segment["attributes"]["http.response.status_code"] == 200 + assert segment["name"].endswith("hi") - # Handle span (created by _patched_handle, child of segment) - assert handle_span["is_segment"] is False - assert handle_span["parent_span_id"] == segment["span_id"] - assert handle_span["name"].endswith("hi") - - # Callback span (created by wrapped_callback, child of handle span) + # Callback span (created by wrapped_callback, child of segment) assert callback_span["is_segment"] is False - assert callback_span["parent_span_id"] == handle_span["span_id"] + assert callback_span["parent_span_id"] == segment["span_id"] assert callback_span["name"] == "bottle" @@ -623,8 +618,8 @@ def hi(): "url,transaction_style,expected_name,expected_source", [ ("/message", "endpoint", "hi", "component"), - ("/message", "url", "/message", "url"), - ("/message/123456", "url", "/message/", "url"), + ("/message", "url", "/message", "route"), + ("/message/123456", "url", "/message/", "route"), ("/message-named-route", "endpoint", "hi", "component"), ], ) @@ -663,18 +658,16 @@ def named_hi(): sentry_sdk.flush() spans = [item.payload for item in items] - assert len(spans) == 3 + assert len(spans) == 2 callback_span = spans[0] - handle_span = spans[1] - segment = spans[2] + segment = spans[1] assert segment["is_segment"] is True - assert handle_span["parent_span_id"] == segment["span_id"] - assert callback_span["parent_span_id"] == handle_span["span_id"] + assert callback_span["parent_span_id"] == segment["span_id"] - assert handle_span["name"].endswith(expected_name) - assert handle_span["attributes"]["sentry.span.source"] == expected_source + assert segment["name"].endswith(expected_name) + assert segment["attributes"]["sentry.span.source"] == expected_source def test_span_streaming_with_error(sentry_init, capture_items): @@ -702,23 +695,20 @@ def error(): events = [item.payload for item in items if item.type == "event"] spans = [item.payload for item in items if item.type == "span"] assert len(events) == 1 - assert len(spans) == 3 + assert len(spans) == 2 error_event = events[0] callback_span = spans[0] - handle_span = spans[1] - segment = spans[2] + segment = spans[1] # All share the same trace assert callback_span["trace_id"] == error_event["contexts"]["trace"]["trace_id"] - assert handle_span["trace_id"] == error_event["contexts"]["trace"]["trace_id"] assert segment["trace_id"] == error_event["contexts"]["trace"]["trace_id"] # Span hierarchy assert segment["is_segment"] is True assert "parent_span_id" not in segment - assert handle_span["parent_span_id"] == segment["span_id"] - assert callback_span["parent_span_id"] == handle_span["span_id"] + assert callback_span["parent_span_id"] == segment["span_id"] # Error event span_id points to the callback span (where the exception was raised) assert error_event["contexts"]["trace"]["span_id"] == callback_span["span_id"] @@ -765,15 +755,13 @@ def handle(): sentry_sdk.flush() spans = [item.payload for item in items] - assert len(spans) == 3 + assert len(spans) == 2 callback_span = spans[0] - handle_span = spans[1] - segment = spans[2] + segment = spans[1] assert segment["is_segment"] is True - assert handle_span["parent_span_id"] == segment["span_id"] - assert callback_span["parent_span_id"] == handle_span["span_id"] + assert callback_span["parent_span_id"] == segment["span_id"] assert segment["status"] == expected_span_status assert segment["attributes"]["http.response.status_code"] == status_code @@ -821,15 +809,13 @@ def handle(): events = [item.payload for item in items if item.type == "event"] spans = [item.payload for item in items if item.type == "span"] - assert len(spans) == 3 + assert len(spans) == 2 callback_span = spans[0] - handle_span = spans[1] - segment = spans[2] + segment = spans[1] assert segment["is_segment"] is True - assert handle_span["parent_span_id"] == segment["span_id"] - assert callback_span["parent_span_id"] == handle_span["span_id"] + assert callback_span["parent_span_id"] == segment["span_id"] if should_capture: assert len(events) == 1 From 0d92dd7e3a283992e41a4e4aa77566db1f9f78ad Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 3 Jun 2026 08:52:55 -0400 Subject: [PATCH 3/4] Address CR comments --- sentry_sdk/integrations/bottle.py | 76 ++++++++++-------------- tests/integrations/bottle/test_bottle.py | 44 +++++--------- 2 files changed, 47 insertions(+), 73 deletions(-) diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 6b35475408..0334343527 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -106,22 +106,9 @@ def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": if has_span_streaming_enabled(sentry_sdk.get_client().options): res = old_handle(self, environ) - try: - if integration.transaction_style == "url": - name = bottle_request.route.rule or "bottle" - else: - name = ( - bottle_request.route.name - or transaction_from_function(bottle_request.route.callback) - or "bottle" - ) - - sentry_sdk.get_current_scope().set_transaction_name( - name, - source=SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style], - ) - except RuntimeError: - pass + _set_segment_name_and_source( + transaction_style=integration.transaction_style + ) else: res = old_handle(self, environ) @@ -143,33 +130,17 @@ def patched_make_callback( return prepared_callback def wrapped_callback(*args: object, **kwargs: object) -> "Any": - if has_span_streaming_enabled(sentry_sdk.get_client().options): - with sentry_sdk.traces.start_span(name="bottle"): - try: - res = prepared_callback(*args, **kwargs) - except Exception as exception: - _capture_exception(exception, handled=False) - raise exception - - if ( - isinstance(res, HTTPResponse) - and res.status_code - in integration.failed_request_status_codes - ): - _capture_exception(res, handled=True) - - else: - try: - res = prepared_callback(*args, **kwargs) - except Exception as exception: - _capture_exception(exception, handled=False) - raise exception - - if ( - isinstance(res, HTTPResponse) - and res.status_code in integration.failed_request_status_codes - ): - _capture_exception(res, handled=True) + try: + res = prepared_callback(*args, **kwargs) + except Exception as exception: + _capture_exception(exception, handled=False) + raise exception + + if ( + isinstance(res, HTTPResponse) + and res.status_code in integration.failed_request_status_codes + ): + _capture_exception(res, handled=True) return res @@ -203,6 +174,25 @@ def size_of_file(self, file: "FileUpload") -> int: return file.content_length +def _set_segment_name_and_source(transaction_style: str) -> None: + try: + if transaction_style == "url": + name = bottle_request.route.rule or "bottle request" + else: + name = ( + bottle_request.route.name + or transaction_from_function(bottle_request.route.callback) + or "bottle request" + ) + + sentry_sdk.get_current_scope().set_transaction_name( + name, + source=SEGMENT_SOURCE_FOR_STYLE[transaction_style], + ) + except RuntimeError: + pass + + def _set_transaction_name_and_source( event: "Event", transaction_style: str, request: "Any" ) -> None: diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py index d5af2e48c4..6a36f8efd5 100644 --- a/tests/integrations/bottle/test_bottle.py +++ b/tests/integrations/bottle/test_bottle.py @@ -593,10 +593,9 @@ def hi(): sentry_sdk.flush() spans = [item.payload for item in items] - assert len(spans) == 2 + assert len(spans) == 1 - callback_span = spans[0] - segment = spans[1] + segment = spans[0] # Segment span (root, created by WSGI middleware) assert segment["is_segment"] is True @@ -608,11 +607,6 @@ def hi(): assert segment["attributes"]["http.response.status_code"] == 200 assert segment["name"].endswith("hi") - # Callback span (created by wrapped_callback, child of segment) - assert callback_span["is_segment"] is False - assert callback_span["parent_span_id"] == segment["span_id"] - assert callback_span["name"] == "bottle" - @pytest.mark.parametrize( "url,transaction_style,expected_name,expected_source", @@ -658,13 +652,11 @@ def named_hi(): sentry_sdk.flush() spans = [item.payload for item in items] - assert len(spans) == 2 + assert len(spans) == 1 - callback_span = spans[0] - segment = spans[1] + segment = spans[0] assert segment["is_segment"] is True - assert callback_span["parent_span_id"] == segment["span_id"] assert segment["name"].endswith(expected_name) assert segment["attributes"]["sentry.span.source"] == expected_source @@ -695,27 +687,23 @@ def error(): events = [item.payload for item in items if item.type == "event"] spans = [item.payload for item in items if item.type == "span"] assert len(events) == 1 - assert len(spans) == 2 + assert len(spans) == 1 error_event = events[0] - callback_span = spans[0] - segment = spans[1] + segment = spans[0] - # All share the same trace - assert callback_span["trace_id"] == error_event["contexts"]["trace"]["trace_id"] + # Confirm the same trace is shared assert segment["trace_id"] == error_event["contexts"]["trace"]["trace_id"] # Span hierarchy assert segment["is_segment"] is True assert "parent_span_id" not in segment - assert callback_span["parent_span_id"] == segment["span_id"] - # Error event span_id points to the callback span (where the exception was raised) - assert error_event["contexts"]["trace"]["span_id"] == callback_span["span_id"] + # Error event span_id points to the segment span (where the exception was raised) + assert error_event["contexts"]["trace"]["span_id"] == segment["span_id"] - # Span statuses + # Span status assert segment["status"] == "error" - assert callback_span["status"] == "error" # Bottle mechanism on the error event assert error_event["exception"]["values"][0]["mechanism"]["type"] == "bottle" @@ -755,13 +743,11 @@ def handle(): sentry_sdk.flush() spans = [item.payload for item in items] - assert len(spans) == 2 + assert len(spans) == 1 - callback_span = spans[0] - segment = spans[1] + segment = spans[0] assert segment["is_segment"] is True - assert callback_span["parent_span_id"] == segment["span_id"] assert segment["status"] == expected_span_status assert segment["attributes"]["http.response.status_code"] == status_code @@ -809,13 +795,11 @@ def handle(): events = [item.payload for item in items if item.type == "event"] spans = [item.payload for item in items if item.type == "span"] - assert len(spans) == 2 + assert len(spans) == 1 - callback_span = spans[0] - segment = spans[1] + segment = spans[0] assert segment["is_segment"] is True - assert callback_span["parent_span_id"] == segment["span_id"] if should_capture: assert len(events) == 1 From decefd3296fb7a702a243098ee27dd302f7507e7 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 3 Jun 2026 10:05:26 -0400 Subject: [PATCH 4/4] cleanup --- sentry_sdk/integrations/bottle.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 0334343527..50f6ca2e1d 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -102,17 +102,13 @@ def _patched_handle(self: "Bottle", environ: "Dict[str, Any]") -> "Any": scope.add_event_processor( _make_request_event_processor(self, bottle_request, integration) ) + res = old_handle(self, environ) if has_span_streaming_enabled(sentry_sdk.get_client().options): - res = old_handle(self, environ) - _set_segment_name_and_source( transaction_style=integration.transaction_style ) - else: - res = old_handle(self, environ) - return res Bottle._handle = _patched_handle