Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add support for setting transaction name to path in FastAPI #1349

Merged
merged 4 commits into from Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 26 additions & 9 deletions sentry_sdk/integrations/asgi.py
Expand Up @@ -37,6 +37,8 @@

_DEFAULT_TRANSACTION_NAME = "generic ASGI request"

TRANSACTION_STYLE_VALUES = ("endpoint", "url")


def _capture_exception(hub, exc):
# type: (Hub, Any) -> None
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down
46 changes: 46 additions & 0 deletions 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
1 change: 1 addition & 0 deletions tox.ini
Expand Up @@ -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
Expand Down