1
1
import sys
2
2
from functools import partial
3
+ from threading import Timer
3
4
4
5
import sentry_sdk
5
6
from sentry_sdk ._werkzeug import get_host , _get_headers
6
7
from sentry_sdk .api import continue_trace
7
8
from 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
9
10
from sentry_sdk .integrations ._wsgi_common import (
10
11
DEFAULT_HTTP_METHODS_TO_CAPTURE ,
11
12
_filter_headers ,
12
- nullcontext ,
13
13
)
14
14
from sentry_sdk .sessions import track_session
15
- from sentry_sdk .scope import use_isolation_scope
16
15
from sentry_sdk .tracing import Transaction , TRANSACTION_SOURCE_ROUTE
16
+ from sentry_sdk .tracing_utils import finish_running_transaction
17
17
from sentry_sdk .utils import (
18
18
ContextVar ,
19
19
capture_internal_exceptions ,
@@ -46,6 +46,9 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore
46
46
pass
47
47
48
48
49
+ MAX_TRANSACTION_DURATION_SECONDS = 5 * 60
50
+
51
+
49
52
_wsgi_middleware_applied = ContextVar ("sentry_wsgi_middleware_applied" )
50
53
51
54
@@ -98,6 +101,7 @@ def __call__(self, environ, start_response):
98
101
_wsgi_middleware_applied .set (True )
99
102
try :
100
103
with sentry_sdk .isolation_scope () as scope :
104
+ current_scope = sentry_sdk .get_current_scope ()
101
105
with track_session (scope , session_mode = "request" ):
102
106
with capture_internal_exceptions ():
103
107
scope .clear_breadcrumbs ()
@@ -109,6 +113,7 @@ def __call__(self, environ, start_response):
109
113
)
110
114
111
115
method = environ .get ("REQUEST_METHOD" , "" ).upper ()
116
+
112
117
transaction = None
113
118
if method in self .http_methods_to_capture :
114
119
transaction = continue_trace (
@@ -119,27 +124,43 @@ def __call__(self, environ, start_response):
119
124
origin = self .span_origin ,
120
125
)
121
126
122
- with (
127
+ timer = None
128
+ if transaction is not None :
123
129
sentry_sdk .start_transaction (
124
130
transaction ,
125
131
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 ),
126
137
)
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
+
139
155
finally :
140
156
_wsgi_middleware_applied .set (False )
141
157
142
- return _ScopedResponse (scope , response )
158
+ return _ScopedResponse (
159
+ response = response ,
160
+ current_scope = current_scope ,
161
+ isolation_scope = scope ,
162
+ timer = timer ,
163
+ )
143
164
144
165
145
166
def _sentry_start_response ( # type: ignore
@@ -201,13 +222,13 @@ def get_client_ip(environ):
201
222
return environ .get ("REMOTE_ADDR" )
202
223
203
224
204
- def _capture_exception ():
205
- # type: () -> ExcInfo
225
+ def _capture_exception (exc_info = None ):
226
+ # type: (Optional[ExcInfo] ) -> ExcInfo
206
227
"""
207
228
Captures the current exception and sends it to Sentry.
208
229
Returns the ExcInfo tuple to it can be reraised afterwards.
209
230
"""
210
- exc_info = sys .exc_info ()
231
+ exc_info = exc_info or sys .exc_info ()
211
232
e = exc_info [1 ]
212
233
213
234
# SystemExit(0) is the only uncaught exception that is expected behavior
@@ -225,7 +246,7 @@ def _capture_exception():
225
246
226
247
class _ScopedResponse :
227
248
"""
228
- Users a separate scope for each response chunk.
249
+ Use separate scopes for each response chunk.
229
250
230
251
This will make WSGI apps more tolerant against:
231
252
- WSGI servers streaming responses from a different thread/from
@@ -234,37 +255,54 @@ class _ScopedResponse:
234
255
- WSGI servers streaming responses interleaved from the same thread
235
256
"""
236
257
237
- __slots__ = ("_response" , "_scope " )
258
+ __slots__ = ("_response" , "_current_scope" , "_isolation_scope" , "_timer " )
238
259
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
242
268
self ._response = response
269
+ self ._current_scope = current_scope
270
+ self ._isolation_scope = isolation_scope
271
+ self ._timer = timer
243
272
244
273
def __iter__ (self ):
245
274
# type: () -> Iterator[bytes]
246
275
iterator = iter (self ._response )
247
276
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
256
289
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 )
258
294
259
295
def close (self ):
260
296
# 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 ())
268
306
269
307
270
308
def _make_wsgi_event_processor (environ , use_x_forwarded_for ):
@@ -308,3 +346,18 @@ def event_processor(event, hint):
308
346
return event
309
347
310
348
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