11import sys
2- from contextlib import nullcontext
32from functools import partial
43
54import sentry_sdk
1211)
1312from sentry_sdk .sessions import track_session
1413from sentry_sdk .tracing import Transaction , TRANSACTION_SOURCE_ROUTE
14+ from sentry_sdk .tracing_utils import finish_running_transaction
1515from sentry_sdk .utils import (
1616 ContextVar ,
1717 capture_internal_exceptions ,
@@ -44,6 +44,9 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore
4444 pass
4545
4646
47+ MAX_TRANSACTION_DURATION_SECONDS = 5 * 60
48+
49+
4750_wsgi_middleware_applied = ContextVar ("sentry_wsgi_middleware_applied" )
4851
4952DEFAULT_TRANSACTION_NAME = "generic WSGI request"
@@ -110,6 +113,7 @@ def __call__(self, environ, start_response):
110113 scope .set_transaction_name (
111114 DEFAULT_TRANSACTION_NAME , source = TRANSACTION_SOURCE_ROUTE
112115 )
116+ current_scope = sentry_sdk .get_current_scope ()
113117
114118 with track_session (scope , session_mode = "request" ):
115119 with capture_internal_exceptions ():
@@ -120,11 +124,14 @@ def __call__(self, environ, start_response):
120124 environ , self .use_x_forwarded_for
121125 )
122126 )
127+
123128 method = environ .get ("REQUEST_METHOD" , "" ).upper ()
124129 should_trace = method in self .http_methods_to_capture
130+ transaction = None
131+
125132 with sentry_sdk .continue_trace (environ ):
126- with (
127- sentry_sdk .start_span (
133+ if should_trace :
134+ transaction = sentry_sdk .start_span (
128135 op = OP .HTTP_SERVER ,
129136 name = DEFAULT_TRANSACTION_NAME ,
130137 source = TRANSACTION_SOURCE_ROUTE ,
@@ -133,9 +140,9 @@ def __call__(self, environ, start_response):
133140 environ , self .use_x_forwarded_for
134141 ),
135142 )
136- if should_trace
137- else nullcontext ()
138- ) as transaction :
143+ transaction . __enter__ ()
144+ current_scope = transaction . scope
145+
139146 try :
140147 response = self .app (
141148 environ ,
@@ -146,12 +153,20 @@ def __call__(self, environ, start_response):
146153 ),
147154 )
148155 except BaseException :
149- reraise (* _capture_exception ())
156+ exc_info = sys .exc_info ()
157+ _capture_exception (exc_info )
158+ finish_running_transaction (transaction , exc_info )
159+ reraise (* exc_info )
150160
151161 finally :
152162 _wsgi_middleware_applied .set (False )
153163
154- return _ScopedResponse (scope , response )
164+ return _ScopedResponse (
165+ response = response ,
166+ current_scope = current_scope ,
167+ isolation_scope = scope ,
168+ transaction = transaction ,
169+ )
155170
156171
157172def _sentry_start_response ( # type: ignore
@@ -213,13 +228,13 @@ def get_client_ip(environ):
213228 return environ .get ("REMOTE_ADDR" )
214229
215230
216- def _capture_exception ():
217- # type: () -> ExcInfo
231+ def _capture_exception (exc_info = None ):
232+ # type: (Optional[ExcInfo] ) -> ExcInfo
218233 """
219234 Captures the current exception and sends it to Sentry.
220235 Returns the ExcInfo tuple to it can be reraised afterwards.
221236 """
222- exc_info = sys .exc_info ()
237+ exc_info = exc_info or sys .exc_info ()
223238 e = exc_info [1 ]
224239
225240 # SystemExit(0) is the only uncaught exception that is expected behavior
@@ -237,7 +252,7 @@ def _capture_exception():
237252
238253class _ScopedResponse :
239254 """
240- Users a separate scope for each response chunk.
255+ Use separate scopes for each response chunk.
241256
242257 This will make WSGI apps more tolerant against:
243258 - WSGI servers streaming responses from a different thread/from
@@ -246,37 +261,54 @@ class _ScopedResponse:
246261 - WSGI servers streaming responses interleaved from the same thread
247262 """
248263
249- __slots__ = ("_response" , "_scope " )
264+ __slots__ = ("_response" , "_current_scope" , "_isolation_scope" , "_transaction " )
250265
251- def __init__ (self , scope , response ):
252- # type: (sentry_sdk.scope.Scope, Iterator[bytes]) -> None
253- self ._scope = scope
266+ def __init__ (
267+ self ,
268+ response , # type: Iterator[bytes]
269+ current_scope , # type: sentry_sdk.scope.Scope
270+ isolation_scope , # type: sentry_sdk.scope.Scope
271+ transaction = None , # type: Optional[Transaction]
272+ ):
273+ # type: (...) -> None
254274 self ._response = response
275+ self ._current_scope = current_scope
276+ self ._isolation_scope = isolation_scope
277+ self ._transaction = transaction
255278
256279 def __iter__ (self ):
257280 # type: () -> Iterator[bytes]
258281 iterator = iter (self ._response )
259282
260- while True :
261- with sentry_sdk .use_isolation_scope (self ._scope ):
262- try :
263- chunk = next (iterator )
264- except StopIteration :
265- break
266- except BaseException :
267- reraise (* _capture_exception ())
283+ try :
284+ while True :
285+ with sentry_sdk .use_isolation_scope (self ._isolation_scope ):
286+ with sentry_sdk .use_scope (self ._current_scope ):
287+ try :
288+ chunk = next (iterator )
289+ except StopIteration :
290+ break
291+ except BaseException :
292+ reraise (* _capture_exception ())
293+
294+ yield chunk
268295
269- yield chunk
296+ finally :
297+ with sentry_sdk .use_isolation_scope (self ._isolation_scope ):
298+ with sentry_sdk .use_scope (self ._current_scope ):
299+ finish_running_transaction (transaction = self ._transaction )
270300
271301 def close (self ):
272302 # type: () -> None
273- with sentry_sdk .use_isolation_scope (self ._scope ):
274- try :
275- self ._response .close () # type: ignore
276- except AttributeError :
277- pass
278- except BaseException :
279- reraise (* _capture_exception ())
303+ with sentry_sdk .use_isolation_scope (self ._isolation_scope ):
304+ with sentry_sdk .use_scope (self ._current_scope ):
305+ try :
306+ finish_running_transaction (transaction = self ._transaction )
307+ self ._response .close () # type: ignore
308+ except AttributeError :
309+ pass
310+ except BaseException :
311+ reraise (* _capture_exception ())
280312
281313
282314def _make_wsgi_event_processor (environ , use_x_forwarded_for ):
0 commit comments