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.
- sentry_sdk/__init__.py +48 -30
- sentry_sdk/_compat.py +74 -61
- 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 +289 -0
- sentry_sdk/_types.py +338 -0
- 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 +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- 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 +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- 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 +134 -0
- 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 +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- 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 +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- 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 +141 -0
- 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 +112 -68
- 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 +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- 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 +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- 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 +201 -119
- 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/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.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/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from django.dispatch import Signal
|
|
4
|
+
|
|
5
|
+
import sentry_sdk
|
|
6
|
+
from sentry_sdk.consts import OP
|
|
7
|
+
from sentry_sdk.integrations.django import DJANGO_VERSION
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any, Union
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_receiver_name(receiver):
|
|
17
|
+
# type: (Callable[..., Any]) -> str
|
|
18
|
+
name = ""
|
|
19
|
+
|
|
20
|
+
if hasattr(receiver, "__qualname__"):
|
|
21
|
+
name = receiver.__qualname__
|
|
22
|
+
elif hasattr(receiver, "__name__"): # Python 2.7 has no __qualname__
|
|
23
|
+
name = receiver.__name__
|
|
24
|
+
elif hasattr(
|
|
25
|
+
receiver, "func"
|
|
26
|
+
): # certain functions (like partials) dont have a name
|
|
27
|
+
if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"):
|
|
28
|
+
name = "partial(<function " + receiver.func.__name__ + ">)"
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
name == ""
|
|
32
|
+
): # In case nothing was found, return the string representation (this is the slowest case)
|
|
33
|
+
return str(receiver)
|
|
34
|
+
|
|
35
|
+
if hasattr(receiver, "__module__"): # prepend with module, if there is one
|
|
36
|
+
name = receiver.__module__ + "." + name
|
|
37
|
+
|
|
38
|
+
return name
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def patch_signals():
|
|
42
|
+
# type: () -> None
|
|
43
|
+
"""
|
|
44
|
+
Patch django signal receivers to create a span.
|
|
45
|
+
|
|
46
|
+
This only wraps sync receivers. Django>=5.0 introduced async receivers, but
|
|
47
|
+
since we don't create transactions for ASGI Django, we don't wrap them.
|
|
48
|
+
"""
|
|
49
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
50
|
+
|
|
51
|
+
old_live_receivers = Signal._live_receivers
|
|
52
|
+
|
|
53
|
+
def _sentry_live_receivers(self, sender):
|
|
54
|
+
# type: (Signal, Any) -> Union[tuple[list[Callable[..., Any]], list[Callable[..., Any]]], list[Callable[..., Any]]]
|
|
55
|
+
if DJANGO_VERSION >= (5, 0):
|
|
56
|
+
sync_receivers, async_receivers = old_live_receivers(self, sender)
|
|
57
|
+
else:
|
|
58
|
+
sync_receivers = old_live_receivers(self, sender)
|
|
59
|
+
async_receivers = []
|
|
60
|
+
|
|
61
|
+
def sentry_sync_receiver_wrapper(receiver):
|
|
62
|
+
# type: (Callable[..., Any]) -> Callable[..., Any]
|
|
63
|
+
@wraps(receiver)
|
|
64
|
+
def wrapper(*args, **kwargs):
|
|
65
|
+
# type: (Any, Any) -> Any
|
|
66
|
+
signal_name = _get_receiver_name(receiver)
|
|
67
|
+
with sentry_sdk.start_span(
|
|
68
|
+
op=OP.EVENT_DJANGO,
|
|
69
|
+
name=signal_name,
|
|
70
|
+
origin=DjangoIntegration.origin,
|
|
71
|
+
) as span:
|
|
72
|
+
span.set_data("signal", signal_name)
|
|
73
|
+
return receiver(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
|
|
78
|
+
if (
|
|
79
|
+
integration
|
|
80
|
+
and integration.signals_spans
|
|
81
|
+
and self not in integration.signals_denylist
|
|
82
|
+
):
|
|
83
|
+
for idx, receiver in enumerate(sync_receivers):
|
|
84
|
+
sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver)
|
|
85
|
+
|
|
86
|
+
if DJANGO_VERSION >= (5, 0):
|
|
87
|
+
return sync_receivers, async_receivers
|
|
88
|
+
else:
|
|
89
|
+
return sync_receivers
|
|
90
|
+
|
|
91
|
+
Signal._live_receivers = _sentry_live_receivers
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
import functools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from django.template import TemplateSyntaxError
|
|
4
|
+
from django.utils.safestring import mark_safe
|
|
5
|
+
from django import VERSION as DJANGO_VERSION
|
|
6
|
+
|
|
7
|
+
import sentry_sdk
|
|
8
|
+
from sentry_sdk.consts import OP
|
|
9
|
+
from sentry_sdk.utils import ensure_integration_enabled
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
4
14
|
from typing import Any
|
|
5
15
|
from typing import Dict
|
|
6
16
|
from typing import Optional
|
|
17
|
+
from typing import Iterator
|
|
18
|
+
from typing import Tuple
|
|
7
19
|
|
|
8
20
|
try:
|
|
9
21
|
# support Django 1.9
|
|
10
|
-
from django.template.base import Origin
|
|
22
|
+
from django.template.base import Origin
|
|
11
23
|
except ImportError:
|
|
12
24
|
# backward compatibility
|
|
13
|
-
from django.template.loader import LoaderOrigin as Origin
|
|
25
|
+
from django.template.loader import LoaderOrigin as Origin
|
|
14
26
|
|
|
15
27
|
|
|
16
28
|
def get_template_frame_from_exception(exc_value):
|
|
@@ -31,11 +43,70 @@ def get_template_frame_from_exception(exc_value):
|
|
|
31
43
|
if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"):
|
|
32
44
|
source = exc_value.source
|
|
33
45
|
if isinstance(source, (tuple, list)) and isinstance(source[0], Origin):
|
|
34
|
-
return _get_template_frame_from_source(source)
|
|
46
|
+
return _get_template_frame_from_source(source) # type: ignore
|
|
35
47
|
|
|
36
48
|
return None
|
|
37
49
|
|
|
38
50
|
|
|
51
|
+
def _get_template_name_description(template_name):
|
|
52
|
+
# type: (str) -> str
|
|
53
|
+
if isinstance(template_name, (list, tuple)):
|
|
54
|
+
if template_name:
|
|
55
|
+
return "[{}, ...]".format(template_name[0])
|
|
56
|
+
else:
|
|
57
|
+
return template_name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def patch_templates():
|
|
61
|
+
# type: () -> None
|
|
62
|
+
from django.template.response import SimpleTemplateResponse
|
|
63
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
64
|
+
|
|
65
|
+
real_rendered_content = SimpleTemplateResponse.rendered_content
|
|
66
|
+
|
|
67
|
+
@property # type: ignore
|
|
68
|
+
@ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget)
|
|
69
|
+
def rendered_content(self):
|
|
70
|
+
# type: (SimpleTemplateResponse) -> str
|
|
71
|
+
with sentry_sdk.start_span(
|
|
72
|
+
op=OP.TEMPLATE_RENDER,
|
|
73
|
+
name=_get_template_name_description(self.template_name),
|
|
74
|
+
origin=DjangoIntegration.origin,
|
|
75
|
+
) as span:
|
|
76
|
+
span.set_data("context", self.context_data)
|
|
77
|
+
return real_rendered_content.fget(self)
|
|
78
|
+
|
|
79
|
+
SimpleTemplateResponse.rendered_content = rendered_content
|
|
80
|
+
|
|
81
|
+
if DJANGO_VERSION < (1, 7):
|
|
82
|
+
return
|
|
83
|
+
import django.shortcuts
|
|
84
|
+
|
|
85
|
+
real_render = django.shortcuts.render
|
|
86
|
+
|
|
87
|
+
@functools.wraps(real_render)
|
|
88
|
+
@ensure_integration_enabled(DjangoIntegration, real_render)
|
|
89
|
+
def render(request, template_name, context=None, *args, **kwargs):
|
|
90
|
+
# type: (django.http.HttpRequest, str, Optional[Dict[str, Any]], *Any, **Any) -> django.http.HttpResponse
|
|
91
|
+
|
|
92
|
+
# Inject trace meta tags into template context
|
|
93
|
+
context = context or {}
|
|
94
|
+
if "sentry_trace_meta" not in context:
|
|
95
|
+
context["sentry_trace_meta"] = mark_safe(
|
|
96
|
+
sentry_sdk.get_current_scope().trace_propagation_meta()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
with sentry_sdk.start_span(
|
|
100
|
+
op=OP.TEMPLATE_RENDER,
|
|
101
|
+
name=_get_template_name_description(template_name),
|
|
102
|
+
origin=DjangoIntegration.origin,
|
|
103
|
+
) as span:
|
|
104
|
+
span.set_data("context", context)
|
|
105
|
+
return real_render(request, template_name, context, *args, **kwargs)
|
|
106
|
+
|
|
107
|
+
django.shortcuts.render = render
|
|
108
|
+
|
|
109
|
+
|
|
39
110
|
def _get_template_frame_from_debug(debug):
|
|
40
111
|
# type: (Dict[str, Any]) -> Dict[str, Any]
|
|
41
112
|
if debug is None:
|
|
@@ -64,10 +135,12 @@ def _get_template_frame_from_debug(debug):
|
|
|
64
135
|
"pre_context": pre_context[-5:],
|
|
65
136
|
"post_context": post_context[:5],
|
|
66
137
|
"context_line": context_line,
|
|
138
|
+
"in_app": True,
|
|
67
139
|
}
|
|
68
140
|
|
|
69
141
|
|
|
70
142
|
def _linebreak_iter(template_source):
|
|
143
|
+
# type: (str) -> Iterator[int]
|
|
71
144
|
yield 0
|
|
72
145
|
p = template_source.find("\n")
|
|
73
146
|
while p >= 0:
|
|
@@ -76,6 +149,7 @@ def _linebreak_iter(template_source):
|
|
|
76
149
|
|
|
77
150
|
|
|
78
151
|
def _get_template_frame_from_source(source):
|
|
152
|
+
# type: (Tuple[Origin, Tuple[int, int]]) -> Optional[Dict[str, Any]]
|
|
79
153
|
if not source:
|
|
80
154
|
return None
|
|
81
155
|
|
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Copied from raven-python.
|
|
3
|
-
`DjangoIntegration(transaction_fron="raven_legacy")`.
|
|
4
|
-
"""
|
|
2
|
+
Copied from raven-python.
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
Despite being called "legacy" in some places this resolver is very much still
|
|
5
|
+
in use.
|
|
6
|
+
"""
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from django.urls.resolvers import URLResolver
|
|
12
14
|
from typing import Dict
|
|
13
15
|
from typing import List
|
|
14
16
|
from typing import Optional
|
|
15
|
-
from django.urls.resolvers import URLPattern
|
|
17
|
+
from django.urls.resolvers import URLPattern
|
|
16
18
|
from typing import Tuple
|
|
17
19
|
from typing import Union
|
|
18
|
-
from re import Pattern
|
|
20
|
+
from re import Pattern
|
|
21
|
+
|
|
22
|
+
from django import VERSION as DJANGO_VERSION
|
|
23
|
+
|
|
24
|
+
if DJANGO_VERSION >= (2, 0):
|
|
25
|
+
from django.urls.resolvers import RoutePattern
|
|
26
|
+
else:
|
|
27
|
+
RoutePattern = None
|
|
19
28
|
|
|
20
29
|
try:
|
|
21
|
-
from django.urls import get_resolver
|
|
30
|
+
from django.urls import get_resolver
|
|
22
31
|
except ImportError:
|
|
23
|
-
from django.core.urlresolvers import get_resolver
|
|
32
|
+
from django.core.urlresolvers import get_resolver
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
def get_regex(resolver_or_pattern):
|
|
27
|
-
# type: (Union[URLPattern, URLResolver]) -> Pattern
|
|
36
|
+
# type: (Union[URLPattern, URLResolver]) -> Pattern[str]
|
|
28
37
|
"""Utility method for django's deprecated resolver.regex"""
|
|
29
38
|
try:
|
|
30
39
|
regex = resolver_or_pattern.regex
|
|
@@ -33,9 +42,12 @@ def get_regex(resolver_or_pattern):
|
|
|
33
42
|
return regex
|
|
34
43
|
|
|
35
44
|
|
|
36
|
-
class RavenResolver
|
|
45
|
+
class RavenResolver:
|
|
46
|
+
_new_style_group_matcher = re.compile(
|
|
47
|
+
r"<(?:([^>:]+):)?([^>]+)>"
|
|
48
|
+
) # https://github.com/django/django/blob/21382e2743d06efbf5623e7c9b6dccf2a325669b/django/urls/resolvers.py#L245-L247
|
|
37
49
|
_optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)")
|
|
38
|
-
_named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)")
|
|
50
|
+
_named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+")
|
|
39
51
|
_non_named_group_matcher = re.compile(r"\([^\)]+\)")
|
|
40
52
|
# [foo|bar|baz]
|
|
41
53
|
_either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]")
|
|
@@ -44,7 +56,7 @@ class RavenResolver(object):
|
|
|
44
56
|
_cache = {} # type: Dict[URLPattern, str]
|
|
45
57
|
|
|
46
58
|
def _simplify(self, pattern):
|
|
47
|
-
# type: (
|
|
59
|
+
# type: (Union[URLPattern, URLResolver]) -> str
|
|
48
60
|
r"""
|
|
49
61
|
Clean up urlpattern regexes into something readable by humans:
|
|
50
62
|
|
|
@@ -54,11 +66,24 @@ class RavenResolver(object):
|
|
|
54
66
|
To:
|
|
55
67
|
> "{sport_slug}/athletes/{athlete_slug}/"
|
|
56
68
|
"""
|
|
69
|
+
# "new-style" path patterns can be parsed directly without turning them
|
|
70
|
+
# into regexes first
|
|
71
|
+
if (
|
|
72
|
+
RoutePattern is not None
|
|
73
|
+
and hasattr(pattern, "pattern")
|
|
74
|
+
and isinstance(pattern.pattern, RoutePattern)
|
|
75
|
+
):
|
|
76
|
+
return self._new_style_group_matcher.sub(
|
|
77
|
+
lambda m: "{%s}" % m.group(2), str(pattern.pattern._route)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
result = get_regex(pattern).pattern
|
|
81
|
+
|
|
57
82
|
# remove optional params
|
|
58
83
|
# TODO(dcramer): it'd be nice to change these into [%s] but it currently
|
|
59
84
|
# conflicts with the other rules because we're doing regexp matches
|
|
60
85
|
# rather than parsing tokens
|
|
61
|
-
result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1),
|
|
86
|
+
result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), result)
|
|
62
87
|
|
|
63
88
|
# handle named groups first
|
|
64
89
|
result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result)
|
|
@@ -74,6 +99,8 @@ class RavenResolver(object):
|
|
|
74
99
|
result.replace("^", "")
|
|
75
100
|
.replace("$", "")
|
|
76
101
|
.replace("?", "")
|
|
102
|
+
.replace("\\A", "")
|
|
103
|
+
.replace("\\Z", "")
|
|
77
104
|
.replace("//", "/")
|
|
78
105
|
.replace("\\", "")
|
|
79
106
|
)
|
|
@@ -97,9 +124,9 @@ class RavenResolver(object):
|
|
|
97
124
|
for pattern in resolver.url_patterns:
|
|
98
125
|
# this is an include()
|
|
99
126
|
if not pattern.callback:
|
|
100
|
-
|
|
101
|
-
if
|
|
102
|
-
return
|
|
127
|
+
match_ = self._resolve(pattern, new_path, parents)
|
|
128
|
+
if match_:
|
|
129
|
+
return match_
|
|
103
130
|
continue
|
|
104
131
|
elif not get_regex(pattern).search(new_path):
|
|
105
132
|
continue
|
|
@@ -109,8 +136,8 @@ class RavenResolver(object):
|
|
|
109
136
|
except KeyError:
|
|
110
137
|
pass
|
|
111
138
|
|
|
112
|
-
prefix = "".join(self._simplify(
|
|
113
|
-
result = prefix + self._simplify(
|
|
139
|
+
prefix = "".join(self._simplify(p) for p in parents)
|
|
140
|
+
result = prefix + self._simplify(pattern)
|
|
114
141
|
if not result.startswith("/"):
|
|
115
142
|
result = "/" + result
|
|
116
143
|
self._cache[pattern] = result
|
|
@@ -123,10 +150,10 @@ class RavenResolver(object):
|
|
|
123
150
|
path, # type: str
|
|
124
151
|
urlconf=None, # type: Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]
|
|
125
152
|
):
|
|
126
|
-
# type: (...) -> str
|
|
153
|
+
# type: (...) -> Optional[str]
|
|
127
154
|
resolver = get_resolver(urlconf)
|
|
128
155
|
match = self._resolve(resolver, path)
|
|
129
|
-
return match
|
|
156
|
+
return match
|
|
130
157
|
|
|
131
158
|
|
|
132
159
|
LEGACY_RESOLVER = RavenResolver()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk.consts import OP
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from asyncio import iscoroutinefunction
|
|
14
|
+
except ImportError:
|
|
15
|
+
iscoroutinefunction = None # type: ignore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from sentry_sdk.integrations.django.asgi import wrap_async_view
|
|
20
|
+
except (ImportError, SyntaxError):
|
|
21
|
+
wrap_async_view = None # type: ignore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def patch_views():
|
|
25
|
+
# type: () -> None
|
|
26
|
+
|
|
27
|
+
from django.core.handlers.base import BaseHandler
|
|
28
|
+
from django.template.response import SimpleTemplateResponse
|
|
29
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
30
|
+
|
|
31
|
+
old_make_view_atomic = BaseHandler.make_view_atomic
|
|
32
|
+
old_render = SimpleTemplateResponse.render
|
|
33
|
+
|
|
34
|
+
def sentry_patched_render(self):
|
|
35
|
+
# type: (SimpleTemplateResponse) -> Any
|
|
36
|
+
with sentry_sdk.start_span(
|
|
37
|
+
op=OP.VIEW_RESPONSE_RENDER,
|
|
38
|
+
name="serialize response",
|
|
39
|
+
origin=DjangoIntegration.origin,
|
|
40
|
+
):
|
|
41
|
+
return old_render(self)
|
|
42
|
+
|
|
43
|
+
@functools.wraps(old_make_view_atomic)
|
|
44
|
+
def sentry_patched_make_view_atomic(self, *args, **kwargs):
|
|
45
|
+
# type: (Any, *Any, **Any) -> Any
|
|
46
|
+
callback = old_make_view_atomic(self, *args, **kwargs)
|
|
47
|
+
|
|
48
|
+
# XXX: The wrapper function is created for every request. Find more
|
|
49
|
+
# efficient way to wrap views (or build a cache?)
|
|
50
|
+
|
|
51
|
+
integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
|
|
52
|
+
if integration is not None and integration.middleware_spans:
|
|
53
|
+
is_async_view = (
|
|
54
|
+
iscoroutinefunction is not None
|
|
55
|
+
and wrap_async_view is not None
|
|
56
|
+
and iscoroutinefunction(callback)
|
|
57
|
+
)
|
|
58
|
+
if is_async_view:
|
|
59
|
+
sentry_wrapped_callback = wrap_async_view(callback)
|
|
60
|
+
else:
|
|
61
|
+
sentry_wrapped_callback = _wrap_sync_view(callback)
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
sentry_wrapped_callback = callback
|
|
65
|
+
|
|
66
|
+
return sentry_wrapped_callback
|
|
67
|
+
|
|
68
|
+
SimpleTemplateResponse.render = sentry_patched_render
|
|
69
|
+
BaseHandler.make_view_atomic = sentry_patched_make_view_atomic
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _wrap_sync_view(callback):
|
|
73
|
+
# type: (Any) -> Any
|
|
74
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
75
|
+
|
|
76
|
+
@functools.wraps(callback)
|
|
77
|
+
def sentry_wrapped_callback(request, *args, **kwargs):
|
|
78
|
+
# type: (Any, *Any, **Any) -> Any
|
|
79
|
+
current_scope = sentry_sdk.get_current_scope()
|
|
80
|
+
if current_scope.transaction is not None:
|
|
81
|
+
current_scope.transaction.update_active_thread()
|
|
82
|
+
|
|
83
|
+
sentry_scope = sentry_sdk.get_isolation_scope()
|
|
84
|
+
# set the active thread id to the handler thread for sync views
|
|
85
|
+
# this isn't necessary for async views since that runs on main
|
|
86
|
+
if sentry_scope.profile is not None:
|
|
87
|
+
sentry_scope.profile.update_active_thread_id()
|
|
88
|
+
|
|
89
|
+
with sentry_sdk.start_span(
|
|
90
|
+
op=OP.VIEW_RENDER,
|
|
91
|
+
name=request.resolver_match.view_name,
|
|
92
|
+
origin=DjangoIntegration.origin,
|
|
93
|
+
):
|
|
94
|
+
return callback(request, *args, **kwargs)
|
|
95
|
+
|
|
96
|
+
return sentry_wrapped_callback
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk.consts import OP, SPANSTATUS
|
|
5
|
+
from sentry_sdk.api import continue_trace, get_baggage, get_traceparent
|
|
6
|
+
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
7
|
+
from sentry_sdk.integrations._wsgi_common import request_body_within_bounds
|
|
8
|
+
from sentry_sdk.tracing import (
|
|
9
|
+
BAGGAGE_HEADER_NAME,
|
|
10
|
+
SENTRY_TRACE_HEADER_NAME,
|
|
11
|
+
TransactionSource,
|
|
12
|
+
)
|
|
13
|
+
from sentry_sdk.utils import (
|
|
14
|
+
AnnotatedValue,
|
|
15
|
+
capture_internal_exceptions,
|
|
16
|
+
event_from_exception,
|
|
17
|
+
)
|
|
18
|
+
from typing import TypeVar
|
|
19
|
+
|
|
20
|
+
R = TypeVar("R")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from dramatiq.broker import Broker
|
|
24
|
+
from dramatiq.middleware import Middleware, default_middleware
|
|
25
|
+
from dramatiq.errors import Retry
|
|
26
|
+
from dramatiq.message import Message
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise DidNotEnable("Dramatiq is not installed")
|
|
29
|
+
|
|
30
|
+
from typing import TYPE_CHECKING
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
34
|
+
from sentry_sdk._types import Event, Hint
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DramatiqIntegration(Integration):
|
|
38
|
+
"""
|
|
39
|
+
Dramatiq integration for Sentry
|
|
40
|
+
|
|
41
|
+
Please make sure that you call `sentry_sdk.init` *before* initializing
|
|
42
|
+
your broker, as it monkey patches `Broker.__init__`.
|
|
43
|
+
|
|
44
|
+
This integration was originally developed and maintained
|
|
45
|
+
by https://github.com/jacobsvante and later donated to the Sentry
|
|
46
|
+
project.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
identifier = "dramatiq"
|
|
50
|
+
origin = f"auto.queue.{identifier}"
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def setup_once():
|
|
54
|
+
# type: () -> None
|
|
55
|
+
|
|
56
|
+
_patch_dramatiq_broker()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _patch_dramatiq_broker():
|
|
60
|
+
# type: () -> None
|
|
61
|
+
original_broker__init__ = Broker.__init__
|
|
62
|
+
|
|
63
|
+
def sentry_patched_broker__init__(self, *args, **kw):
|
|
64
|
+
# type: (Broker, *Any, **Any) -> None
|
|
65
|
+
integration = sentry_sdk.get_client().get_integration(DramatiqIntegration)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
middleware = kw.pop("middleware")
|
|
69
|
+
except KeyError:
|
|
70
|
+
# Unfortunately Broker and StubBroker allows middleware to be
|
|
71
|
+
# passed in as positional arguments, whilst RabbitmqBroker and
|
|
72
|
+
# RedisBroker does not.
|
|
73
|
+
if len(args) == 1:
|
|
74
|
+
middleware = args[0]
|
|
75
|
+
args = [] # type: ignore
|
|
76
|
+
else:
|
|
77
|
+
middleware = None
|
|
78
|
+
|
|
79
|
+
if middleware is None:
|
|
80
|
+
middleware = list(m() for m in default_middleware)
|
|
81
|
+
else:
|
|
82
|
+
middleware = list(middleware)
|
|
83
|
+
|
|
84
|
+
if integration is not None:
|
|
85
|
+
middleware = [m for m in middleware if not isinstance(m, SentryMiddleware)]
|
|
86
|
+
middleware.insert(0, SentryMiddleware())
|
|
87
|
+
|
|
88
|
+
kw["middleware"] = middleware
|
|
89
|
+
original_broker__init__(self, *args, **kw)
|
|
90
|
+
|
|
91
|
+
Broker.__init__ = sentry_patched_broker__init__
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SentryMiddleware(Middleware): # type: ignore[misc]
|
|
95
|
+
"""
|
|
96
|
+
A Dramatiq middleware that automatically captures and sends
|
|
97
|
+
exceptions to Sentry.
|
|
98
|
+
|
|
99
|
+
This is automatically added to every instantiated broker via the
|
|
100
|
+
DramatiqIntegration.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
SENTRY_HEADERS_NAME = "_sentry_headers"
|
|
104
|
+
|
|
105
|
+
def before_enqueue(self, broker, message, delay):
|
|
106
|
+
# type: (Broker, Message[R], int) -> None
|
|
107
|
+
integration = sentry_sdk.get_client().get_integration(DramatiqIntegration)
|
|
108
|
+
if integration is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
message.options[self.SENTRY_HEADERS_NAME] = {
|
|
112
|
+
BAGGAGE_HEADER_NAME: get_baggage(),
|
|
113
|
+
SENTRY_TRACE_HEADER_NAME: get_traceparent(),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def before_process_message(self, broker, message):
|
|
117
|
+
# type: (Broker, Message[R]) -> None
|
|
118
|
+
integration = sentry_sdk.get_client().get_integration(DramatiqIntegration)
|
|
119
|
+
if integration is None:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
message._scope_manager = sentry_sdk.isolation_scope()
|
|
123
|
+
scope = message._scope_manager.__enter__()
|
|
124
|
+
scope.clear_breadcrumbs()
|
|
125
|
+
scope.set_extra("dramatiq_message_id", message.message_id)
|
|
126
|
+
scope.add_event_processor(_make_message_event_processor(message, integration))
|
|
127
|
+
|
|
128
|
+
sentry_headers = message.options.get(self.SENTRY_HEADERS_NAME) or {}
|
|
129
|
+
if "retries" in message.options:
|
|
130
|
+
# start new trace in case of retrying
|
|
131
|
+
sentry_headers = {}
|
|
132
|
+
|
|
133
|
+
transaction = continue_trace(
|
|
134
|
+
sentry_headers,
|
|
135
|
+
name=message.actor_name,
|
|
136
|
+
op=OP.QUEUE_TASK_DRAMATIQ,
|
|
137
|
+
source=TransactionSource.TASK,
|
|
138
|
+
origin=DramatiqIntegration.origin,
|
|
139
|
+
)
|
|
140
|
+
transaction.set_status(SPANSTATUS.OK)
|
|
141
|
+
sentry_sdk.start_transaction(
|
|
142
|
+
transaction,
|
|
143
|
+
name=message.actor_name,
|
|
144
|
+
op=OP.QUEUE_TASK_DRAMATIQ,
|
|
145
|
+
source=TransactionSource.TASK,
|
|
146
|
+
)
|
|
147
|
+
transaction.__enter__()
|
|
148
|
+
|
|
149
|
+
def after_process_message(self, broker, message, *, result=None, exception=None):
|
|
150
|
+
# type: (Broker, Message[R], Optional[Any], Optional[Exception]) -> None
|
|
151
|
+
integration = sentry_sdk.get_client().get_integration(DramatiqIntegration)
|
|
152
|
+
if integration is None:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
actor = broker.get_actor(message.actor_name)
|
|
156
|
+
throws = message.options.get("throws") or actor.options.get("throws")
|
|
157
|
+
|
|
158
|
+
scope_manager = message._scope_manager
|
|
159
|
+
transaction = sentry_sdk.get_current_scope().transaction
|
|
160
|
+
if not transaction:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
is_event_capture_required = (
|
|
164
|
+
exception is not None
|
|
165
|
+
and not (throws and isinstance(exception, throws))
|
|
166
|
+
and not isinstance(exception, Retry)
|
|
167
|
+
)
|
|
168
|
+
if not is_event_capture_required:
|
|
169
|
+
# normal transaction finish
|
|
170
|
+
transaction.__exit__(None, None, None)
|
|
171
|
+
scope_manager.__exit__(None, None, None)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
event, hint = event_from_exception(
|
|
175
|
+
exception, # type: ignore[arg-type]
|
|
176
|
+
client_options=sentry_sdk.get_client().options,
|
|
177
|
+
mechanism={
|
|
178
|
+
"type": DramatiqIntegration.identifier,
|
|
179
|
+
"handled": False,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
183
|
+
# transaction error
|
|
184
|
+
transaction.__exit__(type(exception), exception, None)
|
|
185
|
+
scope_manager.__exit__(type(exception), exception, None)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _make_message_event_processor(message, integration):
|
|
189
|
+
# type: (Message[R], DramatiqIntegration) -> Callable[[Event, Hint], Optional[Event]]
|
|
190
|
+
|
|
191
|
+
def inner(event, hint):
|
|
192
|
+
# type: (Event, Hint) -> Optional[Event]
|
|
193
|
+
with capture_internal_exceptions():
|
|
194
|
+
DramatiqMessageExtractor(message).extract_into_event(event)
|
|
195
|
+
|
|
196
|
+
return event
|
|
197
|
+
|
|
198
|
+
return inner
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class DramatiqMessageExtractor:
|
|
202
|
+
def __init__(self, message):
|
|
203
|
+
# type: (Message[R]) -> None
|
|
204
|
+
self.message_data = dict(message.asdict())
|
|
205
|
+
|
|
206
|
+
def content_length(self):
|
|
207
|
+
# type: () -> int
|
|
208
|
+
return len(json.dumps(self.message_data))
|
|
209
|
+
|
|
210
|
+
def extract_into_event(self, event):
|
|
211
|
+
# type: (Event) -> None
|
|
212
|
+
client = sentry_sdk.get_client()
|
|
213
|
+
if not client.is_active():
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
contexts = event.setdefault("contexts", {})
|
|
217
|
+
request_info = contexts.setdefault("dramatiq", {})
|
|
218
|
+
request_info["type"] = "dramatiq"
|
|
219
|
+
|
|
220
|
+
data = None # type: Optional[Union[AnnotatedValue, Dict[str, Any]]]
|
|
221
|
+
if not request_body_within_bounds(client, self.content_length()):
|
|
222
|
+
data = AnnotatedValue.removed_because_over_size_limit()
|
|
223
|
+
else:
|
|
224
|
+
data = self.message_data
|
|
225
|
+
|
|
226
|
+
request_info["data"] = data
|