diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index da59ba6d7c..9b7d7e4fe4 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -124,6 +124,7 @@ def __call__(self, environ, start_response): origin=self.span_origin, ) + timer = None if transaction is not None: sentry_sdk.start_transaction( transaction, @@ -147,7 +148,7 @@ def __call__(self, environ, start_response): except BaseException: exc_info = sys.exc_info() _capture_exception(exc_info) - finish_running_transaction(current_scope, exc_info) + finish_running_transaction(current_scope, exc_info, timer) reraise(*exc_info) finally: @@ -157,6 +158,7 @@ def __call__(self, environ, start_response): response=response, current_scope=current_scope, isolation_scope=scope, + timer=timer, ) @@ -271,18 +273,20 @@ class _ScopedResponse: - WSGI servers streaming responses interleaved from the same thread """ - __slots__ = ("_response", "_current_scope", "_isolation_scope") + __slots__ = ("_response", "_current_scope", "_isolation_scope", "_timer") def __init__( self, response, # type: Iterator[bytes] current_scope, # type: sentry_sdk.scope.Scope isolation_scope, # type: sentry_sdk.scope.Scope + timer=None, # type: Optional[Timer] ): # type: (...) -> None self._response = response self._current_scope = current_scope self._isolation_scope = isolation_scope + self._timer = timer def __iter__(self): # type: () -> Iterator[bytes] @@ -304,14 +308,14 @@ def __iter__(self): finally: with use_isolation_scope(self._isolation_scope): with use_scope(self._current_scope): - finish_running_transaction() + finish_running_transaction(timer=self._timer) def close(self): # type: () -> None with use_isolation_scope(self._isolation_scope): with use_scope(self._current_scope): try: - finish_running_transaction() + finish_running_transaction(timer=self._timer) self._response.close() # type: ignore except AttributeError: pass diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 71ef710259..63f490b0bc 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -37,6 +37,7 @@ from types import FrameType from sentry_sdk._types import ExcInfo + from threading import Timer SENTRY_TRACE_REGEX = re.compile( @@ -743,12 +744,15 @@ def get_current_span(scope=None): from sentry_sdk.tracing import Span -def finish_running_transaction(scope=None, exc_info=None): - # type: (Optional[sentry_sdk.Scope], Optional[ExcInfo]) -> None +def finish_running_transaction(scope=None, exc_info=None, timer=None): + # type: (Optional[sentry_sdk.Scope], Optional[ExcInfo], Timer) -> None current_scope = scope or sentry_sdk.get_current_scope() if current_scope.transaction is not None and hasattr( current_scope.transaction, "_context_manager_state" ): + if timer is not None: + timer.cancel() + if exc_info is not None: current_scope.transaction.__exit__(*exc_info) else: diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 6c0ff44022..b7e1ac379d 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -537,3 +537,40 @@ def long_running_app(environ, start_response): assert ( transaction_duration <= new_max_duration * 1.2 ) # we allow 2% margin for processing the request + + +def test_long_running_transaction_timer_canceled(sentry_init, capture_events): + # we allow transactions to be 0.5 seconds as a maximum + new_max_duration = 0.5 + + with mock.patch.object( + sentry_sdk.integrations.wsgi, + "MAX_TRANSACTION_DURATION_SECONDS", + new_max_duration, + ): + with mock.patch( + "sentry_sdk.integrations.wsgi.finish_long_running_transaction" + ) as mock_finish: + + def generate_content(): + # This response will take 0.3 seconds to generate + for _ in range(3): + time.sleep(0.1) + yield "ok" + + def long_running_app(environ, start_response): + start_response("200 OK", []) + return generate_content() + + sentry_init(send_default_pii=True, traces_sample_rate=1.0) + app = SentryWsgiMiddleware(long_running_app) + + events = capture_events() + + client = Client(app) + response = client.get("/") + _ = response.get_data() + + (transaction,) = events + + mock_finish.assert_not_called()