Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
282ba35
feat(asgi): Migrate away from event processor in span first
sentrivana Mar 30, 2026
e2484bd
fixes
sentrivana Mar 30, 2026
51af434
Merge branch 'master' into ivana/migrate-asgi-event-processor
sentrivana Mar 30, 2026
5c3174f
.
sentrivana Mar 30, 2026
b270e5a
.
sentrivana Apr 1, 2026
e93fd1b
.
sentrivana Apr 1, 2026
7cde973
capture_items
sentrivana Apr 2, 2026
ef9e640
Merge branch 'master' into ivana/migrate-asgi-event-processor
sentrivana Apr 2, 2026
df3f3af
.
sentrivana Apr 2, 2026
babb3bd
.
sentrivana Apr 2, 2026
75429dc
no annotatedvalues
sentrivana Apr 2, 2026
d9d7674
.
sentrivana Apr 2, 2026
32e4191
.
sentrivana Apr 2, 2026
5cecf97
more tests
sentrivana Apr 2, 2026
a39337b
Merge branch 'master' into ivana/migrate-asgi-event-processor
sentrivana Apr 2, 2026
7fb4863
.
sentrivana Apr 2, 2026
7490fe3
.
sentrivana Apr 2, 2026
c11e8f1
.
sentrivana Apr 2, 2026
982d471
Merge branch 'master' into ivana/migrate-asgi-event-processor
sentrivana Apr 13, 2026
10bf7c9
ruff
sentrivana Apr 13, 2026
87d00e6
source is not default anymore
sentrivana Apr 13, 2026
507306f
Merge branch 'master' into ivana/migrate-asgi-event-processor
sentrivana Apr 13, 2026
eebc385
mypy
sentrivana Apr 13, 2026
e388901
var name
sentrivana Apr 13, 2026
77b9298
Merge branch 'master' into ivana/migrate-asgi-event-processor
sentrivana Apr 13, 2026
f33e407
feat(wsgi): Migrate WSGI integration to span first
sentrivana Apr 13, 2026
a2ff75c
fixes, more tests
sentrivana Apr 13, 2026
6a8d60f
Merge branch 'master' into ivana/migrate-wsgi-to-span-first
sentrivana Apr 13, 2026
86fd48a
dont set on scope
sentrivana Apr 13, 2026
9648022
move imports
sentrivana Apr 13, 2026
0d85002
.
sentrivana Apr 14, 2026
54064a5
fix
sentrivana Apr 14, 2026
94532f6
set server.port as int
sentrivana Apr 14, 2026
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
4 changes: 3 additions & 1 deletion sentry_sdk/integrations/_asgi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]":

client = asgi_scope.get("client")
if client and should_send_default_pii():
attributes["client.address"] = _get_ip(asgi_scope)
ip = _get_ip(asgi_scope)
attributes["client.address"] = ip
attributes["user.ip_address"] = ip

return attributes
156 changes: 131 additions & 25 deletions sentry_sdk/integrations/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
)
from sentry_sdk.scope import should_send_default_pii, use_isolation_scope
from sentry_sdk.sessions import track_session
from sentry_sdk.tracing import Transaction, TransactionSource
from sentry_sdk.traces import StreamedSpan, SegmentSource
from sentry_sdk.tracing import Span, TransactionSource
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
ContextVar,
capture_internal_exceptions,
Expand All @@ -22,7 +24,18 @@
)

if TYPE_CHECKING:
from typing import Any, Callable, Dict, Iterator, Optional, Protocol, Tuple, TypeVar
from typing import (
Any,
Callable,
ContextManager,
Dict,
Iterator,
Optional,
Protocol,
Tuple,
TypeVar,
Union,
)

from sentry_sdk._types import Event, EventProcessor
from sentry_sdk.utils import ExcInfo
Expand All @@ -42,6 +55,7 @@ def __call__(


_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
_DEFAULT_TRANSACTION_NAME = "generic WSGI request"


def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str:
Expand Down Expand Up @@ -94,6 +108,9 @@ def __call__(
if _wsgi_middleware_applied.get(False):
return self.app(environ, start_response)

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

_wsgi_middleware_applied.set(True)
try:
with sentry_sdk.isolation_scope() as scope:
Expand All @@ -108,34 +125,72 @@ def __call__(
)

method = environ.get("REQUEST_METHOD", "").upper()
transaction = None

span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None
if method in self.http_methods_to_capture:
transaction = continue_trace(
environ,
op=OP.HTTP_SERVER,
name="generic WSGI request",
source=TransactionSource.ROUTE,
origin=self.span_origin,
)
if span_streaming:
sentry_sdk.traces.continue_trace(
dict(_get_headers(environ))
)
scope.set_custom_sampling_context({"wsgi_environ": environ})

span_ctx = sentry_sdk.traces.start_span(
name=_DEFAULT_TRANSACTION_NAME,
attributes={
"sentry.span.source": SegmentSource.ROUTE,
"sentry.origin": self.span_origin,
"sentry.op": OP.HTTP_SERVER,
},
)
else:
transaction = continue_trace(
environ,
op=OP.HTTP_SERVER,
name=_DEFAULT_TRANSACTION_NAME,
source=TransactionSource.ROUTE,
origin=self.span_origin,
)

span_ctx = sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"wsgi_environ": environ},
)

span_ctx = span_ctx or nullcontext()

with span_ctx as span:
if isinstance(span, StreamedSpan):
with capture_internal_exceptions():
for attr, value in _get_request_attributes(
environ, self.use_x_forwarded_for
).items():
span.set_attribute(attr, value)

transaction_context = (
sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"wsgi_environ": environ},
)
if transaction is not None
else nullcontext()
)
with transaction_context:
try:
response = self.app(
environ,
partial(
_sentry_start_response, start_response, transaction
),
partial(_sentry_start_response, start_response, span),
)
except BaseException:
reraise(*_capture_exception())
finally:
if isinstance(span, StreamedSpan):
already_set = (
span.name != _DEFAULT_TRANSACTION_NAME
and span.get_attributes().get("sentry.span.source")
in [
SegmentSource.COMPONENT.value,
SegmentSource.ROUTE.value,
SegmentSource.CUSTOM.value,
]
)
if not already_set:
with capture_internal_exceptions():
span.name = _DEFAULT_TRANSACTION_NAME
span.set_attribute(
"sentry.span.source",
SegmentSource.ROUTE.value,
)
finally:
_wsgi_middleware_applied.set(False)

Expand Down Expand Up @@ -167,15 +222,19 @@ def __call__(

def _sentry_start_response(
old_start_response: "StartResponse",
transaction: "Optional[Transaction]",
span: "Optional[Union[Span, StreamedSpan]]",
status: str,
response_headers: "WsgiResponseHeaders",
exc_info: "Optional[WsgiExcInfo]" = None,
) -> "WsgiResponseIter": # type: ignore[type-var]
with capture_internal_exceptions():
status_int = int(status.split(" ", 1)[0])
if transaction is not None:
transaction.set_http_status(status_int)
if span is not None:
if isinstance(span, StreamedSpan):
span.status = "error" if status_int >= 400 else "ok"
span.set_attribute("http.response.status_code", status_int)
else:
span.set_http_status(status_int)

if exc_info is None:
# The Django Rest Framework WSGI test client, and likely other
Expand Down Expand Up @@ -326,3 +385,50 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
return event

return event_processor


def _get_request_attributes(
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 essentially a port of the event processor

environ: "Dict[str, str]",
use_x_forwarded_for: bool = False,
) -> "Dict[str, Any]":
"""
Return span attributes related to the HTTP request from the WSGI environ.
"""
attributes: "dict[str, Any]" = {}

method = environ.get("REQUEST_METHOD")
if method:
attributes["http.request.method"] = method.upper()

headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False)
for header, value in headers.items():
attributes[f"http.request.header.{header.lower()}"] = value

query_string = environ.get("QUERY_STRING")
if query_string:
attributes["http.query"] = query_string

attributes["url.full"] = get_request_url(environ, use_x_forwarded_for)

url_scheme = environ.get("wsgi.url_scheme")
if url_scheme:
attributes["network.protocol.name"] = url_scheme

server_name = environ.get("SERVER_NAME")
if server_name:
attributes["server.address"] = server_name

server_port = environ.get("SERVER_PORT")
if server_port:
try:
attributes["server.port"] = int(server_port)
except ValueError:
pass

if should_send_default_pii():
client_ip = get_client_ip(environ)
if client_ip:
attributes["client.address"] = client_ip
attributes["user.ip_address"] = client_ip

return attributes
2 changes: 1 addition & 1 deletion sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ def get_active_propagation_context(self) -> "PropagationContext":
def set_custom_sampling_context(
self, custom_sampling_context: "dict[str, Any]"
) -> None:
self.get_active_propagation_context()._set_custom_sampling_context(
self.get_current_scope().get_active_propagation_context()._set_custom_sampling_context(
Copy link
Copy Markdown
Contributor Author

@sentrivana sentrivana Apr 14, 2026

Choose a reason for hiding this comment

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

This change was necessary as we're now always starting spans, regardless of type (segment or child), on the current scope instead of the isolation scope, so we also need to use the propagation context from there.

custom_sampling_context
)

Expand Down
Loading
Loading