Skip to content
4 changes: 3 additions & 1 deletion sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Span Finalization Prevents Source Recording

The add_http_request_source(span) call is placed after span.finish(). Since spans are immutable once finished, this prevents the source information from being recorded and potentially interferes with duration-based checks within the function.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I placed add_http_request_source(span) after the span is finished because you need the end timestamp to determine the delay in receiving a response to the HTTP request.

It's done analogously in asyncpg and sqlalchemy.


trace_config = TraceConfig()

trace_config.on_request_start.append(on_request_start)
Expand Down
6 changes: 6 additions & 0 deletions tests/integrations/aiohttp/__init__.py
Original file line number Diff line number Diff line change
@@ -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__)))
Empty file.
2 changes: 2 additions & 0 deletions tests/integrations/aiohttp/aiohttp_helpers/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
async def get_request_with_client(client, url):
await client.get(url)
346 changes: 345 additions & 1 deletion tests/integrations/aiohttp/test_aiohttp.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
import datetime
import asyncio
import json

Expand All @@ -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


Expand Down Expand Up @@ -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,
Expand Down