Skip to content

Commit 9d01b5a

Browse files
authored
Merge branch 'master' into antonpirker/github-actions-automations
2 parents 7e56a1e + da20623 commit 9d01b5a

File tree

6 files changed

+270
-73
lines changed

6 files changed

+270
-73
lines changed

sentry_sdk/integrations/wsgi.py

Lines changed: 94 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import sys
22
from functools import partial
3+
from threading import Timer
34

45
import sentry_sdk
56
from sentry_sdk._werkzeug import get_host, _get_headers
67
from sentry_sdk.api import continue_trace
78
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
910
from sentry_sdk.integrations._wsgi_common import (
1011
DEFAULT_HTTP_METHODS_TO_CAPTURE,
1112
_filter_headers,
12-
nullcontext,
1313
)
1414
from sentry_sdk.sessions import track_session
15-
from sentry_sdk.scope import use_isolation_scope
1615
from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_ROUTE
16+
from sentry_sdk.tracing_utils import finish_running_transaction
1717
from 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

145166
def _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

226247
class _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

270308
def _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

sentry_sdk/tracing_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636

3737
from types import FrameType
3838

39+
from sentry_sdk._types import ExcInfo
40+
from threading import Timer
41+
3942

4043
SENTRY_TRACE_REGEX = re.compile(
4144
"^[ \t]*" # whitespace
@@ -739,3 +742,18 @@ def get_current_span(scope=None):
739742

740743
if TYPE_CHECKING:
741744
from sentry_sdk.tracing import Span
745+
746+
747+
def finish_running_transaction(scope=None, exc_info=None, timer=None):
748+
# type: (Optional[sentry_sdk.Scope], Optional[ExcInfo], Optional[Timer]) -> None
749+
if timer is not None:
750+
timer.cancel()
751+
752+
current_scope = scope or sentry_sdk.get_current_scope()
753+
if current_scope.transaction is not None and hasattr(
754+
current_scope.transaction, "_context_manager_state"
755+
):
756+
if exc_info is not None:
757+
current_scope.transaction.__exit__(*exc_info)
758+
else:
759+
current_scope.transaction.__exit__(None, None, None)

0 commit comments

Comments
 (0)