11import sys
22from functools import partial
3+ from threading import Timer
34
45import sentry_sdk
56from sentry_sdk ._werkzeug import get_host , _get_headers
67from sentry_sdk .api import continue_trace
78from sentry_sdk .consts import OP
8- from sentry_sdk .scope import should_send_default_pii
9+ from sentry_sdk .scope import should_send_default_pii , use_isolation_scope , use_scope
910from sentry_sdk .integrations ._wsgi_common import (
1011 DEFAULT_HTTP_METHODS_TO_CAPTURE ,
1112 _filter_headers ,
12- nullcontext ,
1313)
1414from sentry_sdk .sessions import track_session
15- from sentry_sdk .scope import use_isolation_scope
1615from sentry_sdk .tracing import Transaction , TRANSACTION_SOURCE_ROUTE
16+ from sentry_sdk .tracing_utils import finish_running_transaction
1717from sentry_sdk .utils import (
1818 ContextVar ,
1919 capture_internal_exceptions ,
@@ -46,6 +46,9 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore
4646 pass
4747
4848
49+ MAX_TRANSACTION_DURATION_SECONDS = 5 * 60
50+
51+
4952_wsgi_middleware_applied = ContextVar ("sentry_wsgi_middleware_applied" )
5053
5154
@@ -98,6 +101,7 @@ def __call__(self, environ, start_response):
98101 _wsgi_middleware_applied .set (True )
99102 try :
100103 with sentry_sdk .isolation_scope () as scope :
104+ current_scope = sentry_sdk .get_current_scope ()
101105 with track_session (scope , session_mode = "request" ):
102106 with capture_internal_exceptions ():
103107 scope .clear_breadcrumbs ()
@@ -109,6 +113,7 @@ def __call__(self, environ, start_response):
109113 )
110114
111115 method = environ .get ("REQUEST_METHOD" , "" ).upper ()
116+
112117 transaction = None
113118 if method in self .http_methods_to_capture :
114119 transaction = continue_trace (
@@ -119,27 +124,43 @@ def __call__(self, environ, start_response):
119124 origin = self .span_origin ,
120125 )
121126
122- with (
127+ timer = None
128+ if transaction is not None :
123129 sentry_sdk .start_transaction (
124130 transaction ,
125131 custom_sampling_context = {"wsgi_environ" : environ },
132+ ).__enter__ ()
133+ timer = Timer (
134+ MAX_TRANSACTION_DURATION_SECONDS ,
135+ _finish_long_running_transaction ,
136+ args = (current_scope , scope ),
126137 )
127- if transaction is not None
128- else nullcontext ()
129- ):
130- try :
131- response = self .app (
132- environ ,
133- partial (
134- _sentry_start_response , start_response , transaction
135- ),
136- )
137- except BaseException :
138- reraise (* _capture_exception ())
138+ timer .start ()
139+
140+ try :
141+ response = self .app (
142+ environ ,
143+ partial (
144+ _sentry_start_response ,
145+ start_response ,
146+ transaction ,
147+ ),
148+ )
149+ except BaseException :
150+ exc_info = sys .exc_info ()
151+ _capture_exception (exc_info )
152+ finish_running_transaction (current_scope , exc_info , timer )
153+ reraise (* exc_info )
154+
139155 finally :
140156 _wsgi_middleware_applied .set (False )
141157
142- return _ScopedResponse (scope , response )
158+ return _ScopedResponse (
159+ response = response ,
160+ current_scope = current_scope ,
161+ isolation_scope = scope ,
162+ timer = timer ,
163+ )
143164
144165
145166def _sentry_start_response ( # type: ignore
@@ -201,13 +222,13 @@ def get_client_ip(environ):
201222 return environ .get ("REMOTE_ADDR" )
202223
203224
204- def _capture_exception ():
205- # type: () -> ExcInfo
225+ def _capture_exception (exc_info = None ):
226+ # type: (Optional[ExcInfo] ) -> ExcInfo
206227 """
207228 Captures the current exception and sends it to Sentry.
208229 Returns the ExcInfo tuple to it can be reraised afterwards.
209230 """
210- exc_info = sys .exc_info ()
231+ exc_info = exc_info or sys .exc_info ()
211232 e = exc_info [1 ]
212233
213234 # SystemExit(0) is the only uncaught exception that is expected behavior
@@ -225,7 +246,7 @@ def _capture_exception():
225246
226247class _ScopedResponse :
227248 """
228- Users a separate scope for each response chunk.
249+ Use separate scopes for each response chunk.
229250
230251 This will make WSGI apps more tolerant against:
231252 - WSGI servers streaming responses from a different thread/from
@@ -234,37 +255,54 @@ class _ScopedResponse:
234255 - WSGI servers streaming responses interleaved from the same thread
235256 """
236257
237- __slots__ = ("_response" , "_scope " )
258+ __slots__ = ("_response" , "_current_scope" , "_isolation_scope" , "_timer " )
238259
239- def __init__ (self , scope , response ):
240- # type: (sentry_sdk.scope.Scope, Iterator[bytes]) -> None
241- self ._scope = scope
260+ def __init__ (
261+ self ,
262+ response , # type: Iterator[bytes]
263+ current_scope , # type: sentry_sdk.scope.Scope
264+ isolation_scope , # type: sentry_sdk.scope.Scope
265+ timer = None , # type: Optional[Timer]
266+ ):
267+ # type: (...) -> None
242268 self ._response = response
269+ self ._current_scope = current_scope
270+ self ._isolation_scope = isolation_scope
271+ self ._timer = timer
243272
244273 def __iter__ (self ):
245274 # type: () -> Iterator[bytes]
246275 iterator = iter (self ._response )
247276
248- while True :
249- with use_isolation_scope (self ._scope ):
250- try :
251- chunk = next (iterator )
252- except StopIteration :
253- break
254- except BaseException :
255- reraise (* _capture_exception ())
277+ try :
278+ while True :
279+ with use_isolation_scope (self ._isolation_scope ):
280+ with use_scope (self ._current_scope ):
281+ try :
282+ chunk = next (iterator )
283+ except StopIteration :
284+ break
285+ except BaseException :
286+ reraise (* _capture_exception ())
287+
288+ yield chunk
256289
257- yield chunk
290+ finally :
291+ with use_isolation_scope (self ._isolation_scope ):
292+ with use_scope (self ._current_scope ):
293+ finish_running_transaction (timer = self ._timer )
258294
259295 def close (self ):
260296 # type: () -> None
261- with use_isolation_scope (self ._scope ):
262- try :
263- self ._response .close () # type: ignore
264- except AttributeError :
265- pass
266- except BaseException :
267- reraise (* _capture_exception ())
297+ with use_isolation_scope (self ._isolation_scope ):
298+ with use_scope (self ._current_scope ):
299+ try :
300+ finish_running_transaction (timer = self ._timer )
301+ self ._response .close () # type: ignore
302+ except AttributeError :
303+ pass
304+ except BaseException :
305+ reraise (* _capture_exception ())
268306
269307
270308def _make_wsgi_event_processor (environ , use_x_forwarded_for ):
@@ -308,3 +346,18 @@ def event_processor(event, hint):
308346 return event
309347
310348 return event_processor
349+
350+
351+ def _finish_long_running_transaction (current_scope , isolation_scope ):
352+ # type: (sentry_sdk.scope.Scope, sentry_sdk.scope.Scope) -> None
353+ """
354+ Make sure we don't keep transactions open for too long.
355+ Triggered after MAX_TRANSACTION_DURATION_SECONDS have passed.
356+ """
357+ try :
358+ with use_isolation_scope (isolation_scope ):
359+ with use_scope (current_scope ):
360+ finish_running_transaction ()
361+ except AttributeError :
362+ # transaction is not there anymore
363+ pass
0 commit comments