From 72077ea604aad1ecde789761079b46b36bc2d0da Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 15:21:37 +0200 Subject: [PATCH] feat(aiohttp,sanic,tornado): Add `user_agent.original` attr for inbound filters --- sentry_sdk/integrations/aiohttp.py | 4 +++ sentry_sdk/integrations/sanic.py | 4 +++ sentry_sdk/integrations/tornado.py | 4 +++ tests/integrations/aiohttp/test_aiohttp.py | 38 ++++++++++++++++++++++ tests/integrations/sanic/test_sanic.py | 34 +++++++++++++++++++ tests/integrations/tornado/test_tornado.py | 28 ++++++++++++++++ 6 files changed, 112 insertions(+) diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 1ae4d47eb2..2b43459132 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -158,6 +158,10 @@ async def sentry_app_handle( ), ) + user_agent = request.headers.get("User-Agent") + if user_agent: + scope.set_attribute(SPANDATA.USER_AGENT_ORIGINAL, user_agent) + headers = dict(request.headers) span_ctx: "ContextManager[Union[Span, StreamedSpan]]" diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 86534e49d3..ee05e7cb5b 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -193,6 +193,10 @@ async def _context_enter(request: "Request") -> None: ), ) + user_agent = request.headers.get("user-agent") + if user_agent: + scope.set_attribute(SPANDATA.USER_AGENT_ORIGINAL, user_agent) + if is_span_streaming_enabled: integration = client.get_integration(SanicIntegration) if ( diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 2080a60779..c1728f9a39 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -137,6 +137,10 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] ), ) + user_agent = headers.get("User-Agent") + if user_agent: + scope.set_attribute(SPANDATA.USER_AGENT_ORIGINAL, user_agent) + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" if is_span_streaming_enabled: diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index b4301ad628..df8266da21 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -2472,6 +2472,44 @@ async def hello(request): assert child_spans[0]["attributes"][SPANDATA.SENTRY_IS_LOCALHOST] == is_localhost +@pytest.mark.asyncio +async def test_user_agent_attribute(sentry_init, aiohttp_client, capture_items): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + + async def hello(request): + with sentry_sdk.traces.start_span(name="child"): + pass + return web.Response(text="hello") + + app = web.Application() + app.router.add_get("/", hello) + + items = capture_items("span") + + client = await aiohttp_client(app) + resp = await client.get("/", headers={"User-Agent": "TestBrowser/1.0"}) + assert resp.status == 200 + + sentry_sdk.flush() + + child_spans = [item.payload for item in items if not item.payload.get("is_segment")] + server_spans = [item.payload for item in items if item.payload.get("is_segment")] + + assert len(server_spans) == 1 + assert len(child_spans) == 1 + + assert ( + server_spans[0]["attributes"][SPANDATA.USER_AGENT_ORIGINAL] == "TestBrowser/1.0" + ) + assert ( + child_spans[0]["attributes"][SPANDATA.USER_AGENT_ORIGINAL] == "TestBrowser/1.0" + ) + + @pytest.mark.asyncio @pytest.mark.parametrize( "init_kwargs, expect_remote_addr", DATA_COLLECTION_REMOTE_ADDR_CASES diff --git a/tests/integrations/sanic/test_sanic.py b/tests/integrations/sanic/test_sanic.py index 1272ac8404..8ab53b517e 100644 --- a/tests/integrations/sanic/test_sanic.py +++ b/tests/integrations/sanic/test_sanic.py @@ -943,3 +943,37 @@ def child_span_handler_localhost(request): assert server_span["attributes"]["sentry.is_localhost"] is is_localhost assert child_span["attributes"]["sentry.is_localhost"] is is_localhost + + +@pytest.mark.skipif( + not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version" +) +def test_user_agent_attribute(sentry_init, app, capture_items): + @app.route("/child-span") + def child_span_handler_ua(request): + with sentry_sdk.traces.start_span(name="child-span"): + pass + return response.text("ok") + + sentry_init( + integrations=[SanicIntegration()], + default_integrations=False, + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + + items = capture_items("span") + + c = get_client(app) + with c as client: + client.get( + "/child-span", + headers={"User-Agent": "TestBrowser/1.0"}, + ) + + sentry_sdk.flush() + + child_span, server_span = [item.payload for item in items] + + assert server_span["attributes"]["user_agent.original"] == "TestBrowser/1.0" + assert child_span["attributes"]["user_agent.original"] == "TestBrowser/1.0" diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index 561b1b22bb..3717f15d2b 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -1165,6 +1165,34 @@ def test_is_localhost_span_attribute( assert child_spans[0]["attributes"][SPANDATA.SENTRY_IS_LOCALHOST] == is_localhost +def test_user_agent_attribute(tornado_testcase, sentry_init, capture_items): + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + + items = capture_items("span") + + client = tornado_testcase(Application([(r"/hi", ChildSpanHandler)])) + client.fetch("/hi", headers={"User-Agent": "TestBrowser/1.0"}) + + sentry_sdk.flush() + + child_spans = [item.payload for item in items if not item.payload.get("is_segment")] + server_spans = [item.payload for item in items if item.payload.get("is_segment")] + + assert len(server_spans) == 1 + assert len(child_spans) == 1 + + assert ( + server_spans[0]["attributes"][SPANDATA.USER_AGENT_ORIGINAL] == "TestBrowser/1.0" + ) + assert ( + child_spans[0]["attributes"][SPANDATA.USER_AGENT_ORIGINAL] == "TestBrowser/1.0" + ) + + @pytest.mark.parametrize( "init_kwargs, expect_remote_addr", DATA_COLLECTION_REMOTE_ADDR_CASES )