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,