1313)
1414from sentry_sdk .scope import should_send_default_pii , use_isolation_scope
1515from sentry_sdk .sessions import track_session
16- from sentry_sdk .tracing import Transaction , TransactionSource
16+ from sentry_sdk .traces import StreamedSpan , SegmentSource
17+ from sentry_sdk .tracing import Span , TransactionSource
18+ from sentry_sdk .tracing_utils import has_span_streaming_enabled
1719from sentry_sdk .utils import (
1820 ContextVar ,
1921 capture_internal_exceptions ,
2224)
2325
2426if TYPE_CHECKING :
25- from typing import Any , Callable , Dict , Iterator , Optional , Protocol , Tuple , TypeVar
27+ from typing import (
28+ Any ,
29+ Callable ,
30+ ContextManager ,
31+ Dict ,
32+ Iterator ,
33+ Optional ,
34+ Protocol ,
35+ Tuple ,
36+ TypeVar ,
37+ Union ,
38+ )
2639
2740 from sentry_sdk ._types import Event , EventProcessor
2841 from sentry_sdk .utils import ExcInfo
@@ -42,6 +55,7 @@ def __call__(
4255
4356
4457_wsgi_middleware_applied = ContextVar ("sentry_wsgi_middleware_applied" )
58+ _DEFAULT_TRANSACTION_NAME = "generic WSGI request"
4559
4660
4761def wsgi_decoding_dance (s : str , charset : str = "utf-8" , errors : str = "replace" ) -> str :
@@ -94,6 +108,9 @@ def __call__(
94108 if _wsgi_middleware_applied .get (False ):
95109 return self .app (environ , start_response )
96110
111+ client = sentry_sdk .get_client ()
112+ span_streaming = has_span_streaming_enabled (client .options )
113+
97114 _wsgi_middleware_applied .set (True )
98115 try :
99116 with sentry_sdk .isolation_scope () as scope :
@@ -108,34 +125,72 @@ def __call__(
108125 )
109126
110127 method = environ .get ("REQUEST_METHOD" , "" ).upper ()
111- transaction = None
128+
129+ span_ctx : "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None
112130 if method in self .http_methods_to_capture :
113- transaction = continue_trace (
114- environ ,
115- op = OP .HTTP_SERVER ,
116- name = "generic WSGI request" ,
117- source = TransactionSource .ROUTE ,
118- origin = self .span_origin ,
119- )
131+ if span_streaming :
132+ sentry_sdk .traces .continue_trace (
133+ dict (_get_headers (environ ))
134+ )
135+ scope .set_custom_sampling_context ({"wsgi_environ" : environ })
136+
137+ span_ctx = sentry_sdk .traces .start_span (
138+ name = _DEFAULT_TRANSACTION_NAME ,
139+ attributes = {
140+ "sentry.span.source" : SegmentSource .ROUTE ,
141+ "sentry.origin" : self .span_origin ,
142+ "sentry.op" : OP .HTTP_SERVER ,
143+ },
144+ )
145+ else :
146+ transaction = continue_trace (
147+ environ ,
148+ op = OP .HTTP_SERVER ,
149+ name = _DEFAULT_TRANSACTION_NAME ,
150+ source = TransactionSource .ROUTE ,
151+ origin = self .span_origin ,
152+ )
153+
154+ span_ctx = sentry_sdk .start_transaction (
155+ transaction ,
156+ custom_sampling_context = {"wsgi_environ" : environ },
157+ )
158+
159+ span_ctx = span_ctx or nullcontext ()
160+
161+ with span_ctx as span :
162+ if isinstance (span , StreamedSpan ):
163+ with capture_internal_exceptions ():
164+ for attr , value in _get_request_attributes (
165+ environ , self .use_x_forwarded_for
166+ ).items ():
167+ span .set_attribute (attr , value )
120168
121- transaction_context = (
122- sentry_sdk .start_transaction (
123- transaction ,
124- custom_sampling_context = {"wsgi_environ" : environ },
125- )
126- if transaction is not None
127- else nullcontext ()
128- )
129- with transaction_context :
130169 try :
131170 response = self .app (
132171 environ ,
133- partial (
134- _sentry_start_response , start_response , transaction
135- ),
172+ partial (_sentry_start_response , start_response , span ),
136173 )
137174 except BaseException :
138175 reraise (* _capture_exception ())
176+ finally :
177+ if isinstance (span , StreamedSpan ):
178+ already_set = (
179+ span .name != _DEFAULT_TRANSACTION_NAME
180+ and span .get_attributes ().get ("sentry.span.source" )
181+ in [
182+ SegmentSource .COMPONENT .value ,
183+ SegmentSource .ROUTE .value ,
184+ SegmentSource .CUSTOM .value ,
185+ ]
186+ )
187+ if not already_set :
188+ with capture_internal_exceptions ():
189+ span .name = _DEFAULT_TRANSACTION_NAME
190+ span .set_attribute (
191+ "sentry.span.source" ,
192+ SegmentSource .ROUTE .value ,
193+ )
139194 finally :
140195 _wsgi_middleware_applied .set (False )
141196
@@ -167,15 +222,19 @@ def __call__(
167222
168223def _sentry_start_response (
169224 old_start_response : "StartResponse" ,
170- transaction : "Optional[Transaction ]" ,
225+ span : "Optional[Union[Span, StreamedSpan] ]" ,
171226 status : str ,
172227 response_headers : "WsgiResponseHeaders" ,
173228 exc_info : "Optional[WsgiExcInfo]" = None ,
174229) -> "WsgiResponseIter" : # type: ignore[type-var]
175230 with capture_internal_exceptions ():
176231 status_int = int (status .split (" " , 1 )[0 ])
177- if transaction is not None :
178- transaction .set_http_status (status_int )
232+ if span is not None :
233+ if isinstance (span , StreamedSpan ):
234+ span .status = "error" if status_int >= 400 else "ok"
235+ span .set_attribute ("http.response.status_code" , status_int )
236+ else :
237+ span .set_http_status (status_int )
179238
180239 if exc_info is None :
181240 # The Django Rest Framework WSGI test client, and likely other
@@ -326,3 +385,50 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
326385 return event
327386
328387 return event_processor
388+
389+
390+ def _get_request_attributes (
391+ environ : "Dict[str, str]" ,
392+ use_x_forwarded_for : bool = False ,
393+ ) -> "Dict[str, Any]" :
394+ """
395+ Return span attributes related to the HTTP request from the WSGI environ.
396+ """
397+ attributes : "dict[str, Any]" = {}
398+
399+ method = environ .get ("REQUEST_METHOD" )
400+ if method :
401+ attributes ["http.request.method" ] = method .upper ()
402+
403+ headers = _filter_headers (dict (_get_headers (environ )), use_annotated_value = False )
404+ for header , value in headers .items ():
405+ attributes [f"http.request.header.{ header .lower ()} " ] = value
406+
407+ query_string = environ .get ("QUERY_STRING" )
408+ if query_string :
409+ attributes ["http.query" ] = query_string
410+
411+ attributes ["url.full" ] = get_request_url (environ , use_x_forwarded_for )
412+
413+ url_scheme = environ .get ("wsgi.url_scheme" )
414+ if url_scheme :
415+ attributes ["network.protocol.name" ] = url_scheme
416+
417+ server_name = environ .get ("SERVER_NAME" )
418+ if server_name :
419+ attributes ["server.address" ] = server_name
420+
421+ server_port = environ .get ("SERVER_PORT" )
422+ if server_port :
423+ try :
424+ attributes ["server.port" ] = int (server_port )
425+ except ValueError :
426+ pass
427+
428+ if should_send_default_pii ():
429+ client_ip = get_client_ip (environ )
430+ if client_ip :
431+ attributes ["client.address" ] = client_ip
432+ attributes ["user.ip_address" ] = client_ip
433+
434+ return attributes
0 commit comments