Skip to content

Commit 84015f9

Browse files
authored
feat(asgi): Add support for setting transaction name to path in FastAPI (#1349)
1 parent de0bc50 commit 84015f9

File tree

3 files changed

+73
-9
lines changed

3 files changed

+73
-9
lines changed

sentry_sdk/integrations/asgi.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
_DEFAULT_TRANSACTION_NAME = "generic ASGI request"
3939

40+
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
41+
4042

4143
def _capture_exception(hub, exc):
4244
# type: (Hub, Any) -> None
@@ -68,10 +70,10 @@ def _looks_like_asgi3(app):
6870

6971

7072
class SentryAsgiMiddleware:
71-
__slots__ = ("app", "__call__")
73+
__slots__ = ("app", "__call__", "transaction_style")
7274

73-
def __init__(self, app, unsafe_context_data=False):
74-
# type: (Any, bool) -> None
75+
def __init__(self, app, unsafe_context_data=False, transaction_style="endpoint"):
76+
# type: (Any, bool, str) -> None
7577
"""
7678
Instrument an ASGI application with Sentry. Provides HTTP/websocket
7779
data to sent events and basic handling for exceptions bubbling up
@@ -87,6 +89,12 @@ def __init__(self, app, unsafe_context_data=False):
8789
"The ASGI middleware for Sentry requires Python 3.7+ "
8890
"or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
8991
)
92+
if transaction_style not in TRANSACTION_STYLE_VALUES:
93+
raise ValueError(
94+
"Invalid value for transaction_style: %s (must be in %s)"
95+
% (transaction_style, TRANSACTION_STYLE_VALUES)
96+
)
97+
self.transaction_style = transaction_style
9098
self.app = app
9199

92100
if _looks_like_asgi3(app):
@@ -179,12 +187,21 @@ def event_processor(self, event, hint, asgi_scope):
179187
event.get("transaction", _DEFAULT_TRANSACTION_NAME)
180188
== _DEFAULT_TRANSACTION_NAME
181189
):
182-
endpoint = asgi_scope.get("endpoint")
183-
# Webframeworks like Starlette mutate the ASGI env once routing is
184-
# done, which is sometime after the request has started. If we have
185-
# an endpoint, overwrite our generic transaction name.
186-
if endpoint:
187-
event["transaction"] = transaction_from_function(endpoint)
190+
if self.transaction_style == "endpoint":
191+
endpoint = asgi_scope.get("endpoint")
192+
# Webframeworks like Starlette mutate the ASGI env once routing is
193+
# done, which is sometime after the request has started. If we have
194+
# an endpoint, overwrite our generic transaction name.
195+
if endpoint:
196+
event["transaction"] = transaction_from_function(endpoint)
197+
elif self.transaction_style == "url":
198+
# FastAPI includes the route object in the scope to let Sentry extract the
199+
# path from it for the transaction name
200+
route = asgi_scope.get("route")
201+
if route:
202+
path = getattr(route, "path", None)
203+
if path is not None:
204+
event["transaction"] = path
188205

189206
event["request"] = request_info
190207

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import sys
2+
3+
import pytest
4+
from fastapi import FastAPI
5+
from fastapi.testclient import TestClient
6+
from sentry_sdk import capture_message
7+
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
8+
9+
10+
@pytest.fixture
11+
def app():
12+
app = FastAPI()
13+
14+
@app.get("/users/{user_id}")
15+
async def get_user(user_id: str):
16+
capture_message("hi", level="error")
17+
return {"user_id": user_id}
18+
19+
app.add_middleware(SentryAsgiMiddleware, transaction_style="url")
20+
21+
return app
22+
23+
24+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
25+
def test_fastapi_transaction_style(sentry_init, app, capture_events):
26+
sentry_init(send_default_pii=True)
27+
events = capture_events()
28+
29+
client = TestClient(app)
30+
response = client.get("/users/rick")
31+
32+
assert response.status_code == 200
33+
34+
(event,) = events
35+
assert event["transaction"] == "/users/{user_id}"
36+
assert event["request"]["env"] == {"REMOTE_ADDR": "testclient"}
37+
assert event["request"]["url"].endswith("/users/rick")
38+
assert event["request"]["method"] == "GET"
39+
40+
# Assert that state is not leaked
41+
events.clear()
42+
capture_message("foo")
43+
(event,) = events
44+
45+
assert "request" not in event
46+
assert "transaction" not in event

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ deps =
212212

213213
asgi: starlette
214214
asgi: requests
215+
asgi: fastapi
215216

216217
sqlalchemy-1.2: sqlalchemy>=1.2,<1.3
217218
sqlalchemy-1.3: sqlalchemy>=1.3,<1.4

0 commit comments

Comments
 (0)