From e61cadb18691d66b3ff56216d6084acd66f67fe8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 9 Oct 2025 13:58:52 +0200 Subject: [PATCH 1/5] feat(httpx): Add source information for slow outgoing HTTP requests --- sentry_sdk/integrations/httpx.py | 19 +- tests/integrations/httpx/__init__.py | 6 + .../httpx/httpx_helpers/__init__.py | 0 .../httpx/httpx_helpers/helpers.py | 6 + tests/integrations/httpx/test_httpx.py | 309 ++++++++++++++++++ 5 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 tests/integrations/httpx/httpx_helpers/__init__.py create mode 100644 tests/integrations/httpx/httpx_helpers/helpers.py diff --git a/sentry_sdk/integrations/httpx.py b/sentry_sdk/integrations/httpx.py index 2ddd44489f..9b9a6a2a89 100644 --- a/sentry_sdk/integrations/httpx.py +++ b/sentry_sdk/integrations/httpx.py @@ -1,8 +1,13 @@ import sentry_sdk +from sentry_sdk import start_span from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.tracing import BAGGAGE_HEADER_NAME -from sentry_sdk.tracing_utils import Baggage, should_propagate_trace +from sentry_sdk.tracing_utils import ( + Baggage, + should_propagate_trace, + add_http_request_source, +) from sentry_sdk.utils import ( SENSITIVE_DATA_SUBSTITUTE, capture_internal_exceptions, @@ -52,7 +57,7 @@ def send(self, request, **kwargs): with capture_internal_exceptions(): parsed_url = parse_url(str(request.url), sanitize=False) - with sentry_sdk.start_span( + with start_span( op=OP.HTTP_CLIENT, name="%s %s" % ( @@ -88,7 +93,10 @@ def send(self, request, **kwargs): span.set_http_status(rv.status_code) span.set_data("reason", rv.reason_phrase) - return rv + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv Client.send = send @@ -144,7 +152,10 @@ async def send(self, request, **kwargs): span.set_http_status(rv.status_code) span.set_data("reason", rv.reason_phrase) - return rv + with capture_internal_exceptions(): + add_http_request_source(span) + + return rv AsyncClient.send = send diff --git a/tests/integrations/httpx/__init__.py b/tests/integrations/httpx/__init__.py index 1afd90ea3a..e524321b8b 100644 --- a/tests/integrations/httpx/__init__.py +++ b/tests/integrations/httpx/__init__.py @@ -1,3 +1,9 @@ +import os +import sys import pytest pytest.importorskip("httpx") + +# Load `httpx_helpers` into the module search path to test request source path names relative to module. See +# `test_request_source_with_module_in_search_path` +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) diff --git a/tests/integrations/httpx/httpx_helpers/__init__.py b/tests/integrations/httpx/httpx_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/httpx/httpx_helpers/helpers.py b/tests/integrations/httpx/httpx_helpers/helpers.py new file mode 100644 index 0000000000..f1d4f3c98b --- /dev/null +++ b/tests/integrations/httpx/httpx_helpers/helpers.py @@ -0,0 +1,6 @@ +def get_request_with_client(client, url): + client.get(url) + + +async def async_get_request_with_client(client, url): + await client.get(url) diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 4fd5275fb7..1e5e26f027 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -1,8 +1,11 @@ +import os +import datetime import asyncio from unittest import mock import httpx import pytest +from contextlib import contextmanager import sentry_sdk from sentry_sdk import capture_message, start_transaction @@ -393,6 +396,312 @@ def test_omit_url_data_if_parsing_fails(sentry_init, capture_events, httpx_mock) assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"] +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_request_source_disabled(sentry_init, capture_events, httpx_client, httpx_mock): + httpx_mock.add_response() + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=False, + http_request_source_threshold_ms=0, + ) + + events = capture_events() + + url = "http://example.com/" + + with start_transaction(name="test_transaction"): + if asyncio.iscoroutinefunction(httpx_client.get): + asyncio.get_event_loop().run_until_complete(httpx_client.get(url)) + else: + httpx_client.get(url) + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.parametrize("enable_http_request_source", [None, True]) +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_request_source_enabled( + sentry_init, capture_events, enable_http_request_source, httpx_client, httpx_mock +): + httpx_mock.add_response() + sentry_options = { + "integrations": [HttpxIntegration()], + "traces_sample_rate": 1.0, + "http_request_source_threshold_ms": 0, + } + if enable_http_request_source is not None: + sentry_options["enable_http_request_source"] = enable_http_request_source + + sentry_init(**sentry_options) + + events = capture_events() + + url = "http://example.com/" + + with start_transaction(name="test_transaction"): + if asyncio.iscoroutinefunction(httpx_client.get): + asyncio.get_event_loop().run_until_complete(httpx_client.get(url)) + else: + httpx_client.get(url) + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_request_source(sentry_init, capture_events, httpx_client, httpx_mock): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=True, + http_request_source_threshold_ms=0, + ) + + events = capture_events() + + url = "http://example.com/" + + with start_transaction(name="test_transaction"): + if asyncio.iscoroutinefunction(httpx_client.get): + asyncio.get_event_loop().run_until_complete(httpx_client.get(url)) + else: + httpx_client.get(url) + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.httpx.test_httpx" + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/httpx/test_httpx.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "test_request_source" + + +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_request_source_with_module_in_search_path( + sentry_init, capture_events, httpx_client, httpx_mock +): + """ + Test that request source is relative to the path of the module it ran in + """ + httpx_mock.add_response() + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=True, + http_request_source_threshold_ms=0, + ) + + events = capture_events() + + url = "http://example.com/" + + with start_transaction(name="test_transaction"): + if asyncio.iscoroutinefunction(httpx_client.get): + from httpx_helpers.helpers import async_get_request_with_client + + asyncio.get_event_loop().run_until_complete( + async_get_request_with_client(httpx_client, url) + ) + else: + from httpx_helpers.helpers import get_request_with_client + + get_request_with_client(httpx_client, url) + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert data.get(SPANDATA.CODE_NAMESPACE) == "httpx_helpers.helpers" + assert data.get(SPANDATA.CODE_FILEPATH) == "httpx_helpers/helpers.py" + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + if asyncio.iscoroutinefunction(httpx_client.get): + assert data.get(SPANDATA.CODE_FUNCTION) == "async_get_request_with_client" + else: + assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_client" + + +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_no_request_source_if_duration_too_short( + sentry_init, capture_events, httpx_client, httpx_mock +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=True, + http_request_source_threshold_ms=100, + ) + + events = capture_events() + + url = "http://example.com/" + + with start_transaction(name="test_transaction"): + + @contextmanager + def fake_start_span(*args, **kwargs): + with sentry_sdk.start_span(*args, **kwargs) as span: + pass + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) + yield span + + with mock.patch( + "sentry_sdk.integrations.httpx.start_span", + fake_start_span, + ): + if asyncio.iscoroutinefunction(httpx_client.get): + asyncio.get_event_loop().run_until_complete(httpx_client.get(url)) + else: + httpx_client.get(url) + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.parametrize( + "httpx_client", + (httpx.Client(), httpx.AsyncClient()), +) +def test_request_source_if_duration_over_threshold( + sentry_init, capture_events, httpx_client, httpx_mock +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + enable_db_query_source=True, + db_query_source_threshold_ms=100, + ) + + events = capture_events() + + url = "http://example.com/" + + with start_transaction(name="test_transaction"): + + @contextmanager + def fake_start_span(*args, **kwargs): + with sentry_sdk.start_span(*args, **kwargs) as span: + pass + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) + yield span + + with mock.patch( + "sentry_sdk.integrations.httpx.start_span", + fake_start_span, + ): + if asyncio.iscoroutinefunction(httpx_client.get): + asyncio.get_event_loop().run_until_complete(httpx_client.get(url)) + else: + httpx_client.get(url) + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.httpx.test_httpx" + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/httpx/test_httpx.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert ( + data.get(SPANDATA.CODE_FUNCTION) + == "test_query_source_if_duration_over_threshold" + ) + + @pytest.mark.parametrize( "httpx_client", (httpx.Client(), httpx.AsyncClient()), From fdf447f373a4d0209c6ed5fd151cd53acf34c663 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 9 Oct 2025 14:10:19 +0200 Subject: [PATCH 2/5] fix test --- tests/integrations/httpx/test_httpx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 1e5e26f027..f2472f1ae6 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -698,7 +698,7 @@ def fake_start_span(*args, **kwargs): assert ( data.get(SPANDATA.CODE_FUNCTION) - == "test_query_source_if_duration_over_threshold" + == "test_request_source_if_duration_over_threshold" ) From aa14164b37addeac699287178e5d8e5c76254176 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 9 Oct 2025 14:19:33 +0200 Subject: [PATCH 3/5] fix test --- sentry_sdk/integrations/httpx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/httpx.py b/sentry_sdk/integrations/httpx.py index 9b9a6a2a89..2ada95aad0 100644 --- a/sentry_sdk/integrations/httpx.py +++ b/sentry_sdk/integrations/httpx.py @@ -114,7 +114,7 @@ async def send(self, request, **kwargs): with capture_internal_exceptions(): parsed_url = parse_url(str(request.url), sanitize=False) - with sentry_sdk.start_span( + with start_span( op=OP.HTTP_CLIENT, name="%s %s" % ( From 2f09638e01350428d0e21c980f16759527d2abd7 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Thu, 9 Oct 2025 14:38:29 +0200 Subject: [PATCH 4/5] fix init arguments in test --- tests/integrations/httpx/test_httpx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index f2472f1ae6..299cca492c 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -647,8 +647,8 @@ def test_request_source_if_duration_over_threshold( sentry_init( integrations=[HttpxIntegration()], traces_sample_rate=1.0, - enable_db_query_source=True, - db_query_source_threshold_ms=100, + enable_http_request_source=True, + http_request_source_threshold_ms=100, ) events = capture_events() From 14398e09156a5337a27285df76681121733d4c01 Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Fri, 10 Oct 2025 12:54:45 +0200 Subject: [PATCH 5/5] feat(aiohttp): Add source information for slow outgoing HTTP requests (#4905) Add code source as described in https://github.com/getsentry/sentry-docs/pull/15161. Uses functionality from SQL query source and tests that it works in the HTTP request setting. --- sentry_sdk/integrations/aiohttp.py | 4 +- tests/integrations/aiohttp/__init__.py | 6 + .../aiohttp/aiohttp_helpers/__init__.py | 0 .../aiohttp/aiohttp_helpers/helpers.py | 2 + tests/integrations/aiohttp/test_aiohttp.py | 346 +++++++++++++++++- 5 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 tests/integrations/aiohttp/aiohttp_helpers/__init__.py create mode 100644 tests/integrations/aiohttp/aiohttp_helpers/helpers.py diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index ad3202bf2c..c5045e9bb1 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -22,7 +22,7 @@ SOURCE_FOR_STYLE, TransactionSource, ) -from sentry_sdk.tracing_utils import should_propagate_trace +from sentry_sdk.tracing_utils import should_propagate_trace, add_http_request_source from sentry_sdk.utils import ( capture_internal_exceptions, ensure_integration_enabled, @@ -279,6 +279,8 @@ async def on_request_end(session, trace_config_ctx, params): span.set_data("reason", params.response.reason) span.finish() + add_http_request_source(span) + trace_config = TraceConfig() trace_config.on_request_start.append(on_request_start) diff --git a/tests/integrations/aiohttp/__init__.py b/tests/integrations/aiohttp/__init__.py index 0e1409fda0..a585c11e34 100644 --- a/tests/integrations/aiohttp/__init__.py +++ b/tests/integrations/aiohttp/__init__.py @@ -1,3 +1,9 @@ +import os +import sys import pytest pytest.importorskip("aiohttp") + +# Load `aiohttp_helpers` into the module search path to test request source path names relative to module. See +# `test_request_source_with_module_in_search_path` +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) diff --git a/tests/integrations/aiohttp/aiohttp_helpers/__init__.py b/tests/integrations/aiohttp/aiohttp_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/aiohttp/aiohttp_helpers/helpers.py b/tests/integrations/aiohttp/aiohttp_helpers/helpers.py new file mode 100644 index 0000000000..86a6fa39e3 --- /dev/null +++ b/tests/integrations/aiohttp/aiohttp_helpers/helpers.py @@ -0,0 +1,2 @@ +async def get_request_with_client(client, url): + await client.get(url) diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 267ce08fdd..57f7e46abe 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1,3 +1,5 @@ +import os +import datetime import asyncio import json @@ -18,7 +20,8 @@ ) from sentry_sdk import capture_message, start_transaction -from sentry_sdk.integrations.aiohttp import AioHttpIntegration +from sentry_sdk.integrations.aiohttp import AioHttpIntegration, create_trace_config +from sentry_sdk.consts import SPANDATA from tests.conftest import ApproxDict @@ -633,6 +636,347 @@ async def handler(request): ) +@pytest.mark.asyncio +async def test_request_source_disabled( + sentry_init, aiohttp_raw_server, aiohttp_client, capture_events +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=False, + http_request_source_threshold_ms=0, + ) + + # server for making span request + async def handler(request): + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + + async def hello(request): + span_client = await aiohttp_client(raw_server) + await span_client.get("/") + return web.Response(text="hello") + + app = web.Application() + app.router.add_get(r"/", hello) + + events = capture_events() + + client = await aiohttp_client(app) + await client.get("/") + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.asyncio +@pytest.mark.parametrize("enable_http_request_source", [None, True]) +async def test_request_source_enabled( + sentry_init, + aiohttp_raw_server, + aiohttp_client, + capture_events, + enable_http_request_source, +): + sentry_options = { + "integrations": [AioHttpIntegration()], + "traces_sample_rate": 1.0, + "http_request_source_threshold_ms": 0, + } + if enable_http_request_source is not None: + sentry_options["enable_http_request_source"] = enable_http_request_source + + sentry_init(**sentry_options) + + # server for making span request + async def handler(request): + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + + async def hello(request): + span_client = await aiohttp_client(raw_server) + await span_client.get("/") + return web.Response(text="hello") + + app = web.Application() + app.router.add_get(r"/", hello) + + events = capture_events() + + client = await aiohttp_client(app) + await client.get("/") + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + +@pytest.mark.asyncio +async def test_request_source( + sentry_init, aiohttp_raw_server, aiohttp_client, capture_events +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=True, + http_request_source_threshold_ms=0, + ) + + # server for making span request + async def handler(request): + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + + async def handler_with_outgoing_request(request): + span_client = await aiohttp_client(raw_server) + await span_client.get("/") + return web.Response(text="hello") + + app = web.Application() + app.router.add_get(r"/", handler_with_outgoing_request) + + events = capture_events() + + client = await aiohttp_client(app) + await client.get("/") + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.aiohttp.test_aiohttp" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/aiohttp/test_aiohttp.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "handler_with_outgoing_request" + + +@pytest.mark.asyncio +async def test_request_source_with_module_in_search_path( + sentry_init, aiohttp_raw_server, aiohttp_client, capture_events +): + """ + Test that request source is relative to the path of the module it ran in + """ + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=True, + http_request_source_threshold_ms=0, + ) + + # server for making span request + async def handler(request): + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + + from aiohttp_helpers.helpers import get_request_with_client + + async def handler_with_outgoing_request(request): + span_client = await aiohttp_client(raw_server) + await get_request_with_client(span_client, "/") + return web.Response(text="hello") + + app = web.Application() + app.router.add_get(r"/", handler_with_outgoing_request) + + events = capture_events() + + client = await aiohttp_client(app) + await client.get("/") + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert data.get(SPANDATA.CODE_NAMESPACE) == "aiohttp_helpers.helpers" + assert data.get(SPANDATA.CODE_FILEPATH) == "aiohttp_helpers/helpers.py" + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_client" + + +@pytest.mark.asyncio +async def test_no_request_source_if_duration_too_short( + sentry_init, aiohttp_raw_server, aiohttp_client, capture_events +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=True, + http_request_source_threshold_ms=100, + ) + + # server for making span request + async def handler(request): + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + + async def handler_with_outgoing_request(request): + span_client = await aiohttp_client(raw_server) + await span_client.get("/") + return web.Response(text="hello") + + app = web.Application() + app.router.add_get(r"/", handler_with_outgoing_request) + + events = capture_events() + + def fake_create_trace_context(*args, **kwargs): + trace_context = create_trace_config() + + async def overwrite_timestamps(session, trace_config_ctx, params): + span = trace_config_ctx.span + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999) + + trace_context.on_request_end.insert(0, overwrite_timestamps) + + return trace_context + + with mock.patch( + "sentry_sdk.integrations.aiohttp.create_trace_config", + fake_create_trace_context, + ): + client = await aiohttp_client(app) + await client.get("/") + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO not in data + assert SPANDATA.CODE_NAMESPACE not in data + assert SPANDATA.CODE_FILEPATH not in data + assert SPANDATA.CODE_FUNCTION not in data + + +@pytest.mark.asyncio +async def test_request_source_if_duration_over_threshold( + sentry_init, aiohttp_raw_server, aiohttp_client, capture_events +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + enable_http_request_source=True, + http_request_source_threshold_ms=100, + ) + + # server for making span request + async def handler(request): + return web.Response(text="OK") + + raw_server = await aiohttp_raw_server(handler) + + async def handler_with_outgoing_request(request): + span_client = await aiohttp_client(raw_server) + await span_client.get("/") + return web.Response(text="hello") + + app = web.Application() + app.router.add_get(r"/", handler_with_outgoing_request) + + events = capture_events() + + def fake_create_trace_context(*args, **kwargs): + trace_context = create_trace_config() + + async def overwrite_timestamps(session, trace_config_ctx, params): + span = trace_config_ctx.span + span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0) + span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001) + + trace_context.on_request_end.insert(0, overwrite_timestamps) + + return trace_context + + with mock.patch( + "sentry_sdk.integrations.aiohttp.create_trace_config", + fake_create_trace_context, + ): + client = await aiohttp_client(app) + await client.get("/") + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("GET") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + assert ( + data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.aiohttp.test_aiohttp" + ) + assert data.get(SPANDATA.CODE_FILEPATH).endswith( + "tests/integrations/aiohttp/test_aiohttp.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "handler_with_outgoing_request" + + @pytest.mark.asyncio async def test_span_origin( sentry_init,