22import warnings
33from functools import wraps
44from threading import Thread , current_thread
5+ from concurrent .futures import ThreadPoolExecutor , Future
56
67import sentry_sdk
78from sentry_sdk .integrations import Integration
2425 from sentry_sdk ._types import ExcInfo
2526
2627 F = TypeVar ("F" , bound = Callable [..., Any ])
28+ T = TypeVar ("T" , bound = Any )
2729
2830
2931class ThreadingIntegration (Integration ):
@@ -59,6 +61,15 @@ def setup_once():
5961 django_version = None
6062 channels_version = None
6163
64+ is_async_emulated_with_threads = (
65+ sys .version_info < (3 , 9 )
66+ and channels_version is not None
67+ and channels_version < "4.0.0"
68+ and django_version is not None
69+ and django_version >= (3 , 0 )
70+ and django_version < (4 , 0 )
71+ )
72+
6273 @wraps (old_start )
6374 def sentry_start (self , * a , ** kw ):
6475 # type: (Thread, *Any, **Any) -> Any
@@ -67,14 +78,7 @@ def sentry_start(self, *a, **kw):
6778 return old_start (self , * a , ** kw )
6879
6980 if integration .propagate_scope :
70- if (
71- sys .version_info < (3 , 9 )
72- and channels_version is not None
73- and channels_version < "4.0.0"
74- and django_version is not None
75- and django_version >= (3 , 0 )
76- and django_version < (4 , 0 )
77- ):
81+ if is_async_emulated_with_threads :
7882 warnings .warn (
7983 "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. "
8084 "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) "
@@ -109,6 +113,9 @@ def sentry_start(self, *a, **kw):
109113 return old_start (self , * a , ** kw )
110114
111115 Thread .start = sentry_start # type: ignore
116+ ThreadPoolExecutor .submit = _wrap_threadpool_executor_submit ( # type: ignore
117+ ThreadPoolExecutor .submit , is_async_emulated_with_threads
118+ )
112119
113120
114121def _wrap_run (isolation_scope_to_use , current_scope_to_use , old_run_func ):
@@ -134,6 +141,43 @@ def _run_old_run_func():
134141 return run # type: ignore
135142
136143
144+ def _wrap_threadpool_executor_submit (func , is_async_emulated_with_threads ):
145+ # type: (Callable[..., Future[T]], bool) -> Callable[..., Future[T]]
146+ """
147+ Wrap submit call to propagate scopes on task submission.
148+ """
149+
150+ @wraps (func )
151+ def sentry_submit (self , fn , * args , ** kwargs ):
152+ # type: (ThreadPoolExecutor, Callable[..., T], *Any, **Any) -> Future[T]
153+ integration = sentry_sdk .get_client ().get_integration (ThreadingIntegration )
154+ if integration is None :
155+ return func (self , fn , * args , ** kwargs )
156+
157+ if integration .propagate_scope and is_async_emulated_with_threads :
158+ isolation_scope = sentry_sdk .get_isolation_scope ()
159+ current_scope = sentry_sdk .get_current_scope ()
160+ elif integration .propagate_scope :
161+ isolation_scope = sentry_sdk .get_isolation_scope ().fork ()
162+ current_scope = sentry_sdk .get_current_scope ().fork ()
163+ else :
164+ isolation_scope = None
165+ current_scope = None
166+
167+ def wrapped_fn (* args , ** kwargs ):
168+ # type: (*Any, **Any) -> Any
169+ if isolation_scope is not None and current_scope is not None :
170+ with use_isolation_scope (isolation_scope ):
171+ with use_scope (current_scope ):
172+ return fn (* args , ** kwargs )
173+
174+ return fn (* args , ** kwargs )
175+
176+ return func (self , wrapped_fn , * args , ** kwargs )
177+
178+ return sentry_submit
179+
180+
137181def _capture_exception ():
138182 # type: () -> ExcInfo
139183 exc_info = sys .exc_info ()
0 commit comments