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.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
8
|
-
from sentry_sdk.
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|