Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions sentry_sdk/integrations/_asgi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import Union
from typing_extensions import Literal

from sentry_sdk._types import Attributes
from sentry_sdk.utils import AnnotatedValue


Expand Down Expand Up @@ -105,3 +106,32 @@
request_data["env"] = {"REMOTE_ADDR": _get_ip(asgi_scope)}

return request_data


def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]":
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is an attributes based copy of _get_request_data just above

"""
Return attributes related to the HTTP request from the ASGI scope.
"""
attributes: "Attributes" = {}

ty = asgi_scope["type"]
if ty in ("http", "websocket"):
if asgi_scope.get("method"):
attributes["http.request.method"] = asgi_scope["method"].upper()

headers = _filter_headers(_get_headers(asgi_scope))
# TODO[span-first]: Correctly merge headers if duplicate
for header, value in headers.items():
attributes[f"http.request.headers.{header.lower()}"] = [value]

attributes["http.query"] = _get_query(asgi_scope)

attributes["url.full"] = _get_url(
asgi_scope, "http" if ty == "http" else "ws", headers.get("host")
)

client = asgi_scope.get("client")
if client and should_send_default_pii():
attributes["client.address"] = {"REMOTE_ADDR": _get_ip(asgi_scope)}

Check warning on line 135 in sentry_sdk/integrations/_asgi_common.py

View check run for this annotation

@sentry/warden / warden: code-review

Attribute value is a dict but AttributeValue type does not support dicts

The `client.address` attribute is assigned a dictionary value `{"REMOTE_ADDR": _get_ip(asgi_scope)}`, but the `AttributeValue` type (defined in `_types.py:224-237`) only supports primitive types (str, bool, float, int) and their lists/tuples. This type mismatch will likely cause issues when the attribute is serialized or processed by Sentry's telemetry system. The value should be a simple string like `_get_ip(asgi_scope)` instead.

return attributes

Check failure on line 137 in sentry_sdk/integrations/_asgi_common.py

View check run for this annotation

@sentry/warden / warden: find-bugs

Return value contract mismatch: _get_request_attributes returns dict but caller unpacks as tuples

The function `_get_request_attributes` returns a `dict[str, Any]`. However, in `asgi.py` line 310, the return value is used as `for attribute, value in _get_request_attributes(scope):`. Iterating over a dict directly yields only keys, not key-value pairs, causing a `ValueError` when trying to unpack a single key into two variables (`attribute, value`). This will crash the ASGI middleware at runtime whenever span streaming is not enabled.
65 changes: 61 additions & 4 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sentry_sdk.consts import OP
from sentry_sdk.integrations._asgi_common import (
_get_headers,
_get_request_attributes,

Check failure on line 18 in sentry_sdk/integrations/asgi.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[NNE-MN5] Return value contract mismatch: _get_request_attributes returns dict but caller unpacks as tuples (additional location)

The function `_get_request_attributes` returns a `dict[str, Any]`. However, in `asgi.py` line 310, the return value is used as `for attribute, value in _get_request_attributes(scope):`. Iterating over a dict directly yields only keys, not key-value pairs, causing a `ValueError` when trying to unpack a single key into two variables (`attribute, value`). This will crash the ASGI middleware at runtime whenever span streaming is not enabled.
_get_request_data,
_get_url,
)
Expand All @@ -23,7 +24,11 @@
nullcontext,
)
from sentry_sdk.sessions import track_session
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.traces import (
StreamedSpan,
SegmentSource,
SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE,
)
from sentry_sdk.tracing import (
SOURCE_FOR_STYLE,
Transaction,
Expand All @@ -40,6 +45,7 @@
_get_installed_modules,
reraise,
capture_internal_exceptions,
qualname_from_function,
)

from typing import TYPE_CHECKING
Expand Down Expand Up @@ -235,7 +241,7 @@
transaction_source, "value", transaction_source
),
"sentry.origin": self.span_origin,
"asgi.type": ty,
"network.protocol.name": ty,
}

if ty in ("http", "websocket"):
Expand Down Expand Up @@ -301,6 +307,9 @@
else nullcontext()
)

for attribute, value in _get_request_attributes(scope):
sentry_scope.set_attribute(attribute, value)

Check failure on line 311 in sentry_sdk/integrations/asgi.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[NNE-MN5] Return value contract mismatch: _get_request_attributes returns dict but caller unpacks as tuples (additional location)

The function `_get_request_attributes` returns a `dict[str, Any]`. However, in `asgi.py` line 310, the return value is used as `for attribute, value in _get_request_attributes(scope):`. Iterating over a dict directly yields only keys, not key-value pairs, causing a `ValueError` when trying to unpack a single key into two variables (`attribute, value`). This will crash the ASGI middleware at runtime whenever span streaming is not enabled.

Check failure on line 311 in sentry_sdk/integrations/asgi.py

View check run for this annotation

@sentry/warden / warden: code-review

Iterating over dict without .items() will cause ValueError at runtime

The code iterates over `_get_request_attributes(scope)` using `for attribute, value in ...`, but `_get_request_attributes` returns a `dict[str, Any]`. Iterating directly over a dict yields only its keys (strings), not key-value pairs. Python will raise `ValueError: too many values to unpack` or `ValueError: not enough values to unpack` when trying to unpack single string keys into two variables. This will cause a runtime crash on every ASGI request when span streaming is not enabled.

with span_ctx as span:
try:

Expand Down Expand Up @@ -329,13 +338,24 @@
return await send(event)

if asgi_version == 2:
return await self.app(scope)(
result = await self.app(scope)(
receive, _sentry_wrapped_send
)
else:
return await self.app(
result = await self.app(
scope, receive, _sentry_wrapped_send
)

with capture_internal_exceptions():
name, source = self._get_segment_name_and_source(
self.transaction_style, scope
)
if isinstance(span, StreamedSpan):
span.name = name
span.set_attribute("sentry.span.source", source)

return result

except Exception as exc:
suppress_chained_exceptions = (
sentry_sdk.get_client()
Expand Down Expand Up @@ -424,3 +444,40 @@
return name, source

return name, source

def _get_segment_name_and_source(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a copy of _get_transaction_name_and_source above, just adapted for segments

self: "SentryAsgiMiddleware", segment_style: str, asgi_scope: "Any"
) -> "Tuple[str, str]":
name = None
source = SEGMENT_SOURCE_FOR_STYLE[segment_style]

Check warning on line 452 in sentry_sdk/integrations/asgi.py

View check run for this annotation

@sentry/warden / warden: code-review

Inconsistent return type for segment source - returns enum or string depending on code path

The `_get_segment_name_and_source` method has inconsistent return types for the `source` value. Line 452 initializes `source` as a `SegmentSource` enum from `SEGMENT_SOURCE_FOR_STYLE[segment_style]`. However, fallback branches (lines 464, 476, 480) assign `SegmentSource.*.value` (strings). When an endpoint or route is successfully found (lines 460-461 or 472-473), the source enum is returned unchanged. When used at line 355 with `span.set_attribute("sentry.span.source", source)`, this causes the attribute to sometimes be set to an enum object and sometimes a string, leading to inconsistent telemetry data.
ty = asgi_scope.get("type")

if segment_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:
name = qualname_from_function(endpoint) or ""
else:
name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None)
source = SegmentSource.URL.value

elif segment_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:
name = path
else:
name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None)
source = SegmentSource.URL.value

if name is None:
name = _DEFAULT_TRANSACTION_NAME
source = SegmentSource.ROUTE.value
return name, source

return name, source
Loading