Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,12 @@ class SPANDATA:
Example: GET
"""

HTTP_ROUTE = "http.route"
"""
The matched route, that is, the path template used to match the request.
Example: /users/{id}
"""

HTTP_QUERY = "http.query"
"""
The Query string present in the URL.
Expand Down
4 changes: 4 additions & 0 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ async def _run_app(
attributes=attributes,
parent_span=None,
)
sentry_scope.get_current_scope()._server_segment_span = segment
else:
sentry_sdk.traces.new_trace()

Expand All @@ -292,6 +293,9 @@ async def _run_app(
attributes=attributes,
parent_span=None,
)
sentry_scope.get_current_scope()._server_segment_span = (
segment
)

span_ctx = segment or nullcontext()

Expand Down
65 changes: 38 additions & 27 deletions sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry_sdk.utils import has_data_collection_enabled, transaction_from_function

if TYPE_CHECKING:
from typing import Any, Awaitable, Callable, Dict
from typing import Any, Awaitable, Callable, Dict, Optional

from sentry_sdk._types import Event

Expand Down Expand Up @@ -50,34 +50,18 @@ def setup_once() -> None:


def _set_transaction_name_and_source(
scope: "sentry_sdk.Scope", transaction_style: str, request: "Any"
scope: "sentry_sdk.Scope",
transaction_style: str,
endpoint: "Optional[Callable[..., Any]]",
route_path: "Optional[str]",
) -> None:
name = ""

if transaction_style == "endpoint":
endpoint = request.scope.get("endpoint")
if endpoint:
name = transaction_from_function(endpoint) or ""

elif transaction_style == "url":
route = request.scope.get("route")

if route:
# FastAPI >= 0.137 stores the prefix-resolved path on an
# effective_route_context in scope["fastapi"], while
# scope["route"].path holds the unprefixed original.
# Prefer the effective context path when available.
effective_route_context = request.scope.get("fastapi", {}).get(
"effective_route_context"
)
context_path = getattr(effective_route_context, "path", None)

if context_path:
name = context_path
else:
path = getattr(route, "path", None)
if path is not None:
name = path
if transaction_style == "endpoint" and endpoint:
name = transaction_from_function(endpoint) or ""

elif transaction_style == "url" and route_path is not None:
name = route_path

if not name:
name = _DEFAULT_TRANSACTION_NAME
Expand All @@ -103,8 +87,35 @@ async def _wrap_async_handler(

request = args[0]

route = request.scope.get("route")

route_path = None
if route:
# FastAPI >= 0.137 stores the prefix-resolved path on an
# effective_route_context in scope["fastapi"], while
# scope["route"].path holds the unprefixed original.
# Prefer the effective context path when available.
effective_route_context = request.scope.get("fastapi", {}).get(
"effective_route_context"
)
context_path = getattr(effective_route_context, "path", None)

if context_path:
route_path = context_path
else:
path = getattr(route, "path", None)
if path is not None:
route_path = path

server_span = sentry_sdk.get_current_scope()._server_segment_span
if server_span is not None and route_path is not None:
server_span.set_attribute(SPANDATA.HTTP_ROUTE, route_path)

_set_transaction_name_and_source(
sentry_sdk.get_current_scope(), integration.transaction_style, request
sentry_sdk.get_current_scope(),
integration.transaction_style,
endpoint=request.scope.get("endpoint"),
route_path=route_path,
)
sentry_scope = sentry_sdk.get_isolation_scope()
extractor = StarletteRequestExtractor(request)
Expand Down
4 changes: 4 additions & 0 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ class Scope:
"_error_processors",
"_should_capture",
"_span",
"_server_segment_span",
"_session",
"_attachments",
"_force_auto_session_tracking",
Expand Down Expand Up @@ -257,6 +258,8 @@ def __init__(
self._n_breadcrumbs_truncated: int = 0
self._gen_ai_original_message_count: "Dict[str, int]" = {}

self._server_segment_span: "Optional[StreamedSpan]" = None

self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient()

if client is not None:
Expand Down Expand Up @@ -296,6 +299,7 @@ def __copy__(self) -> "Scope":

rv._should_capture = self._should_capture
rv._span = self._span
rv._server_segment_span = self._server_segment_span
rv._session = self._session
rv._force_auto_session_tracking = self._force_auto_session_tracking
rv._attachments = self._attachments.copy()
Expand Down
3 changes: 2 additions & 1 deletion tests/integrations/fastapi/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ def test_transaction_name(


@pytest.mark.parametrize("span_streaming", [True, False])
def test_transaction_name_with_prefix(
def test_http_route_with_prefix(
sentry_init,
capture_envelopes,
capture_items,
Expand Down Expand Up @@ -883,6 +883,7 @@ async def get_user(user_id: int):
segment = segments[0]
assert segment["name"] == "/api/users/{user_id}"
assert segment["attributes"]["sentry.segment.name.source"] == "route"
assert segment["attributes"]["http.route"] == "/api/users/{user_id}"
else:
(transaction_envelope,) = envelopes
transaction_event = transaction_envelope.get_transaction_event()
Expand Down
Loading