Skip to content
Open
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
110 changes: 83 additions & 27 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
nullcontext,
)
from sentry_sdk.sessions import track_session
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.tracing import (
SOURCE_FOR_STYLE,
Transaction,
TransactionSource,
)
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
ContextVar,
event_from_exception,
Expand All @@ -35,17 +38,19 @@
transaction_from_function,
_get_installed_modules,
)
from sentry_sdk.tracing import Transaction

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any
from typing import ContextManager
from typing import Dict
from typing import Optional
from typing import Tuple
from typing import Union

from sentry_sdk._types import Event, Hint
from sentry_sdk._types import Attributes, Event, Hint
from sentry_sdk.tracing import Span


_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied")
Expand Down Expand Up @@ -185,6 +190,9 @@ async def _run_app(
self._capture_lifespan_exception(exc)
raise exc from None

client = sentry_sdk.get_client()
span_streaming = has_span_streaming_enabled(client.options)

_asgi_middleware_applied.set(True)
try:
with sentry_sdk.isolation_scope() as sentry_scope:
Expand All @@ -204,48 +212,96 @@ async def _run_app(
)

method = scope.get("method", "").upper()
transaction = None
if ty in ("http", "websocket"):
if ty == "websocket" or method in self.http_methods_to_capture:
transaction = continue_trace(
_get_headers(scope),
op="{}.server".format(ty),

span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]"
if span_streaming:
segment: "Optional[StreamedSpan]" = None
attributes: "Attributes" = {
"sentry.span.source": getattr(
transaction_source, "value", transaction_source
),
"sentry.origin": self.span_origin,
"asgi.type": ty,
}
sentry_scope.set_custom_sampling_context({"asgi_scope": scope})
Copy link

Choose a reason for hiding this comment

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

Custom sampling context lost due to ordering issue

High Severity

In the span streaming path, sentry_scope.set_custom_sampling_context({"asgi_scope": scope}) is called before sentry_sdk.traces.continue_trace() and sentry_sdk.traces.new_trace(). Both of those functions replace the scope's propagation context with a brand-new PropagationContext object, discarding the custom sampling context that was just set. This means traces_sampler callbacks will never receive the asgi_scope when span streaming is enabled, breaking custom sampling decisions.

Additional Locations (2)
Fix in Cursor Fix in Web


if ty in ("http", "websocket"):
if (
ty == "websocket"
or method in self.http_methods_to_capture
):
sentry_sdk.traces.continue_trace(_get_headers(scope))
attributes["sentry.op"] = f"{ty}.server"
segment = sentry_sdk.traces.start_span(
name=transaction_name, attributes=attributes
)
else:
sentry_sdk.traces.new_trace()
attributes["sentry.op"] = OP.HTTP_SERVER
segment = sentry_sdk.traces.start_span(
name=transaction_name, attributes=attributes
)

span_ctx = segment or nullcontext()

else:
transaction = None
if ty in ("http", "websocket"):
if (
ty == "websocket"
or method in self.http_methods_to_capture
):
transaction = continue_trace(
_get_headers(scope),
op="{}.server".format(ty),
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)
else:
transaction = Transaction(
op=OP.HTTP_SERVER,
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)
else:
transaction = Transaction(
op=OP.HTTP_SERVER,
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)

if transaction:
transaction.set_tag("asgi.type", ty)
if transaction:
transaction.set_tag("asgi.type", ty)

transaction_context = (
sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"asgi_scope": scope},
span_ctx = (
sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"asgi_scope": scope},
)
if transaction is not None
else nullcontext()
)
if transaction is not None
else nullcontext()
)
with transaction_context:

with span_ctx as span:
try:

async def _sentry_wrapped_send(
event: "Dict[str, Any]",
) -> "Any":
if transaction is not None:
if span is not None:
is_http_response = (
event.get("type") == "http.response.start"
and "status" in event
)
if is_http_response:
transaction.set_http_status(event["status"])
if isinstance(span, StreamedSpan):
span.status = (
"error"
if event["status"] >= 400
else "ok"
)
span.set_attribute(
"http.response.status_code",
event["status"],
)
else:
span.set_http_status(event["status"])

return await send(event)

Expand Down
Loading