Skip to content
Draft
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
9 changes: 8 additions & 1 deletion sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
SegmentNameSource,
SpanStatus,
StreamedSpan,
get_current_span,
)
from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
Expand Down Expand Up @@ -327,6 +328,7 @@ async def sentry_urldispatcher_resolve(
return rv

name = None
http_route = None

try:
if integration.transaction_style == "handler_name":
Expand All @@ -335,11 +337,12 @@ async def sentry_urldispatcher_resolve(
route_info = rv.get_info()
pattern = route_info.get("path") or route_info.get("formatter")
name = "{} {}".format(request.method, pattern)
http_route = pattern
except Exception:
pass

if name is not None:
current_span = sentry_sdk.get_current_span()
current_span = get_current_span()
if isinstance(current_span, StreamedSpan) and not isinstance(
current_span, NoOpStreamedSpan
):
Expand All @@ -348,6 +351,10 @@ async def sentry_urldispatcher_resolve(
"sentry.segment.name.source",
SEGMENT_SOURCE_FOR_STYLE[integration.transaction_style].value,
)
if http_route is not None:
current_span._segment.set_attribute(
SPANDATA.HTTP_ROUTE, http_route
)
else:
current_scope = sentry_sdk.get_current_scope()
current_scope.set_transaction_name(
Expand Down
19 changes: 16 additions & 3 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,13 +246,19 @@ async def _run_app(
span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]"
if span_streaming:
segment: "Optional[StreamedSpan]" = None
segment_source = getattr(
transaction_source, "value", transaction_source
)
attributes: "Attributes" = {
"sentry.segment.name.source": getattr(
transaction_source, "value", transaction_source
),
"sentry.segment.name.source": segment_source,
"sentry.origin": self.span_origin,
"network.protocol.name": ty,
}
if (
segment_source == SegmentNameSource.ROUTE.value
and transaction_name != _DEFAULT_TRANSACTION_NAME
):
attributes[SPANDATA.HTTP_ROUTE] = transaction_name

if scope.get("client"):
client_options = sentry_sdk.get_client().options
Expand Down Expand Up @@ -412,6 +418,13 @@ async def _sentry_wrapped_send(
span.set_attribute(
"sentry.segment.name.source", source
)
if (
source == SegmentNameSource.ROUTE.value
and name != _DEFAULT_TRANSACTION_NAME
Comment thread
mjq marked this conversation as resolved.
):
span.set_attribute(
SPANDATA.HTTP_ROUTE, name
)
finally:
_asgi_middleware_applied.set(False)

Expand Down
20 changes: 17 additions & 3 deletions sentry_sdk/integrations/bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import (
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
DidNotEnable,
Expand All @@ -10,8 +11,15 @@
)
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE
from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE
from sentry_sdk.traces import (
SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE,
)
from sentry_sdk.traces import (
SegmentNameSource,
)
from sentry_sdk.tracing import (
SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE,
)
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
capture_internal_exceptions,
Expand Down Expand Up @@ -181,9 +189,15 @@ def _set_segment_name_and_source(transaction_style: str) -> None:
or "bottle request"
)

source = SEGMENT_SOURCE_FOR_STYLE[transaction_style]
if source == SegmentNameSource.ROUTE:
sentry_sdk.get_current_scope().set_segment_attribute(
SPANDATA.HTTP_ROUTE, name
)

sentry_sdk.get_current_scope().set_transaction_name(
name,
source=SEGMENT_SOURCE_FOR_STYLE[transaction_style],
source=source,
)
except RuntimeError:
pass
Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ def _set_transaction_name_and_source(
source = TransactionSource.URL
else:
source = SOURCE_FOR_STYLE[transaction_style]
if source == TransactionSource.ROUTE:
scope.set_segment_attribute(SPANDATA.HTTP_ROUTE, transaction_name)

scope.set_transaction_name(
transaction_name,
Expand Down
7 changes: 6 additions & 1 deletion sentry_sdk/integrations/falcon.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.traces import SegmentNameSource
from sentry_sdk.tracing import SOURCE_FOR_STYLE
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
Expand Down Expand Up @@ -122,7 +124,10 @@ def process_resource(
}
name = name_for_style[integration.transaction_style]
source = sentry_sdk.traces.SOURCE_FOR_STYLE[integration.transaction_style]
sentry_sdk.set_transaction_name(name, source)
scope = sentry_sdk.get_current_scope()
if source == SegmentNameSource.ROUTE:
scope.set_segment_attribute(SPANDATA.HTTP_ROUTE, name)
scope.set_transaction_name(name, source)


TRANSACTION_STYLE_VALUES = ("uri_template", "path")
Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def _set_transaction_name_and_source(
source = TransactionSource.ROUTE
else:
source = SOURCE_FOR_STYLE[transaction_style]
if source == TransactionSource.ROUTE:
scope.set_segment_attribute(SPANDATA.HTTP_ROUTE, name)

scope.set_transaction_name(name, source=source)

Expand Down
12 changes: 7 additions & 5 deletions sentry_sdk/integrations/flask.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
from sentry_sdk.integrations._wsgi_common import (
DEFAULT_HTTP_METHODS_TO_CAPTURE,
RequestExtractor,
)
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.tracing import SOURCE_FOR_STYLE
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
from sentry_sdk.utils import (
capture_internal_exceptions,
ensure_integration_enabled,
Expand Down Expand Up @@ -134,10 +135,11 @@ def _set_transaction_name_and_source(
"url": request.url_rule.rule,
"endpoint": request.url_rule.endpoint,
}
scope.set_transaction_name(
name_for_style[transaction_style],
source=SOURCE_FOR_STYLE[transaction_style],
)
name = name_for_style[transaction_style]
source = SOURCE_FOR_STYLE[transaction_style]
if source == TransactionSource.ROUTE:
scope.set_segment_attribute(SPANDATA.HTTP_ROUTE, name)
scope.set_transaction_name(name, source=source)
except Exception:
pass

Expand Down
14 changes: 12 additions & 2 deletions sentry_sdk/integrations/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
import weakref

import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE
from sentry_sdk.traces import (
SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE,
)
from sentry_sdk.traces import (
SegmentNameSource,
)
from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
Expand Down Expand Up @@ -182,8 +188,12 @@ def _set_transaction_name_and_source(
if is_span_streaming_enabled
else TRANSACTION_SOURCE_FOR_STYLE[transaction_style]
)
name = name_for_style[transaction_style]
if source == SegmentNameSource.ROUTE:
scope.set_segment_attribute(SPANDATA.HTTP_ROUTE, name)

scope.set_transaction_name(
name_for_style[transaction_style],
name,
source=source,
)
except Exception:
Expand Down
16 changes: 13 additions & 3 deletions sentry_sdk/integrations/quart.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.consts import SPANDATA
from sentry_sdk.data_collection import _apply_data_collection_filtering_to_query_string
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.traces import SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE
from sentry_sdk.traces import StreamedSpan, get_current_span
from sentry_sdk.tracing import SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE
from sentry_sdk.traces import SegmentNameSource, StreamedSpan, get_current_span
from sentry_sdk.tracing import (
SOURCE_FOR_STYLE as TRANSACTION_SOURCE_FOR_STYLE,
)
from sentry_sdk.tracing import (
TransactionSource,
)
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
capture_internal_exceptions,
Expand Down Expand Up @@ -163,8 +169,12 @@ def _set_transaction_name_and_source(
else TRANSACTION_SOURCE_FOR_STYLE[transaction_style]
)

name = name_for_style[transaction_style]
if source in (TransactionSource.ROUTE, SegmentNameSource.ROUTE):
scope.set_segment_attribute(SPANDATA.HTTP_ROUTE, name)

scope.set_transaction_name(
name=name_for_style[transaction_style],
name=name,
source=source,
)
except Exception:
Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,8 @@ def _set_transaction_name_and_source(
if name is None:
name = _DEFAULT_TRANSACTION_NAME
source = TransactionSource.ROUTE
elif source == TransactionSource.ROUTE:
scope.set_segment_attribute(SPANDATA.HTTP_ROUTE, name)
Comment thread
mjq marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Host routes set raw request path

Low Severity

For Starlette Host routes, _transaction_name_from_router falls back to the raw request path. That value is then stored as http.route, which is supposed to be a path template. High-cardinality paths can leak into span attributes and descriptions.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1d15a6d. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an existing bug. PR up to fix in #7266.


scope.set_transaction_name(name, source=source)

Expand Down
14 changes: 14 additions & 0 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,20 @@ def set_attributes(self, attributes: "dict[str, AttributeValue]") -> None:
for attribute, value in attributes.items():
self.set_attribute(attribute, value)

def set_segment_attribute(self, key: str, value: "AttributeValue") -> None:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed like a useful helper function to have (and avoids duplicating this code in every HTTP integration), but I would also understand if we don't want to expand the API surface with this: it makes the "segment" name a public thing which AIUI we are trying to avoid?

"""
Set an attribute on the active segment span (the root of the trace in
this service).

Unlike :py:meth:`set_attribute`, which applies to all telemetry captured
while the scope is active, this sets the attribute on the segment span
only.

This method has no effect outside of span streaming mode.
"""
if isinstance(self._span, StreamedSpan):
self._span._segment.set_attribute(key, value)

def remove_attribute(self, attribute: str) -> None:
"""Remove an attribute if set on the scope. No-op if there is no such attribute."""
try:
Expand Down
4 changes: 4 additions & 0 deletions tests/integrations/aiohttp/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,10 @@ async def hello(request):
assert server_segment["name"] == expected_name
assert server_segment["is_segment"]
assert server_segment["attributes"]["sentry.segment.name.source"] == expected_source
if expected_source == "route":
assert server_segment["attributes"]["http.route"] == "/{var}"
else:
assert "http.route" not in server_segment["attributes"]


@pytest.mark.asyncio
Expand Down
35 changes: 35 additions & 0 deletions tests/integrations/asgi/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ async def test_transaction_style(

assert span["name"] == expected_transaction
assert span["attributes"]["sentry.segment.name.source"] == expected_source
assert "http.route" not in span["attributes"]

else:
(transaction_event,) = events
Expand All @@ -678,6 +679,40 @@ async def test_transaction_style(
assert transaction_event["transaction_info"] == {"source": expected_source}


@pytest.mark.asyncio
async def test_http_route_set_for_route_segment_name(
sentry_init,
asgi3_app,
capture_items,
):
sentry_init(
traces_sample_rate=1.0,
trace_lifecycle="stream",
)
app = SentryAsgiMiddleware(asgi3_app, transaction_style="url")

class Route:
path = "/message/{message_id}"

scope = {
"endpoint": asgi3_app,
"route": Route(),
"client": ("127.0.0.1", 60457),
}

async with TestClient(app, scope=scope) as client:
items = capture_items("span")
await client.get("/message/123456")

sentry_sdk.flush()

assert len(items) == 1
span = items[0].payload
assert span["name"] == "/message/{message_id}"
assert span["attributes"]["sentry.segment.name.source"] == "route"
assert span["attributes"]["http.route"] == "/message/{message_id}"


def mock_asgi2_app():
pass

Expand Down
4 changes: 4 additions & 0 deletions tests/integrations/bottle/test_bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,10 @@ def named_hi():

assert segment["name"].endswith(expected_name)
assert segment["attributes"]["sentry.segment.name.source"] == expected_source
if expected_source == "route":
assert segment["attributes"]["http.route"] == expected_name
else:
assert "http.route" not in segment["attributes"]


def test_span_streaming_with_error(sentry_init, capture_items):
Expand Down
4 changes: 4 additions & 0 deletions tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,10 @@ def test_transaction_style(

assert spans[2]["is_segment"] is True
assert spans[2]["attributes"]["sentry.segment.name.source"] == expected_source
if expected_source == "route":
assert spans[2]["attributes"]["http.route"] == expected_transaction
else:
assert "http.route" not in spans[2]["attributes"]

(event,) = (item.payload for item in items if item.type == "event")
else:
Expand Down
Loading
Loading