diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 6a7c543c8c..f8d96a4507 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -1,6 +1,5 @@ import sys from copy import deepcopy -from functools import wraps from typing import TYPE_CHECKING import sentry_sdk @@ -8,7 +7,6 @@ from sentry_sdk.integrations import DidNotEnable from sentry_sdk.traces import StreamedSpan, get_current_span from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource -from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import has_data_collection_enabled, transaction_from_function if TYPE_CHECKING: @@ -21,6 +19,7 @@ StarletteIntegration, StarletteRequestExtractor, _get_cached_request_body_attribute, + _wrap_sync_handler, ) except DidNotEnable: raise DidNotEnable("Starlette is not installed") @@ -184,38 +183,8 @@ def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any": dependant and dependant.call is not None and not iscoroutinefunction(dependant.call) - # FastAPI >= 0.137 calls get_request_handler() on every request - # (router-tree traversal) rather than once at registration. Guard - # against accumulating _sentry_call wrappers on the shared - # dependant object, which would cause a RecursionError after ~987 - # requests as the call chain grows past Python's recursion limit. - and not getattr(dependant.call, "_sentry_is_patched", False) ): - old_call = dependant.call - - @wraps(old_call) - def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": - current_scope = sentry_sdk.get_current_scope() - - client = sentry_sdk.get_client() - if has_span_streaming_enabled(client.options): - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - segment = current_span._segment - segment._update_active_thread() - - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() - - return old_call(*args, **kwargs) - - _sentry_call._sentry_is_patched = True # type: ignore[attr-defined] - dependant.call = _sentry_call + dependant.call = _wrap_sync_handler(dependant.call) old_app = old_get_request_handler(*args, **kwargs) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index a9ed8f2f73..f6988d096d 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -630,6 +630,37 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": ) +def _update_active_thread() -> None: + client = sentry_sdk.get_client() + current_scope = sentry_sdk.get_current_scope() + + span_streaming = has_span_streaming_enabled(client.options) + if span_streaming: + current_span = current_scope.streamed_span + + if type(current_span) is StreamedSpan: + current_span._segment._update_active_thread() + elif current_scope.transaction is not None: + current_scope.transaction.update_active_thread() + + sentry_scope = sentry_sdk.get_isolation_scope() + if sentry_scope.profile is not None: + sentry_scope.profile.update_active_thread_id() + + +def _wrap_sync_handler(handler: "Callable[..., Any]") -> "Callable[..., Any]": + if getattr(handler, "_sentry_patched", False): + return handler + + @functools.wraps(handler) + def _sentry_sync_handler(*args: "Any", **kwargs: "Any") -> "Any": + _update_active_thread() + return handler(*args, **kwargs) + + _sentry_sync_handler._sentry_patched = True # type: ignore[attr-defined] + return _sentry_sync_handler + + def patch_request_response() -> None: old_request_response = starlette.routing.request_response @@ -654,20 +685,9 @@ def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any": if integration is None: return old_func(*args, **kwargs) + _update_active_thread() current_scope = sentry_sdk.get_current_scope() - - span_streaming = has_span_streaming_enabled(client.options) - if span_streaming: - current_span = current_scope.streamed_span - - if type(current_span) is StreamedSpan: - current_span._segment._update_active_thread() - elif current_scope.transaction is not None: - current_scope.transaction.update_active_thread() - sentry_scope = sentry_sdk.get_isolation_scope() - if sentry_scope.profile is not None: - sentry_scope.profile.update_active_thread_id() request = args[0]