Skip to content

Commit 14398e0

Browse files
feat(aiohttp): Add source information for slow outgoing HTTP requests (#4905)
Add code source as described in getsentry/sentry-docs#15161. Uses functionality from SQL query source and tests that it works in the HTTP request setting.
1 parent e2881a8 commit 14398e0

File tree

5 files changed

+356
-2
lines changed

5 files changed

+356
-2
lines changed

sentry_sdk/integrations/aiohttp.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
SOURCE_FOR_STYLE,
2323
TransactionSource,
2424
)
25-
from sentry_sdk.tracing_utils import should_propagate_trace
25+
from sentry_sdk.tracing_utils import should_propagate_trace, add_http_request_source
2626
from sentry_sdk.utils import (
2727
capture_internal_exceptions,
2828
ensure_integration_enabled,
@@ -279,6 +279,8 @@ async def on_request_end(session, trace_config_ctx, params):
279279
span.set_data("reason", params.response.reason)
280280
span.finish()
281281

282+
add_http_request_source(span)
283+
282284
trace_config = TraceConfig()
283285

284286
trace_config.on_request_start.append(on_request_start)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import os
2+
import sys
13
import pytest
24

35
pytest.importorskip("aiohttp")
6+
7+
# Load `aiohttp_helpers` into the module search path to test request source path names relative to module. See
8+
# `test_request_source_with_module_in_search_path`
9+
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))

tests/integrations/aiohttp/aiohttp_helpers/__init__.py

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
async def get_request_with_client(client, url):
2+
await client.get(url)

tests/integrations/aiohttp/test_aiohttp.py

Lines changed: 345 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
import datetime
13
import asyncio
24
import json
35

@@ -18,7 +20,8 @@
1820
)
1921

2022
from sentry_sdk import capture_message, start_transaction
21-
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
23+
from sentry_sdk.integrations.aiohttp import AioHttpIntegration, create_trace_config
24+
from sentry_sdk.consts import SPANDATA
2225
from tests.conftest import ApproxDict
2326

2427

@@ -633,6 +636,347 @@ async def handler(request):
633636
)
634637

635638

639+
@pytest.mark.asyncio
640+
async def test_request_source_disabled(
641+
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
642+
):
643+
sentry_init(
644+
integrations=[AioHttpIntegration()],
645+
traces_sample_rate=1.0,
646+
enable_http_request_source=False,
647+
http_request_source_threshold_ms=0,
648+
)
649+
650+
# server for making span request
651+
async def handler(request):
652+
return web.Response(text="OK")
653+
654+
raw_server = await aiohttp_raw_server(handler)
655+
656+
async def hello(request):
657+
span_client = await aiohttp_client(raw_server)
658+
await span_client.get("/")
659+
return web.Response(text="hello")
660+
661+
app = web.Application()
662+
app.router.add_get(r"/", hello)
663+
664+
events = capture_events()
665+
666+
client = await aiohttp_client(app)
667+
await client.get("/")
668+
669+
(event,) = events
670+
671+
span = event["spans"][-1]
672+
assert span["description"].startswith("GET")
673+
674+
data = span.get("data", {})
675+
676+
assert SPANDATA.CODE_LINENO not in data
677+
assert SPANDATA.CODE_NAMESPACE not in data
678+
assert SPANDATA.CODE_FILEPATH not in data
679+
assert SPANDATA.CODE_FUNCTION not in data
680+
681+
682+
@pytest.mark.asyncio
683+
@pytest.mark.parametrize("enable_http_request_source", [None, True])
684+
async def test_request_source_enabled(
685+
sentry_init,
686+
aiohttp_raw_server,
687+
aiohttp_client,
688+
capture_events,
689+
enable_http_request_source,
690+
):
691+
sentry_options = {
692+
"integrations": [AioHttpIntegration()],
693+
"traces_sample_rate": 1.0,
694+
"http_request_source_threshold_ms": 0,
695+
}
696+
if enable_http_request_source is not None:
697+
sentry_options["enable_http_request_source"] = enable_http_request_source
698+
699+
sentry_init(**sentry_options)
700+
701+
# server for making span request
702+
async def handler(request):
703+
return web.Response(text="OK")
704+
705+
raw_server = await aiohttp_raw_server(handler)
706+
707+
async def hello(request):
708+
span_client = await aiohttp_client(raw_server)
709+
await span_client.get("/")
710+
return web.Response(text="hello")
711+
712+
app = web.Application()
713+
app.router.add_get(r"/", hello)
714+
715+
events = capture_events()
716+
717+
client = await aiohttp_client(app)
718+
await client.get("/")
719+
720+
(event,) = events
721+
722+
span = event["spans"][-1]
723+
assert span["description"].startswith("GET")
724+
725+
data = span.get("data", {})
726+
727+
assert SPANDATA.CODE_LINENO in data
728+
assert SPANDATA.CODE_NAMESPACE in data
729+
assert SPANDATA.CODE_FILEPATH in data
730+
assert SPANDATA.CODE_FUNCTION in data
731+
732+
733+
@pytest.mark.asyncio
734+
async def test_request_source(
735+
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
736+
):
737+
sentry_init(
738+
integrations=[AioHttpIntegration()],
739+
traces_sample_rate=1.0,
740+
enable_http_request_source=True,
741+
http_request_source_threshold_ms=0,
742+
)
743+
744+
# server for making span request
745+
async def handler(request):
746+
return web.Response(text="OK")
747+
748+
raw_server = await aiohttp_raw_server(handler)
749+
750+
async def handler_with_outgoing_request(request):
751+
span_client = await aiohttp_client(raw_server)
752+
await span_client.get("/")
753+
return web.Response(text="hello")
754+
755+
app = web.Application()
756+
app.router.add_get(r"/", handler_with_outgoing_request)
757+
758+
events = capture_events()
759+
760+
client = await aiohttp_client(app)
761+
await client.get("/")
762+
763+
(event,) = events
764+
765+
span = event["spans"][-1]
766+
assert span["description"].startswith("GET")
767+
768+
data = span.get("data", {})
769+
770+
assert SPANDATA.CODE_LINENO in data
771+
assert SPANDATA.CODE_NAMESPACE in data
772+
assert SPANDATA.CODE_FILEPATH in data
773+
assert SPANDATA.CODE_FUNCTION in data
774+
775+
assert type(data.get(SPANDATA.CODE_LINENO)) == int
776+
assert data.get(SPANDATA.CODE_LINENO) > 0
777+
assert (
778+
data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.aiohttp.test_aiohttp"
779+
)
780+
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
781+
"tests/integrations/aiohttp/test_aiohttp.py"
782+
)
783+
784+
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
785+
assert is_relative_path
786+
787+
assert data.get(SPANDATA.CODE_FUNCTION) == "handler_with_outgoing_request"
788+
789+
790+
@pytest.mark.asyncio
791+
async def test_request_source_with_module_in_search_path(
792+
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
793+
):
794+
"""
795+
Test that request source is relative to the path of the module it ran in
796+
"""
797+
sentry_init(
798+
integrations=[AioHttpIntegration()],
799+
traces_sample_rate=1.0,
800+
enable_http_request_source=True,
801+
http_request_source_threshold_ms=0,
802+
)
803+
804+
# server for making span request
805+
async def handler(request):
806+
return web.Response(text="OK")
807+
808+
raw_server = await aiohttp_raw_server(handler)
809+
810+
from aiohttp_helpers.helpers import get_request_with_client
811+
812+
async def handler_with_outgoing_request(request):
813+
span_client = await aiohttp_client(raw_server)
814+
await get_request_with_client(span_client, "/")
815+
return web.Response(text="hello")
816+
817+
app = web.Application()
818+
app.router.add_get(r"/", handler_with_outgoing_request)
819+
820+
events = capture_events()
821+
822+
client = await aiohttp_client(app)
823+
await client.get("/")
824+
825+
(event,) = events
826+
827+
span = event["spans"][-1]
828+
assert span["description"].startswith("GET")
829+
830+
data = span.get("data", {})
831+
832+
assert SPANDATA.CODE_LINENO in data
833+
assert SPANDATA.CODE_NAMESPACE in data
834+
assert SPANDATA.CODE_FILEPATH in data
835+
assert SPANDATA.CODE_FUNCTION in data
836+
837+
assert type(data.get(SPANDATA.CODE_LINENO)) == int
838+
assert data.get(SPANDATA.CODE_LINENO) > 0
839+
assert data.get(SPANDATA.CODE_NAMESPACE) == "aiohttp_helpers.helpers"
840+
assert data.get(SPANDATA.CODE_FILEPATH) == "aiohttp_helpers/helpers.py"
841+
842+
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
843+
assert is_relative_path
844+
845+
assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_client"
846+
847+
848+
@pytest.mark.asyncio
849+
async def test_no_request_source_if_duration_too_short(
850+
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
851+
):
852+
sentry_init(
853+
integrations=[AioHttpIntegration()],
854+
traces_sample_rate=1.0,
855+
enable_http_request_source=True,
856+
http_request_source_threshold_ms=100,
857+
)
858+
859+
# server for making span request
860+
async def handler(request):
861+
return web.Response(text="OK")
862+
863+
raw_server = await aiohttp_raw_server(handler)
864+
865+
async def handler_with_outgoing_request(request):
866+
span_client = await aiohttp_client(raw_server)
867+
await span_client.get("/")
868+
return web.Response(text="hello")
869+
870+
app = web.Application()
871+
app.router.add_get(r"/", handler_with_outgoing_request)
872+
873+
events = capture_events()
874+
875+
def fake_create_trace_context(*args, **kwargs):
876+
trace_context = create_trace_config()
877+
878+
async def overwrite_timestamps(session, trace_config_ctx, params):
879+
span = trace_config_ctx.span
880+
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
881+
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999)
882+
883+
trace_context.on_request_end.insert(0, overwrite_timestamps)
884+
885+
return trace_context
886+
887+
with mock.patch(
888+
"sentry_sdk.integrations.aiohttp.create_trace_config",
889+
fake_create_trace_context,
890+
):
891+
client = await aiohttp_client(app)
892+
await client.get("/")
893+
894+
(event,) = events
895+
896+
span = event["spans"][-1]
897+
assert span["description"].startswith("GET")
898+
899+
data = span.get("data", {})
900+
901+
assert SPANDATA.CODE_LINENO not in data
902+
assert SPANDATA.CODE_NAMESPACE not in data
903+
assert SPANDATA.CODE_FILEPATH not in data
904+
assert SPANDATA.CODE_FUNCTION not in data
905+
906+
907+
@pytest.mark.asyncio
908+
async def test_request_source_if_duration_over_threshold(
909+
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
910+
):
911+
sentry_init(
912+
integrations=[AioHttpIntegration()],
913+
traces_sample_rate=1.0,
914+
enable_http_request_source=True,
915+
http_request_source_threshold_ms=100,
916+
)
917+
918+
# server for making span request
919+
async def handler(request):
920+
return web.Response(text="OK")
921+
922+
raw_server = await aiohttp_raw_server(handler)
923+
924+
async def handler_with_outgoing_request(request):
925+
span_client = await aiohttp_client(raw_server)
926+
await span_client.get("/")
927+
return web.Response(text="hello")
928+
929+
app = web.Application()
930+
app.router.add_get(r"/", handler_with_outgoing_request)
931+
932+
events = capture_events()
933+
934+
def fake_create_trace_context(*args, **kwargs):
935+
trace_context = create_trace_config()
936+
937+
async def overwrite_timestamps(session, trace_config_ctx, params):
938+
span = trace_config_ctx.span
939+
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
940+
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001)
941+
942+
trace_context.on_request_end.insert(0, overwrite_timestamps)
943+
944+
return trace_context
945+
946+
with mock.patch(
947+
"sentry_sdk.integrations.aiohttp.create_trace_config",
948+
fake_create_trace_context,
949+
):
950+
client = await aiohttp_client(app)
951+
await client.get("/")
952+
953+
(event,) = events
954+
955+
span = event["spans"][-1]
956+
assert span["description"].startswith("GET")
957+
958+
data = span.get("data", {})
959+
960+
assert SPANDATA.CODE_LINENO in data
961+
assert SPANDATA.CODE_NAMESPACE in data
962+
assert SPANDATA.CODE_FILEPATH in data
963+
assert SPANDATA.CODE_FUNCTION in data
964+
965+
assert type(data.get(SPANDATA.CODE_LINENO)) == int
966+
assert data.get(SPANDATA.CODE_LINENO) > 0
967+
assert (
968+
data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.aiohttp.test_aiohttp"
969+
)
970+
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
971+
"tests/integrations/aiohttp/test_aiohttp.py"
972+
)
973+
974+
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
975+
assert is_relative_path
976+
977+
assert data.get(SPANDATA.CODE_FUNCTION) == "handler_with_outgoing_request"
978+
979+
636980
@pytest.mark.asyncio
637981
async def test_span_origin(
638982
sentry_init,

0 commit comments

Comments
 (0)