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,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
- from django.template import TemplateSyntaxError # type: ignore
1
+ import functools
2
2
 
3
- if False:
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 # type: ignore
22
+ from django.template.base import Origin
11
23
  except ImportError:
12
24
  # backward compatibility
13
- from django.template.loader import LoaderOrigin as Origin # type: ignore
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. Used for
3
- `DjangoIntegration(transaction_fron="raven_legacy")`.
4
- """
2
+ Copied from raven-python.
5
3
 
6
- from __future__ import absolute_import
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
- if False:
11
- from django.urls.resolvers import URLResolver # type: ignore
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 # type: ignore
17
+ from django.urls.resolvers import URLPattern
16
18
  from typing import Tuple
17
19
  from typing import Union
18
- from re import Pattern # type: ignore
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 # type: ignore
30
+ from django.urls import get_resolver
22
31
  except ImportError:
23
- from django.core.urlresolvers import get_resolver # type: ignore
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(object):
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: (str) -> str
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), pattern)
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
- match = self._resolve(pattern, new_path, parents)
101
- if match:
102
- return match
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(get_regex(p).pattern) for p in parents)
113
- result = prefix + self._simplify(get_regex(pattern).pattern)
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 or path
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