From 98a6a549760bdda2ad20720432c4d6a1a2f76601 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 5 May 2026 15:29:48 +0200 Subject: [PATCH 1/5] ref(tornado): Migrate integration to span-first Add span-streaming support to the Tornado integration. When span streaming is enabled, the request handler emits a StreamedSpan with HTTP request attributes (method, headers, query, URL, client address) and sets the response status on completion. The legacy transaction path is preserved for non-streaming mode. Co-Authored-By: Claude Opus 4.7 (1M context) --- sentry_sdk/integrations/tornado.py | 112 +++++++++-- tests/integrations/tornado/test_tornado.py | 206 +++++++++++++++------ 2 files changed, 240 insertions(+), 78 deletions(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 2f4630b723..58d102ed57 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.integrations._wsgi_common import ( RequestExtractor, @@ -13,7 +13,9 @@ ) from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.traces import SegmentSource, StreamedSpan from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( CONTEXTVARS_ERROR_MESSAGE, HAS_REAL_CONTEXTVARS, @@ -33,9 +35,10 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable, Dict, Generator, Optional + from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union from sentry_sdk._types import Event, EventProcessor + from sentry_sdk.tracing import Span class TornadoIntegration(Integration): @@ -97,6 +100,9 @@ def sentry_log_exception( RequestHandler.log_exception = sentry_log_exception +_DEFAULT_TRANSACTION_NAME = "generic Tornado request" + + @contextlib.contextmanager def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]": integration = sentry_sdk.get_client().get_integration(TornadoIntegration) @@ -106,6 +112,8 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] return weak_handler = weakref.ref(self) + client = sentry_sdk.get_client() + span_streaming = has_span_streaming_enabled(client.options) with sentry_sdk.isolation_scope() as scope: headers = self.request.headers @@ -114,22 +122,90 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] processor = _make_event_processor(weak_handler) scope.add_event_processor(processor) - transaction = continue_trace( - headers, - op=OP.HTTP_SERVER, - # Like with all other integrations, this is our - # fallback transaction in case there is no route. - # sentry_urldispatcher_resolve is responsible for - # setting a transaction name later. - name="generic Tornado request", - source=TransactionSource.ROUTE, - origin=TornadoIntegration.origin, - ) - - with sentry_sdk.start_transaction( - transaction, custom_sampling_context={"tornado_request": self.request} - ): - yield + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" + + if span_streaming: + sentry_sdk.traces.continue_trace(dict(headers)) + scope.set_custom_sampling_context({"tornado_request": self.request}) + + span_ctx = sentry_sdk.traces.start_span( + name=_DEFAULT_TRANSACTION_NAME, + attributes={ + "sentry.op": OP.HTTP_SERVER, + "sentry.origin": TornadoIntegration.origin, + "sentry.span.source": SegmentSource.ROUTE, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.HTTP_SERVER, + # Like with all other integrations, this is our + # fallback transaction in case there is no route. + # sentry_urldispatcher_resolve is responsible for + # setting a transaction name later. + name=_DEFAULT_TRANSACTION_NAME, + source=TransactionSource.ROUTE, + origin=TornadoIntegration.origin, + ) + span_ctx = sentry_sdk.start_transaction( + transaction, + custom_sampling_context={"tornado_request": self.request}, + ) + + with span_ctx as span: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + for attr, value in _get_request_attributes(self.request).items(): + span.set_attribute(attr, value) + + method = getattr(self, self.request.method.lower(), None) + if method is not None: + tx_name = transaction_from_function(method) or "" + if tx_name: + span.name = tx_name + span.set_attribute( + "sentry.span.source", + SegmentSource.COMPONENT.value, + ) + + try: + yield + finally: + if isinstance(span, StreamedSpan): + with capture_internal_exceptions(): + status_int = self.get_status() + span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) + span.status = "error" if status_int >= 400 else "ok" + + +def _get_request_attributes(request: "Any") -> "Dict[str, Any]": + attributes = {} # type: Dict[str, Any] + + if request.method: + attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper() + + headers = _filter_headers(dict(request.headers), use_annotated_value=False) + for header, value in headers.items(): + attributes[f"http.request.header.{header.lower()}"] = value + + if request.query: + attributes[SPANDATA.HTTP_QUERY] = request.query + + attributes[SPANDATA.URL_FULL] = "%s://%s%s" % ( + request.protocol, + request.host, + request.path, + ) + + if request.protocol: + attributes["network.protocol.name"] = request.protocol + + if should_send_default_pii() and request.remote_ip: + attributes["client.address"] = request.remote_ip + attributes["user.ip_address"] = request.remote_ip + + return attributes @ensure_integration_enabled(TornadoIntegration) diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index 397342458b..8228a98dee 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -103,6 +103,7 @@ def test_basic(tornado_testcase, sentry_init, capture_events): assert not sentry_sdk.get_isolation_scope()._tags +@pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize( "handler,code", [ @@ -110,79 +111,144 @@ def test_basic(tornado_testcase, sentry_init, capture_events): (HelloHandler, 200), ], ) -def test_transactions(tornado_testcase, sentry_init, capture_events, handler, code): - sentry_init(integrations=[TornadoIntegration()], traces_sample_rate=1.0) - events = capture_events() +def test_transactions( + tornado_testcase, + sentry_init, + capture_events, + capture_items, + handler, + code, + span_streaming, +): + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + + if span_streaming: + items = capture_items("event", "span") + else: + events = capture_events() + client = tornado_testcase(Application([(r"/hi", handler)])) - with start_transaction(name="client") as span: - pass + if span_streaming: + with sentry_sdk.traces.start_span(name="client") as span: + request_headers = dict(span._iter_headers()) + else: + with start_transaction(name="client") as span: + pass + request_headers = dict(span.iter_headers()) response = client.fetch( - "/hi", method="POST", body=b"heyoo", headers=dict(span.iter_headers()) + "/hi", method="POST", body=b"heyoo", headers=request_headers ) assert response.code == code - if code == 200: - client_tx, server_tx = events - server_error = None - else: - client_tx, server_error, server_tx = events + sentry_sdk.flush() - assert client_tx["type"] == "transaction" - assert client_tx["transaction"] == "client" - assert client_tx["transaction_info"] == { - "source": "custom" - } # because this is just the start_transaction() above. + if span_streaming: + spans = [i.payload for i in items if i.type == "span"] + errors = [i.payload for i in items if i.type == "event"] - if server_error is not None: - assert server_error["exception"]["values"][0]["type"] == "ZeroDivisionError" - assert ( - server_error["transaction"] - == "tests.integrations.tornado.test_tornado.CrashingHandler.post" + # client tx + server segment span + assert len(spans) >= 2 + server_segment = next( + s for s in spans if s["attributes"].get("sentry.op") == "http.server" + ) + client_segment = next( + s + for s in spans + if s["attributes"].get("sentry.op") != "http.server" and s.get("is_segment") ) - assert server_error["transaction_info"] == {"source": "component"} - if code == 200: - assert ( - server_tx["transaction"] - == "tests.integrations.tornado.test_tornado.HelloHandler.post" + if code == 500: + assert len(errors) == 1 + server_error = errors[0] + assert server_error["exception"]["values"][0]["type"] == "ZeroDivisionError" + assert ( + server_error["transaction"] + == "tests.integrations.tornado.test_tornado.CrashingHandler.post" + ) + assert server_error["transaction_info"] == {"source": "component"} + assert ( + server_error["contexts"]["trace"]["trace_id"] + == server_segment["trace_id"] + ) + + expected_handler = ( + "tests.integrations.tornado.test_tornado.HelloHandler.post" + if code == 200 + else "tests.integrations.tornado.test_tornado.CrashingHandler.post" ) + assert server_segment["name"] == expected_handler + assert server_segment["attributes"]["sentry.span.source"] == "component" + assert server_segment["attributes"]["http.request.method"] == "POST" + assert server_segment["attributes"]["http.response.status_code"] == code + assert server_segment["status"] == ("ok" if code == 200 else "error") + assert client_segment["trace_id"] == server_segment["trace_id"] else: - assert ( - server_tx["transaction"] - == "tests.integrations.tornado.test_tornado.CrashingHandler.post" - ) - - assert server_tx["transaction_info"] == {"source": "component"} - assert server_tx["type"] == "transaction" - - request = server_tx["request"] - host = request["headers"]["Host"] - assert server_tx["request"] == { - "env": {"REMOTE_ADDR": "127.0.0.1"}, - "headers": { - "Accept-Encoding": "gzip", - "Connection": "close", - **request["headers"], - }, - "method": "POST", - "query_string": "", - "data": {"heyoo": [""]}, - "url": "http://{host}/hi".format(host=host), - } + if code == 200: + client_tx, server_tx = events + server_error = None + else: + client_tx, server_error, server_tx = events + + assert client_tx["type"] == "transaction" + assert client_tx["transaction"] == "client" + assert client_tx["transaction_info"] == { + "source": "custom" + } # because this is just the start_transaction() above. + + if server_error is not None: + assert server_error["exception"]["values"][0]["type"] == "ZeroDivisionError" + assert ( + server_error["transaction"] + == "tests.integrations.tornado.test_tornado.CrashingHandler.post" + ) + assert server_error["transaction_info"] == {"source": "component"} + + if code == 200: + assert ( + server_tx["transaction"] + == "tests.integrations.tornado.test_tornado.HelloHandler.post" + ) + else: + assert ( + server_tx["transaction"] + == "tests.integrations.tornado.test_tornado.CrashingHandler.post" + ) + + assert server_tx["transaction_info"] == {"source": "component"} + assert server_tx["type"] == "transaction" + + request = server_tx["request"] + host = request["headers"]["Host"] + assert server_tx["request"] == { + "env": {"REMOTE_ADDR": "127.0.0.1"}, + "headers": { + "Accept-Encoding": "gzip", + "Connection": "close", + **request["headers"], + }, + "method": "POST", + "query_string": "", + "data": {"heyoo": [""]}, + "url": "http://{host}/hi".format(host=host), + } - assert ( - client_tx["contexts"]["trace"]["trace_id"] - == server_tx["contexts"]["trace"]["trace_id"] - ) - - if server_error is not None: assert ( - server_error["contexts"]["trace"]["trace_id"] + client_tx["contexts"]["trace"]["trace_id"] == server_tx["contexts"]["trace"]["trace_id"] ) + if server_error is not None: + assert ( + server_error["contexts"]["trace"]["trace_id"] + == server_tx["contexts"]["trace"]["trace_id"] + ) + def test_400_not_logged(tornado_testcase, sentry_init, capture_events): sentry_init(integrations=[TornadoIntegration()]) @@ -437,15 +503,35 @@ def test_error_has_existing_trace_context_performance_disabled( ) -def test_span_origin(tornado_testcase, sentry_init, capture_events): - sentry_init(integrations=[TornadoIntegration()], traces_sample_rate=1.0) - events = capture_events() +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_span_origin( + tornado_testcase, sentry_init, capture_events, capture_items, span_streaming +): + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() + client = tornado_testcase(Application([(r"/hi", CrashingHandler)])) client.fetch( "/hi?foo=bar", headers={"Cookie": "name=value; name2=value2; name3=value3"} ) - (_, event) = events + sentry_sdk.flush() - assert event["contexts"]["trace"]["origin"] == "auto.http.tornado" + if span_streaming: + spans = [i.payload for i in items] + segment = next( + s for s in spans if s["attributes"].get("sentry.op") == "http.server" + ) + assert segment["attributes"]["sentry.origin"] == "auto.http.tornado" + else: + (_, event) = events + assert event["contexts"]["trace"]["origin"] == "auto.http.tornado" From 9146357082d4c98bff2f5dbfb2ceea24c2176eed Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 7 May 2026 15:17:21 +0200 Subject: [PATCH 2/5] address PR comments --- sentry_sdk/integrations/tornado.py | 13 +++++++------ tests/integrations/tornado/test_tornado.py | 16 ++-------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 58d102ed57..026474988d 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -113,7 +113,7 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] weak_handler = weakref.ref(self) client = sentry_sdk.get_client() - span_streaming = has_span_streaming_enabled(client.options) + is_span_streaming_enabled = has_span_streaming_enabled(client.options) with sentry_sdk.isolation_scope() as scope: headers = self.request.headers @@ -124,7 +124,7 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" - if span_streaming: + if is_span_streaming_enabled: sentry_sdk.traces.continue_trace(dict(headers)) scope.set_custom_sampling_context({"tornado_request": self.request}) @@ -135,6 +135,7 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] "sentry.origin": TornadoIntegration.origin, "sentry.span.source": SegmentSource.ROUTE, }, + parent_span=None, ) else: transaction = continue_trace( @@ -161,12 +162,12 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] method = getattr(self, self.request.method.lower(), None) if method is not None: - tx_name = transaction_from_function(method) or "" - if tx_name: - span.name = tx_name + span_name = transaction_from_function(method) or "" + if span_name: + span.name = span_name span.set_attribute( "sentry.span.source", - SegmentSource.COMPONENT.value, + SegmentSource.COMPONENT, ) try: diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index 8228a98dee..840b5c5d4e 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -152,16 +152,7 @@ def test_transactions( spans = [i.payload for i in items if i.type == "span"] errors = [i.payload for i in items if i.type == "event"] - # client tx + server segment span - assert len(spans) >= 2 - server_segment = next( - s for s in spans if s["attributes"].get("sentry.op") == "http.server" - ) - client_segment = next( - s - for s in spans - if s["attributes"].get("sentry.op") != "http.server" and s.get("is_segment") - ) + client_segment, server_segment = spans if code == 500: assert len(errors) == 1 @@ -527,10 +518,7 @@ def test_span_origin( sentry_sdk.flush() if span_streaming: - spans = [i.payload for i in items] - segment = next( - s for s in spans if s["attributes"].get("sentry.op") == "http.server" - ) + (segment,) = [i.payload for i in items] assert segment["attributes"]["sentry.origin"] == "auto.http.tornado" else: (_, event) = events From 51f05cb25018f6ba1d1db24c85352ca963865425 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 15 May 2026 14:27:57 +0200 Subject: [PATCH 3/5] req body --- sentry_sdk/consts.py | 31 ++++++++++++++++ sentry_sdk/integrations/tornado.py | 41 +++++++++++++++++----- tests/integrations/tornado/test_tornado.py | 1 + 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 008256e110..b544701b6f 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -404,6 +404,12 @@ class SPANDATA: Example: template.cache.some_item.867da7e2af8e6b2f3aa7213a4080edb3 """ + CLIENT_ADDRESS = "client.address" + """ + Client address of the network connection - IP address or Unix domain socket name. + Example: "10.1.2.80" + """ + CODE_FILEPATH = "code.filepath" """ .. deprecated:: @@ -819,12 +825,25 @@ class SPANDATA: Example: GET """ + HTTP_REQUEST_HEADER = "http.request.header" + """ + Prefix for HTTP request header attributes. The header name (lowercased) is + appended to form the full attribute key. + Example: "http.request.header.content-type" + """ + HTTP_REQUEST_METHOD = "http.request.method" """ The HTTP method used. Example: GET """ + HTTP_REQUEST_BODY_DATA = "http.request.body.data" + """ + The HTTP request body data as string. + Example: "[{\"role\": \"user\", \"message\": \"hello\"}]" + """ + HTTP_QUERY = "http.query" """ The Query string present in the URL. @@ -863,6 +882,12 @@ class SPANDATA: The messaging system's name, e.g. `kafka`, `aws_sqs` """ + NETWORK_PROTOCOL_NAME = "network.protocol.name" + """ + The application layer protocol name used for the network connection. + Example: "http", "https" + """ + NETWORK_PEER_ADDRESS = "network.peer.address" """ Peer address of the network connection - IP address or Unix domain socket name. @@ -942,6 +967,12 @@ class SPANDATA: Example: "MainThread" """ + USER_IP_ADDRESS = "user.ip_address" + """ + The IP address of the user that triggered the request. + Example: "10.1.2.80" + """ + URL_FULL = "url.full" """ The URL of the resource that was fetched. diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 026474988d..0a3e6906c2 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -10,15 +10,17 @@ RequestExtractor, _filter_headers, _is_json_content_type, + request_body_within_bounds, ) from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import SegmentSource, StreamedSpan +from sentry_sdk.traces import NoOpStreamedSpan, SegmentSource, StreamedSpan from sentry_sdk.tracing import TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( CONTEXTVARS_ERROR_MESSAGE, HAS_REAL_CONTEXTVARS, + AnnotatedValue, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, @@ -155,14 +157,16 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] ) with span_ctx as span: - if isinstance(span, StreamedSpan): + if isinstance(span, StreamedSpan) and not isinstance( + span, NoOpStreamedSpan + ): with capture_internal_exceptions(): for attr, value in _get_request_attributes(self.request).items(): span.set_attribute(attr, value) method = getattr(self, self.request.method.lower(), None) if method is not None: - span_name = transaction_from_function(method) or "" + span_name = transaction_from_function(method) if span_name: span.name = span_name span.set_attribute( @@ -173,7 +177,9 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] try: yield finally: - if isinstance(span, StreamedSpan): + if isinstance(span, StreamedSpan) and not isinstance( + span, NoOpStreamedSpan + ): with capture_internal_exceptions(): status_int = self.get_status() span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) @@ -188,7 +194,7 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": headers = _filter_headers(dict(request.headers), use_annotated_value=False) for header, value in headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value if request.query: attributes[SPANDATA.HTTP_QUERY] = request.query @@ -200,15 +206,34 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": ) if request.protocol: - attributes["network.protocol.name"] = request.protocol + attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol if should_send_default_pii() and request.remote_ip: - attributes["client.address"] = request.remote_ip - attributes["user.ip_address"] = request.remote_ip + attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip + attributes[SPANDATA.USER_IP_ADDRESS] = request.remote_ip + + with capture_internal_exceptions(): + raw_data = _get_tornado_request_data(request) + body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data + if body_data is not None: + attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data return attributes +def _get_tornado_request_data( + request: "Any", +) -> "Union[Optional[str], AnnotatedValue]": + body = request.body + if not body: + return None + + if not request_body_within_bounds(sentry_sdk.get_client(), len(body)): + return AnnotatedValue.substituted_because_over_size_limit() + + return body.decode("utf-8", "replace") + + @ensure_integration_enabled(TornadoIntegration) def _capture_exception(ty: type, value: BaseException, tb: "Any") -> None: if isinstance(value, HTTPError): diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index 840b5c5d4e..a445304d1c 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -176,6 +176,7 @@ def test_transactions( assert server_segment["name"] == expected_handler assert server_segment["attributes"]["sentry.span.source"] == "component" assert server_segment["attributes"]["http.request.method"] == "POST" + assert server_segment["attributes"]["http.request.body.data"] == "heyoo" assert server_segment["attributes"]["http.response.status_code"] == code assert server_segment["status"] == ("ok" if code == 200 else "error") assert client_segment["trace_id"] == server_segment["trace_id"] From 1684e7f70ff64bda3932e46394a08ac8caa1a677 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 15 May 2026 15:20:59 +0200 Subject: [PATCH 4/5] move request attrs to finally --- sentry_sdk/integrations/tornado.py | 32 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 0a3e6906c2..f9d7cb963a 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -157,23 +157,6 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] ) with span_ctx as span: - if isinstance(span, StreamedSpan) and not isinstance( - span, NoOpStreamedSpan - ): - with capture_internal_exceptions(): - for attr, value in _get_request_attributes(self.request).items(): - span.set_attribute(attr, value) - - method = getattr(self, self.request.method.lower(), None) - if method is not None: - span_name = transaction_from_function(method) - if span_name: - span.name = span_name - span.set_attribute( - "sentry.span.source", - SegmentSource.COMPONENT, - ) - try: yield finally: @@ -181,6 +164,21 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] span, NoOpStreamedSpan ): with capture_internal_exceptions(): + for attr, value in _get_request_attributes( + self.request + ).items(): + span.set_attribute(attr, value) + + method = getattr(self, self.request.method.lower(), None) + if method is not None: + span_name = transaction_from_function(method) + if span_name: + span.name = span_name + span.set_attribute( + "sentry.span.source", + SegmentSource.COMPONENT, + ) + status_int = self.get_status() span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) span.status = "error" if status_int >= 400 else "ok" From 4c597a40fbe3f827b17cef2bf7e00a95fceedada Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 15 May 2026 15:51:49 +0200 Subject: [PATCH 5/5] bot comments --- sentry_sdk/integrations/tornado.py | 10 ++++------ tests/integrations/tornado/test_tornado.py | 7 +++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index f9d7cb963a..db0d68c443 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -169,6 +169,7 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] ).items(): span.set_attribute(attr, value) + with capture_internal_exceptions(): method = getattr(self, self.request.method.lower(), None) if method is not None: span_name = transaction_from_function(method) @@ -179,6 +180,7 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] SegmentSource.COMPONENT, ) + with capture_internal_exceptions(): status_int = self.get_status() span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int) span.status = "error" if status_int >= 400 else "ok" @@ -195,13 +197,9 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value if request.query: - attributes[SPANDATA.HTTP_QUERY] = request.query + attributes[SPANDATA.URL_QUERY] = request.query - attributes[SPANDATA.URL_FULL] = "%s://%s%s" % ( - request.protocol, - request.host, - request.path, - ) + attributes[SPANDATA.URL_FULL] = request.full_url() if request.protocol: attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index a445304d1c..d72421b8d1 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -142,7 +142,7 @@ def test_transactions( request_headers = dict(span.iter_headers()) response = client.fetch( - "/hi", method="POST", body=b"heyoo", headers=request_headers + "/hi?foo=bar", method="POST", body=b"heyoo", headers=request_headers ) assert response.code == code @@ -178,6 +178,9 @@ def test_transactions( assert server_segment["attributes"]["http.request.method"] == "POST" assert server_segment["attributes"]["http.request.body.data"] == "heyoo" assert server_segment["attributes"]["http.response.status_code"] == code + assert server_segment["attributes"]["url.query"] == "foo=bar" + assert server_segment["attributes"]["url.full"].endswith("/hi?foo=bar") + assert server_segment["attributes"]["url.full"].startswith("http://") assert server_segment["status"] == ("ok" if code == 200 else "error") assert client_segment["trace_id"] == server_segment["trace_id"] else: @@ -225,7 +228,7 @@ def test_transactions( **request["headers"], }, "method": "POST", - "query_string": "", + "query_string": "foo=bar", "data": {"heyoo": [""]}, "url": "http://{host}/hi".format(host=host), }