diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 29812fce7c..5f7810732b 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -37,6 +37,8 @@ _DEFAULT_TRANSACTION_NAME = "generic ASGI request" +TRANSACTION_STYLE_VALUES = ("endpoint", "url") + def _capture_exception(hub, exc): # type: (Hub, Any) -> None @@ -68,10 +70,10 @@ def _looks_like_asgi3(app): class SentryAsgiMiddleware: - __slots__ = ("app", "__call__") + __slots__ = ("app", "__call__", "transaction_style") - def __init__(self, app, unsafe_context_data=False): - # type: (Any, bool) -> None + def __init__(self, app, unsafe_context_data=False, transaction_style="endpoint"): + # type: (Any, bool, str) -> None """ Instrument an ASGI application with Sentry. Provides HTTP/websocket data to sent events and basic handling for exceptions bubbling up @@ -87,6 +89,12 @@ def __init__(self, app, unsafe_context_data=False): "The ASGI middleware for Sentry requires Python 3.7+ " "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE ) + if transaction_style not in TRANSACTION_STYLE_VALUES: + raise ValueError( + "Invalid value for transaction_style: %s (must be in %s)" + % (transaction_style, TRANSACTION_STYLE_VALUES) + ) + self.transaction_style = transaction_style self.app = app if _looks_like_asgi3(app): @@ -179,12 +187,21 @@ def event_processor(self, event, hint, asgi_scope): event.get("transaction", _DEFAULT_TRANSACTION_NAME) == _DEFAULT_TRANSACTION_NAME ): - endpoint = asgi_scope.get("endpoint") - # Webframeworks like Starlette mutate the ASGI env once routing is - # done, which is sometime after the request has started. If we have - # an endpoint, overwrite our generic transaction name. - if endpoint: - event["transaction"] = transaction_from_function(endpoint) + if self.transaction_style == "endpoint": + endpoint = asgi_scope.get("endpoint") + # Webframeworks like Starlette mutate the ASGI env once routing is + # done, which is sometime after the request has started. If we have + # an endpoint, overwrite our generic transaction name. + if endpoint: + event["transaction"] = transaction_from_function(endpoint) + elif self.transaction_style == "url": + # FastAPI includes the route object in the scope to let Sentry extract the + # path from it for the transaction name + route = asgi_scope.get("route") + if route: + path = getattr(route, "path", None) + if path is not None: + event["transaction"] = path event["request"] = request_info diff --git a/tests/integrations/asgi/test_fastapi.py b/tests/integrations/asgi/test_fastapi.py new file mode 100644 index 0000000000..518b8544b2 --- /dev/null +++ b/tests/integrations/asgi/test_fastapi.py @@ -0,0 +1,46 @@ +import sys + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from sentry_sdk import capture_message +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware + + +@pytest.fixture +def app(): + app = FastAPI() + + @app.get("/users/{user_id}") + async def get_user(user_id: str): + capture_message("hi", level="error") + return {"user_id": user_id} + + app.add_middleware(SentryAsgiMiddleware, transaction_style="url") + + return app + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +def test_fastapi_transaction_style(sentry_init, app, capture_events): + sentry_init(send_default_pii=True) + events = capture_events() + + client = TestClient(app) + response = client.get("/users/rick") + + assert response.status_code == 200 + + (event,) = events + assert event["transaction"] == "/users/{user_id}" + assert event["request"]["env"] == {"REMOTE_ADDR": "testclient"} + assert event["request"]["url"].endswith("/users/rick") + assert event["request"]["method"] == "GET" + + # Assert that state is not leaked + events.clear() + capture_message("foo") + (event,) = events + + assert "request" not in event + assert "transaction" not in event diff --git a/tox.ini b/tox.ini index cb158d7209..bc087ad23c 100644 --- a/tox.ini +++ b/tox.ini @@ -212,6 +212,7 @@ deps = asgi: starlette asgi: requests + asgi: fastapi sqlalchemy-1.2: sqlalchemy>=1.2,<1.3 sqlalchemy-1.3: sqlalchemy>=1.3,<1.4