sentry-sdk 0.7.5__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 -30
  2. sentry_sdk/_compat.py +74 -61
  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 +289 -0
  8. sentry_sdk/_types.py +338 -0
  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 +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  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 +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  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 +134 -0
  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 +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  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 +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  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 +141 -0
  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 +112 -68
  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 +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,245 @@
1
+ """
2
+ Instrumentation for Django 3.0
3
+
4
+ Since this file contains `async def` it is conditionally imported in
5
+ `sentry_sdk.integrations.django` (depending on the existence of
6
+ `django.core.handlers.asgi`.
7
+ """
8
+
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
17
+
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
+ )
24
+
25
+ from typing import TYPE_CHECKING
26
+
27
+ if TYPE_CHECKING:
28
+ from typing import Any, Callable, Union, TypeVar
29
+
30
+ from django.core.handlers.asgi import ASGIRequest
31
+ from django.http.response import HttpResponse
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
+
83
+
84
+ def patch_django_asgi_handler_impl(cls):
85
+ # type: (Any) -> None
86
+
87
+ from sentry_sdk.integrations.django import DjangoIntegration
88
+
89
+ old_app = cls.__call__
90
+
91
+ async def sentry_patched_asgi_handler(self, scope, receive, send):
92
+ # type: (Any, Any, Any, Any) -> Any
93
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
94
+ if integration is None:
95
+ return await old_app(self, scope, receive, send)
96
+
97
+ middleware = SentryAsgiMiddleware(
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,
102
+ )._run_asgi3
103
+
104
+ return await middleware(scope, receive, send)
105
+
106
+ cls.__call__ = sentry_patched_asgi_handler
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
+
123
+
124
+ def patch_get_response_async(cls, _before_get_response):
125
+ # type: (Any, Any) -> None
126
+ old_get_response_async = cls.get_response_async
127
+
128
+ async def sentry_patched_get_response_async(self, request):
129
+ # type: (Any, Any) -> Union[HttpResponse, BaseException]
130
+ _before_get_response(request)
131
+ return await old_get_response_async(self, request)
132
+
133
+ cls.get_response_async = sentry_patched_get_response_async
134
+
135
+
136
+ def patch_channels_asgi_handler_impl(cls):
137
+ # type: (Any) -> None
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
@@ -0,0 +1,187 @@
1
+ """
2
+ Create spans from Django middleware invocations
3
+ """
4
+
5
+ from functools import wraps
6
+
7
+ from django import VERSION as DJANGO_VERSION
8
+
9
+ import sentry_sdk
10
+ from sentry_sdk.consts import OP
11
+ from sentry_sdk.utils import (
12
+ ContextVar,
13
+ transaction_from_function,
14
+ capture_internal_exceptions,
15
+ )
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from typing import Any
21
+ from typing import Callable
22
+ from typing import Optional
23
+ from typing import TypeVar
24
+
25
+ from sentry_sdk.tracing import Span
26
+
27
+ F = TypeVar("F", bound=Callable[..., Any])
28
+
29
+ _import_string_should_wrap_middleware = ContextVar(
30
+ "import_string_should_wrap_middleware"
31
+ )
32
+
33
+ DJANGO_SUPPORTS_ASYNC_MIDDLEWARE = DJANGO_VERSION >= (3, 1)
34
+
35
+ if not DJANGO_SUPPORTS_ASYNC_MIDDLEWARE:
36
+ _asgi_middleware_mixin_factory = lambda _: object
37
+ else:
38
+ from .asgi import _asgi_middleware_mixin_factory
39
+
40
+
41
+ def patch_django_middlewares():
42
+ # type: () -> None
43
+ from django.core.handlers import base
44
+
45
+ old_import_string = base.import_string
46
+
47
+ def sentry_patched_import_string(dotted_path):
48
+ # type: (str) -> Any
49
+ rv = old_import_string(dotted_path)
50
+
51
+ if _import_string_should_wrap_middleware.get(None):
52
+ rv = _wrap_middleware(rv, dotted_path)
53
+
54
+ return rv
55
+
56
+ base.import_string = sentry_patched_import_string
57
+
58
+ old_load_middleware = base.BaseHandler.load_middleware
59
+
60
+ def sentry_patched_load_middleware(*args, **kwargs):
61
+ # type: (Any, Any) -> Any
62
+ _import_string_should_wrap_middleware.set(True)
63
+ try:
64
+ return old_load_middleware(*args, **kwargs)
65
+ finally:
66
+ _import_string_should_wrap_middleware.set(False)
67
+
68
+ base.BaseHandler.load_middleware = sentry_patched_load_middleware
69
+
70
+
71
+ def _wrap_middleware(middleware, middleware_name):
72
+ # type: (Any, str) -> Any
73
+ from sentry_sdk.integrations.django import DjangoIntegration
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
+
98
+ def _get_wrapped_method(old_method):
99
+ # type: (F) -> F
100
+ with capture_internal_exceptions():
101
+
102
+ def sentry_wrapped_method(*args, **kwargs):
103
+ # type: (*Any, **Any) -> Any
104
+ middleware_span = _check_middleware_span(old_method)
105
+
106
+ if middleware_span is None:
107
+ return old_method(*args, **kwargs)
108
+
109
+ with middleware_span:
110
+ return old_method(*args, **kwargs)
111
+
112
+ try:
113
+ # fails for __call__ of function on Python 2 (see py2.7-django-1.11)
114
+ sentry_wrapped_method = wraps(old_method)(sentry_wrapped_method)
115
+
116
+ # Necessary for Django 3.1
117
+ sentry_wrapped_method.__self__ = old_method.__self__ # type: ignore
118
+ except Exception:
119
+ pass
120
+
121
+ return sentry_wrapped_method # type: ignore
122
+
123
+ return old_method
124
+
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
140
+ self._call_method = None
141
+ if self.async_capable:
142
+ super().__init__(get_response)
143
+
144
+ # We need correct behavior for `hasattr()`, which we can only determine
145
+ # when we have an instance of the middleware we're wrapping.
146
+ def __getattr__(self, method_name):
147
+ # type: (str) -> Any
148
+ if method_name not in (
149
+ "process_request",
150
+ "process_view",
151
+ "process_template_response",
152
+ "process_response",
153
+ "process_exception",
154
+ ):
155
+ raise AttributeError()
156
+
157
+ old_method = getattr(self._inner, method_name)
158
+ rv = _get_wrapped_method(old_method)
159
+ self.__dict__[method_name] = rv
160
+ return rv
161
+
162
+ def __call__(self, *args, **kwargs):
163
+ # type: (*Any, **Any) -> Any
164
+ if hasattr(self, "async_route_check") and self.async_route_check():
165
+ return self.__acall__(*args, **kwargs)
166
+
167
+ f = self._call_method
168
+ if f is None:
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)
178
+
179
+ for attr in (
180
+ "__name__",
181
+ "__module__",
182
+ "__qualname__",
183
+ ):
184
+ if hasattr(middleware, attr):
185
+ setattr(SentryWrappingMiddleware, attr, getattr(middleware, attr))
186
+
187
+ return SentryWrappingMiddleware