Skip to content

Commit 8fae8a6

Browse files
authored
fix(starlette): Use url transaction source for host routing (#7266)
### Description When host routing, we don't have a route and instead fall back to the request path for the transaction name. This should be given transaction source `url` (for raw URLs) instead of the current value of `route` (for parameterized routes). This matters for #7183, where the mis-categorized transaction source would lead to bad data in `http.route`. (Bug found by Cursor in #7183 (comment) when reviewing that PR).
1 parent 05b284e commit 8fae8a6

2 files changed

Lines changed: 36 additions & 9 deletions

File tree

sentry_sdk/integrations/starlette.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -843,21 +843,24 @@ async def json(self: "StarletteRequestExtractor") -> "Optional[Dict[str, Any]]":
843843
return None
844844

845845

846-
def _transaction_name_from_router(scope: "StarletteScope") -> "Optional[str]":
846+
def _transaction_name_and_source_from_router(
847+
scope: "StarletteScope",
848+
) -> "Tuple[Optional[str], TransactionSource]":
847849
router = scope.get("router")
848850
if not router:
849-
return None
851+
return None, TransactionSource.ROUTE
850852

851853
for route in router.routes:
852854
match = route.matches(scope)
853855
if match[0] == Match.FULL:
854856
try:
855-
return route.path
857+
return route.path, TransactionSource.ROUTE
856858
except AttributeError:
857-
# routes added via app.host() won't have a path attribute
858-
return scope.get("path")
859+
# Host routes have no path template, so fall back to the
860+
# concrete request path and classify it as a URL.
861+
return scope.get("path"), TransactionSource.URL
859862

860-
return None
863+
return None, TransactionSource.ROUTE
861864

862865

863866
def _set_transaction_name_and_source(
@@ -872,7 +875,7 @@ def _set_transaction_name_and_source(
872875
name = transaction_from_function(endpoint) or None
873876

874877
elif transaction_style == "url":
875-
name = _transaction_name_from_router(request.scope)
878+
name, source = _transaction_name_and_source_from_router(request.scope)
876879

877880
if name is None:
878881
name = _DEFAULT_TRANSACTION_NAME
@@ -891,7 +894,6 @@ def _get_transaction_from_middleware(
891894
name = transaction_from_function(app.__class__)
892895
source = TransactionSource.COMPONENT
893896
elif integration.transaction_style == "url":
894-
name = _transaction_name_from_router(asgi_scope)
895-
source = TransactionSource.ROUTE
897+
name, source = _transaction_name_and_source_from_router(asgi_scope)
896898

897899
return name, source

tests/integrations/starlette/test_starlette.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,31 @@ def test_transaction_style(
10511051
assert event["transaction_info"] == {"source": expected_source}
10521052

10531053

1054+
def test_host_route_path_has_url_source(sentry_init, capture_events):
1055+
sentry_init(
1056+
integrations=[StarletteIntegration(transaction_style="url")],
1057+
traces_sample_rate=1.0,
1058+
)
1059+
1060+
async def hosted_endpoint(request):
1061+
return starlette.responses.JSONResponse({"status": "ok"})
1062+
1063+
subapp = starlette.applications.Starlette(
1064+
routes=[starlette.routing.Route("/users/{user_id}", hosted_endpoint)]
1065+
)
1066+
app = starlette.applications.Starlette(
1067+
routes=[starlette.routing.Host("subapp", subapp)]
1068+
)
1069+
1070+
events = capture_events()
1071+
client = TestClient(app)
1072+
client.get("/users/123456", headers={"Host": "subapp"})
1073+
1074+
(event,) = events
1075+
assert event["transaction"].endswith("/users/123456")
1076+
assert event["transaction_info"] == {"source": "url"}
1077+
1078+
10541079
@pytest.mark.parametrize(
10551080
"test_url,expected_error,expected_message",
10561081
[

0 commit comments

Comments
 (0)