sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +81 -19
  8. sentry_sdk/_types.py +311 -11
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +35 -28
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +20 -11
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +71 -60
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -6,35 +6,120 @@ Since this file contains `async def` it is conditionally imported in
6
6
  `django.core.handlers.asgi`.
7
7
  """
8
8
 
9
- from sentry_sdk import Hub
10
- from sentry_sdk._types import MYPY
9
+ import asyncio
10
+ import functools
11
+ import inspect
12
+
13
+ from django.core.handlers.wsgi import WSGIRequest
14
+
15
+ import sentry_sdk
16
+ from sentry_sdk.consts import OP
11
17
 
12
- from sentry_sdk.integrations.django import DjangoIntegration
13
18
  from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
19
+ from sentry_sdk.scope import should_send_default_pii
20
+ from sentry_sdk.utils import (
21
+ capture_internal_exceptions,
22
+ ensure_integration_enabled,
23
+ )
14
24
 
15
- if MYPY:
16
- from typing import Any
17
- from typing import Union
25
+ from typing import TYPE_CHECKING
18
26
 
27
+ if TYPE_CHECKING:
28
+ from typing import Any, Callable, Union, TypeVar
29
+
30
+ from django.core.handlers.asgi import ASGIRequest
19
31
  from django.http.response import HttpResponse
20
32
 
33
+ from sentry_sdk._types import Event, EventProcessor
34
+
35
+ _F = TypeVar("_F", bound=Callable[..., Any])
36
+
37
+
38
+ # Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
39
+ # inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
40
+ # The latter is replaced with the inspect.markcoroutinefunction decorator.
41
+ # Until 3.12 is the minimum supported Python version, provide a shim.
42
+ # This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py
43
+ if hasattr(inspect, "markcoroutinefunction"):
44
+ iscoroutinefunction = inspect.iscoroutinefunction
45
+ markcoroutinefunction = inspect.markcoroutinefunction
46
+ else:
47
+ iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]
48
+
49
+ def markcoroutinefunction(func: "_F") -> "_F":
50
+ func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
51
+ return func
52
+
53
+
54
+ def _make_asgi_request_event_processor(request):
55
+ # type: (ASGIRequest) -> EventProcessor
56
+ def asgi_request_event_processor(event, hint):
57
+ # type: (Event, dict[str, Any]) -> Event
58
+ # if the request is gone we are fine not logging the data from
59
+ # it. This might happen if the processor is pushed away to
60
+ # another thread.
61
+ from sentry_sdk.integrations.django import (
62
+ DjangoRequestExtractor,
63
+ _set_user_info,
64
+ )
65
+
66
+ if request is None:
67
+ return event
68
+
69
+ if type(request) == WSGIRequest:
70
+ return event
71
+
72
+ with capture_internal_exceptions():
73
+ DjangoRequestExtractor(request).extract_into_event(event)
74
+
75
+ if should_send_default_pii():
76
+ with capture_internal_exceptions():
77
+ _set_user_info(request, event)
78
+
79
+ return event
80
+
81
+ return asgi_request_event_processor
82
+
21
83
 
22
84
  def patch_django_asgi_handler_impl(cls):
23
85
  # type: (Any) -> None
86
+
87
+ from sentry_sdk.integrations.django import DjangoIntegration
88
+
24
89
  old_app = cls.__call__
25
90
 
26
91
  async def sentry_patched_asgi_handler(self, scope, receive, send):
27
92
  # type: (Any, Any, Any, Any) -> Any
28
- if Hub.current.get_integration(DjangoIntegration) is None:
93
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
94
+ if integration is None:
29
95
  return await old_app(self, scope, receive, send)
30
96
 
31
97
  middleware = SentryAsgiMiddleware(
32
- old_app.__get__(self, cls), unsafe_context_data=True
98
+ old_app.__get__(self, cls),
99
+ unsafe_context_data=True,
100
+ span_origin=DjangoIntegration.origin,
101
+ http_methods_to_capture=integration.http_methods_to_capture,
33
102
  )._run_asgi3
103
+
34
104
  return await middleware(scope, receive, send)
35
105
 
36
106
  cls.__call__ = sentry_patched_asgi_handler
37
107
 
108
+ modern_django_asgi_support = hasattr(cls, "create_request")
109
+ if modern_django_asgi_support:
110
+ old_create_request = cls.create_request
111
+
112
+ @ensure_integration_enabled(DjangoIntegration, old_create_request)
113
+ def sentry_patched_create_request(self, *args, **kwargs):
114
+ # type: (Any, *Any, **Any) -> Any
115
+ request, error_response = old_create_request(self, *args, **kwargs)
116
+ scope = sentry_sdk.get_isolation_scope()
117
+ scope.add_event_processor(_make_asgi_request_event_processor(request))
118
+
119
+ return request, error_response
120
+
121
+ cls.create_request = sentry_patched_create_request
122
+
38
123
 
39
124
  def patch_get_response_async(cls, _before_get_response):
40
125
  # type: (Any, Any) -> None
@@ -50,17 +135,111 @@ def patch_get_response_async(cls, _before_get_response):
50
135
 
51
136
  def patch_channels_asgi_handler_impl(cls):
52
137
  # type: (Any) -> None
53
- old_app = cls.__call__
54
-
55
- async def sentry_patched_asgi_handler(self, receive, send):
56
- # type: (Any, Any, Any) -> Any
57
- if Hub.current.get_integration(DjangoIntegration) is None:
58
- return await old_app(self, receive, send)
59
-
60
- middleware = SentryAsgiMiddleware(
61
- lambda _scope: old_app.__get__(self, cls), unsafe_context_data=True
62
- )
63
-
64
- return await middleware(self.scope)(receive, send)
65
-
66
- cls.__call__ = sentry_patched_asgi_handler
138
+ import channels # type: ignore
139
+
140
+ from sentry_sdk.integrations.django import DjangoIntegration
141
+
142
+ if channels.__version__ < "3.0.0":
143
+ old_app = cls.__call__
144
+
145
+ async def sentry_patched_asgi_handler(self, receive, send):
146
+ # type: (Any, Any, Any) -> Any
147
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
148
+ if integration is None:
149
+ return await old_app(self, receive, send)
150
+
151
+ middleware = SentryAsgiMiddleware(
152
+ lambda _scope: old_app.__get__(self, cls),
153
+ unsafe_context_data=True,
154
+ span_origin=DjangoIntegration.origin,
155
+ http_methods_to_capture=integration.http_methods_to_capture,
156
+ )
157
+
158
+ return await middleware(self.scope)(receive, send) # type: ignore
159
+
160
+ cls.__call__ = sentry_patched_asgi_handler
161
+
162
+ else:
163
+ # The ASGI handler in Channels >= 3 has the same signature as
164
+ # the Django handler.
165
+ patch_django_asgi_handler_impl(cls)
166
+
167
+
168
+ def wrap_async_view(callback):
169
+ # type: (Any) -> Any
170
+ from sentry_sdk.integrations.django import DjangoIntegration
171
+
172
+ @functools.wraps(callback)
173
+ async def sentry_wrapped_callback(request, *args, **kwargs):
174
+ # type: (Any, *Any, **Any) -> Any
175
+ current_scope = sentry_sdk.get_current_scope()
176
+ if current_scope.transaction is not None:
177
+ current_scope.transaction.update_active_thread()
178
+
179
+ sentry_scope = sentry_sdk.get_isolation_scope()
180
+ if sentry_scope.profile is not None:
181
+ sentry_scope.profile.update_active_thread_id()
182
+
183
+ with sentry_sdk.start_span(
184
+ op=OP.VIEW_RENDER,
185
+ name=request.resolver_match.view_name,
186
+ origin=DjangoIntegration.origin,
187
+ ):
188
+ return await callback(request, *args, **kwargs)
189
+
190
+ return sentry_wrapped_callback
191
+
192
+
193
+ def _asgi_middleware_mixin_factory(_check_middleware_span):
194
+ # type: (Callable[..., Any]) -> Any
195
+ """
196
+ Mixin class factory that generates a middleware mixin for handling requests
197
+ in async mode.
198
+ """
199
+
200
+ class SentryASGIMixin:
201
+ if TYPE_CHECKING:
202
+ _inner = None
203
+
204
+ def __init__(self, get_response):
205
+ # type: (Callable[..., Any]) -> None
206
+ self.get_response = get_response
207
+ self._acall_method = None
208
+ self._async_check()
209
+
210
+ def _async_check(self):
211
+ # type: () -> None
212
+ """
213
+ If get_response is a coroutine function, turns us into async mode so
214
+ a thread is not consumed during a whole request.
215
+ Taken from django.utils.deprecation::MiddlewareMixin._async_check
216
+ """
217
+ if iscoroutinefunction(self.get_response):
218
+ markcoroutinefunction(self)
219
+
220
+ def async_route_check(self):
221
+ # type: () -> bool
222
+ """
223
+ Function that checks if we are in async mode,
224
+ and if we are forwards the handling of requests to __acall__
225
+ """
226
+ return iscoroutinefunction(self.get_response)
227
+
228
+ async def __acall__(self, *args, **kwargs):
229
+ # type: (*Any, **Any) -> Any
230
+ f = self._acall_method
231
+ if f is None:
232
+ if hasattr(self._inner, "__acall__"):
233
+ self._acall_method = f = self._inner.__acall__ # type: ignore
234
+ else:
235
+ self._acall_method = f = self._inner
236
+
237
+ middleware_span = _check_middleware_span(old_method=f)
238
+
239
+ if middleware_span is None:
240
+ return await f(*args, **kwargs) # type: ignore
241
+
242
+ with middleware_span:
243
+ return await f(*args, **kwargs) # type: ignore
244
+
245
+ return SentryASGIMixin
@@ -0,0 +1,204 @@
1
+ import functools
2
+ from typing import TYPE_CHECKING
3
+ from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string
4
+ from urllib3.util import parse_url as urlparse
5
+
6
+ from django import VERSION as DJANGO_VERSION
7
+ from django.core.cache import CacheHandler
8
+
9
+ import sentry_sdk
10
+ from sentry_sdk.consts import OP, SPANDATA
11
+ from sentry_sdk.utils import (
12
+ capture_internal_exceptions,
13
+ ensure_integration_enabled,
14
+ )
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from typing import Any
19
+ from typing import Callable
20
+ from typing import Optional
21
+
22
+
23
+ METHODS_TO_INSTRUMENT = [
24
+ "set",
25
+ "set_many",
26
+ "get",
27
+ "get_many",
28
+ ]
29
+
30
+
31
+ def _get_span_description(method_name, args, kwargs):
32
+ # type: (str, tuple[Any], dict[str, Any]) -> str
33
+ return _key_as_string(_get_safe_key(method_name, args, kwargs))
34
+
35
+
36
+ def _patch_cache_method(cache, method_name, address, port):
37
+ # type: (CacheHandler, str, Optional[str], Optional[int]) -> None
38
+ from sentry_sdk.integrations.django import DjangoIntegration
39
+
40
+ original_method = getattr(cache, method_name)
41
+
42
+ @ensure_integration_enabled(DjangoIntegration, original_method)
43
+ def _instrument_call(
44
+ cache, method_name, original_method, args, kwargs, address, port
45
+ ):
46
+ # type: (CacheHandler, str, Callable[..., Any], tuple[Any, ...], dict[str, Any], Optional[str], Optional[int]) -> Any
47
+ is_set_operation = method_name.startswith("set")
48
+ is_get_method = method_name == "get"
49
+ is_get_many_method = method_name == "get_many"
50
+
51
+ op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET
52
+ description = _get_span_description(method_name, args, kwargs)
53
+
54
+ with sentry_sdk.start_span(
55
+ op=op,
56
+ name=description,
57
+ origin=DjangoIntegration.origin,
58
+ ) as span:
59
+ value = original_method(*args, **kwargs)
60
+
61
+ with capture_internal_exceptions():
62
+ if address is not None:
63
+ span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address)
64
+
65
+ if port is not None:
66
+ span.set_data(SPANDATA.NETWORK_PEER_PORT, port)
67
+
68
+ key = _get_safe_key(method_name, args, kwargs)
69
+ if key is not None:
70
+ span.set_data(SPANDATA.CACHE_KEY, key)
71
+
72
+ item_size = None
73
+ if is_get_many_method:
74
+ if value != {}:
75
+ item_size = len(str(value))
76
+ span.set_data(SPANDATA.CACHE_HIT, True)
77
+ else:
78
+ span.set_data(SPANDATA.CACHE_HIT, False)
79
+ elif is_get_method:
80
+ default_value = None
81
+ if len(args) >= 2:
82
+ default_value = args[1]
83
+ elif "default" in kwargs:
84
+ default_value = kwargs["default"]
85
+
86
+ if value != default_value:
87
+ item_size = len(str(value))
88
+ span.set_data(SPANDATA.CACHE_HIT, True)
89
+ else:
90
+ span.set_data(SPANDATA.CACHE_HIT, False)
91
+ else: # TODO: We don't handle `get_or_set` which we should
92
+ arg_count = len(args)
93
+ if arg_count >= 2:
94
+ # 'set' command
95
+ item_size = len(str(args[1]))
96
+ elif arg_count == 1:
97
+ # 'set_many' command
98
+ item_size = len(str(args[0]))
99
+
100
+ if item_size is not None:
101
+ span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size)
102
+
103
+ return value
104
+
105
+ @functools.wraps(original_method)
106
+ def sentry_method(*args, **kwargs):
107
+ # type: (*Any, **Any) -> Any
108
+ return _instrument_call(
109
+ cache, method_name, original_method, args, kwargs, address, port
110
+ )
111
+
112
+ setattr(cache, method_name, sentry_method)
113
+
114
+
115
+ def _patch_cache(cache, address=None, port=None):
116
+ # type: (CacheHandler, Optional[str], Optional[int]) -> None
117
+ if not hasattr(cache, "_sentry_patched"):
118
+ for method_name in METHODS_TO_INSTRUMENT:
119
+ _patch_cache_method(cache, method_name, address, port)
120
+ cache._sentry_patched = True
121
+
122
+
123
+ def _get_address_port(settings):
124
+ # type: (dict[str, Any]) -> tuple[Optional[str], Optional[int]]
125
+ location = settings.get("LOCATION")
126
+
127
+ # TODO: location can also be an array of locations
128
+ # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis
129
+ # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062
130
+ if not isinstance(location, str):
131
+ return None, None
132
+
133
+ if "://" in location:
134
+ parsed_url = urlparse(location)
135
+ # remove the username and password from URL to not leak sensitive data.
136
+ address = "{}://{}{}".format(
137
+ parsed_url.scheme or "",
138
+ parsed_url.hostname or "",
139
+ parsed_url.path or "",
140
+ )
141
+ port = parsed_url.port
142
+ else:
143
+ address = location
144
+ port = None
145
+
146
+ return address, int(port) if port is not None else None
147
+
148
+
149
+ def should_enable_cache_spans():
150
+ # type: () -> bool
151
+ from sentry_sdk.integrations.django import DjangoIntegration
152
+
153
+ client = sentry_sdk.get_client()
154
+ integration = client.get_integration(DjangoIntegration)
155
+ from django.conf import settings
156
+
157
+ return integration is not None and (
158
+ (client.spotlight is not None and settings.DEBUG is True)
159
+ or integration.cache_spans is True
160
+ )
161
+
162
+
163
+ def patch_caching():
164
+ # type: () -> None
165
+ if not hasattr(CacheHandler, "_sentry_patched"):
166
+ if DJANGO_VERSION < (3, 2):
167
+ original_get_item = CacheHandler.__getitem__
168
+
169
+ @functools.wraps(original_get_item)
170
+ def sentry_get_item(self, alias):
171
+ # type: (CacheHandler, str) -> Any
172
+ cache = original_get_item(self, alias)
173
+
174
+ if should_enable_cache_spans():
175
+ from django.conf import settings
176
+
177
+ address, port = _get_address_port(
178
+ settings.CACHES[alias or "default"]
179
+ )
180
+
181
+ _patch_cache(cache, address, port)
182
+
183
+ return cache
184
+
185
+ CacheHandler.__getitem__ = sentry_get_item
186
+ CacheHandler._sentry_patched = True
187
+
188
+ else:
189
+ original_create_connection = CacheHandler.create_connection
190
+
191
+ @functools.wraps(original_create_connection)
192
+ def sentry_create_connection(self, alias):
193
+ # type: (CacheHandler, str) -> Any
194
+ cache = original_create_connection(self, alias)
195
+
196
+ if should_enable_cache_spans():
197
+ address, port = _get_address_port(self.settings[alias or "default"])
198
+
199
+ _patch_cache(cache, address, port)
200
+
201
+ return cache
202
+
203
+ CacheHandler.create_connection = sentry_create_connection
204
+ CacheHandler._sentry_patched = True
@@ -2,39 +2,47 @@
2
2
  Create spans from Django middleware invocations
3
3
  """
4
4
 
5
+ from functools import wraps
6
+
5
7
  from django import VERSION as DJANGO_VERSION
6
8
 
7
- from sentry_sdk import Hub
8
- from sentry_sdk._functools import wraps
9
- from sentry_sdk._types import MYPY
9
+ import sentry_sdk
10
+ from sentry_sdk.consts import OP
10
11
  from sentry_sdk.utils import (
11
12
  ContextVar,
12
13
  transaction_from_function,
13
14
  capture_internal_exceptions,
14
15
  )
15
16
 
16
- if MYPY:
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
17
20
  from typing import Any
18
21
  from typing import Callable
22
+ from typing import Optional
19
23
  from typing import TypeVar
20
24
 
25
+ from sentry_sdk.tracing import Span
26
+
21
27
  F = TypeVar("F", bound=Callable[..., Any])
22
28
 
23
29
  _import_string_should_wrap_middleware = ContextVar(
24
30
  "import_string_should_wrap_middleware"
25
31
  )
26
32
 
27
- if DJANGO_VERSION < (1, 7):
28
- import_string_name = "import_by_path"
33
+ DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1)
34
+
35
+ if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE:
36
+ _asgi_middleware_mixin_factory = lambda _: object
29
37
  else:
30
- import_string_name = "import_string"
38
+ from .asgi import _asgi_middleware_mixin_factory
31
39
 
32
40
 
33
41
  def patch_django_middlewares():
34
42
  # type: () -> None
35
43
  from django.core.handlers import base
36
44
 
37
- old_import_string = getattr(base, import_string_name)
45
+ old_import_string = base.import_string
38
46
 
39
47
  def sentry_patched_import_string(dotted_path):
40
48
  # type: (str) -> Any
@@ -45,7 +53,7 @@ def patch_django_middlewares():
45
53
 
46
54
  return rv
47
55
 
48
- setattr(base, import_string_name, sentry_patched_import_string)
56
+ base.import_string = sentry_patched_import_string
49
57
 
50
58
  old_load_middleware = base.BaseHandler.load_middleware
51
59
 
@@ -64,29 +72,41 @@ def _wrap_middleware(middleware, middleware_name):
64
72
  # type: (Any, str) -> Any
65
73
  from sentry_sdk.integrations.django import DjangoIntegration
66
74
 
75
+ def _check_middleware_span(old_method):
76
+ # type: (Callable[..., Any]) -> Optional[Span]
77
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
78
+ if integration is None or not integration.middleware_spans:
79
+ return None
80
+
81
+ function_name = transaction_from_function(old_method)
82
+
83
+ description = middleware_name
84
+ function_basename = getattr(old_method, "__name__", None)
85
+ if function_basename:
86
+ description = "{}.{}".format(description, function_basename)
87
+
88
+ middleware_span = sentry_sdk.start_span(
89
+ op=OP.MIDDLEWARE_DJANGO,
90
+ name=description,
91
+ origin=DjangoIntegration.origin,
92
+ )
93
+ middleware_span.set_tag("django.function_name", function_name)
94
+ middleware_span.set_tag("django.middleware_name", middleware_name)
95
+
96
+ return middleware_span
97
+
67
98
  def _get_wrapped_method(old_method):
68
99
  # type: (F) -> F
69
100
  with capture_internal_exceptions():
70
101
 
71
102
  def sentry_wrapped_method(*args, **kwargs):
72
103
  # type: (*Any, **Any) -> Any
73
- hub = Hub.current
74
- integration = hub.get_integration(DjangoIntegration)
75
- if integration is None or not integration.middleware_spans:
76
- return old_method(*args, **kwargs)
104
+ middleware_span = _check_middleware_span(old_method)
77
105
 
78
- function_name = transaction_from_function(old_method)
79
-
80
- description = middleware_name
81
- function_basename = getattr(old_method, "__name__", None)
82
- if function_basename:
83
- description = "{}.{}".format(description, function_basename)
106
+ if middleware_span is None:
107
+ return old_method(*args, **kwargs)
84
108
 
85
- with hub.start_span(
86
- op="django.middleware", description=description
87
- ) as span:
88
- span.set_tag("django.function_name", function_name)
89
- span.set_tag("django.middleware_name", middleware_name)
109
+ with middleware_span:
90
110
  return old_method(*args, **kwargs)
91
111
 
92
112
  try:
@@ -102,11 +122,24 @@ def _wrap_middleware(middleware, middleware_name):
102
122
 
103
123
  return old_method
104
124
 
105
- class SentryWrappingMiddleware(object):
106
- def __init__(self, *args, **kwargs):
107
- # type: (*Any, **Any) -> None
108
- self._inner = middleware(*args, **kwargs)
125
+ class SentryWrappingMiddleware(
126
+ _asgi_middleware_mixin_factory(_check_middleware_span) # type: ignore
127
+ ):
128
+ sync_capable = getattr(middleware, "sync_capable", True)
129
+ async_capable = DJANGO_SUPPORTS_ASYNC_MIDDLEWARE and getattr(
130
+ middleware, "async_capable", False
131
+ )
132
+
133
+ def __init__(self, get_response=None, *args, **kwargs):
134
+ # type: (Optional[Callable[..., Any]], *Any, **Any) -> None
135
+ if get_response:
136
+ self._inner = middleware(get_response, *args, **kwargs)
137
+ else:
138
+ self._inner = middleware(*args, **kwargs)
139
+ self.get_response = get_response
109
140
  self._call_method = None
141
+ if self.async_capable:
142
+ super().__init__(get_response)
110
143
 
111
144
  # We need correct behavior for `hasattr()`, which we can only determine
112
145
  # when we have an instance of the middleware we're wrapping.
@@ -128,12 +161,27 @@ def _wrap_middleware(middleware, middleware_name):
128
161
 
129
162
  def __call__(self, *args, **kwargs):
130
163
  # type: (*Any, **Any) -> Any
164
+ if hasattr(self, "async_route_check") and self.async_route_check():
165
+ return self.__acall__(*args, **kwargs)
166
+
131
167
  f = self._call_method
132
168
  if f is None:
133
- self._call_method = f = _get_wrapped_method(self._inner.__call__)
134
- return f(*args, **kwargs)
169
+ self._call_method = f = self._inner.__call__
170
+
171
+ middleware_span = _check_middleware_span(old_method=f)
172
+
173
+ if middleware_span is None:
174
+ return f(*args, **kwargs)
175
+
176
+ with middleware_span:
177
+ return f(*args, **kwargs)
135
178
 
136
- if hasattr(middleware, "__name__"):
137
- SentryWrappingMiddleware.__name__ = middleware.__name__
179
+ for attr in (
180
+ "__name__",
181
+ "__module__",
182
+ "__qualname__",
183
+ ):
184
+ if hasattr(middleware, attr):
185
+ setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr))
138
186
 
139
187
  return SentryWrappingMiddleware