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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +81 -19
  8. sentry_sdk/_types.py +311 -11
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +35 -28
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +20 -11
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +71 -60
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -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,8 +1,16 @@
1
+ import functools
2
+
1
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
2
10
 
3
- from sentry_sdk._types import MYPY
11
+ from typing import TYPE_CHECKING
4
12
 
5
- if MYPY:
13
+ if TYPE_CHECKING:
6
14
  from typing import Any
7
15
  from typing import Dict
8
16
  from typing import Optional
@@ -40,6 +48,65 @@ def get_template_frame_from_exception(exc_value):
40
48
  return None
41
49
 
42
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
+
43
110
  def _get_template_frame_from_debug(debug):
44
111
  # type: (Dict[str, Any]) -> Dict[str, Any]
45
112
  if debug is None:
@@ -1,15 +1,15 @@
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
- from sentry_sdk._types import MYPY
10
+ from typing import TYPE_CHECKING
11
11
 
12
- if MYPY:
12
+ if TYPE_CHECKING:
13
13
  from django.urls.resolvers import URLResolver
14
14
  from typing import Dict
15
15
  from typing import List
@@ -19,6 +19,13 @@ if MYPY:
19
19
  from typing import Union
20
20
  from re import Pattern
21
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
28
+
22
29
  try:
23
30
  from django.urls import get_resolver
24
31
  except ImportError:
@@ -35,9 +42,12 @@ def get_regex(resolver_or_pattern):
35
42
  return regex
36
43
 
37
44
 
38
- 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
39
49
  _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)")
40
- _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)")
50
+ _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)+")
41
51
  _non_named_group_matcher = re.compile(r"\([^\)]+\)")
42
52
  # [foo|bar|baz]
43
53
  _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]")
@@ -46,7 +56,7 @@ class RavenResolver(object):
46
56
  _cache = {} # type: Dict[URLPattern, str]
47
57
 
48
58
  def _simplify(self, pattern):
49
- # type: (str) -> str
59
+ # type: (Union[URLPattern, URLResolver]) -> str
50
60
  r"""
51
61
  Clean up urlpattern regexes into something readable by humans:
52
62
 
@@ -56,11 +66,24 @@ class RavenResolver(object):
56
66
  To:
57
67
  > "{sport_slug}/athletes/{athlete_slug}/"
58
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
+
59
82
  # remove optional params
60
83
  # TODO(dcramer): it'd be nice to change these into [%s] but it currently
61
84
  # conflicts with the other rules because we're doing regexp matches
62
85
  # rather than parsing tokens
63
- 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)
64
87
 
65
88
  # handle named groups first
66
89
  result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result)
@@ -76,6 +99,8 @@ class RavenResolver(object):
76
99
  result.replace("^", "")
77
100
  .replace("$", "")
78
101
  .replace("?", "")
102
+ .replace("\\A", "")
103
+ .replace("\\Z", "")
79
104
  .replace("//", "/")
80
105
  .replace("\\", "")
81
106
  )
@@ -111,8 +136,8 @@ class RavenResolver(object):
111
136
  except KeyError:
112
137
  pass
113
138
 
114
- prefix = "".join(self._simplify(get_regex(p).pattern) for p in parents)
115
- result = prefix + self._simplify(get_regex(pattern).pattern)
139
+ prefix = "".join(self._simplify(p) for p in parents)
140
+ result = prefix + self._simplify(pattern)
116
141
  if not result.startswith("/"):
117
142
  result = "/" + result
118
143
  self._cache[pattern] = result
@@ -125,10 +150,10 @@ class RavenResolver(object):
125
150
  path, # type: str
126
151
  urlconf=None, # type: Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]
127
152
  ):
128
- # type: (...) -> str
153
+ # type: (...) -> Optional[str]
129
154
  resolver = get_resolver(urlconf)
130
155
  match = self._resolve(resolver, path)
131
- return match or path
156
+ return match
132
157
 
133
158
 
134
159
  LEGACY_RESOLVER = RavenResolver()
@@ -1,20 +1,46 @@
1
- from sentry_sdk.hub import Hub
2
- from sentry_sdk._types import MYPY
3
- from sentry_sdk import _functools
1
+ import functools
4
2
 
5
- if MYPY:
3
+ import sentry_sdk
4
+ from sentry_sdk.consts import OP
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
6
9
  from typing import Any
7
10
 
8
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
+
9
24
  def patch_views():
10
25
  # type: () -> None
11
26
 
12
27
  from django.core.handlers.base import BaseHandler
28
+ from django.template.response import SimpleTemplateResponse
13
29
  from sentry_sdk.integrations.django import DjangoIntegration
14
30
 
15
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)
16
42
 
17
- @_functools.wraps(old_make_view_atomic)
43
+ @functools.wraps(old_make_view_atomic)
18
44
  def sentry_patched_make_view_atomic(self, *args, **kwargs):
19
45
  # type: (Any, *Any, **Any) -> Any
20
46
  callback = old_make_view_atomic(self, *args, **kwargs)
@@ -22,22 +48,49 @@ def patch_views():
22
48
  # XXX: The wrapper function is created for every request. Find more
23
49
  # efficient way to wrap views (or build a cache?)
24
50
 
25
- hub = Hub.current
26
- integration = hub.get_integration(DjangoIntegration)
27
-
51
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
28
52
  if integration is not None and integration.middleware_spans:
29
-
30
- @_functools.wraps(callback)
31
- def sentry_wrapped_callback(request, *args, **kwargs):
32
- # type: (Any, *Any, **Any) -> Any
33
- with hub.start_span(
34
- op="django.view", description=request.resolver_match.view_name
35
- ):
36
- return callback(request, *args, **kwargs)
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)
37
62
 
38
63
  else:
39
64
  sentry_wrapped_callback = callback
40
65
 
41
66
  return sentry_wrapped_callback
42
67
 
68
+ SimpleTemplateResponse.render = sentry_patched_render
43
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
@@ -1,20 +1,24 @@
1
1
  import sys
2
2
 
3
- from sentry_sdk.hub import Hub
4
- from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
3
+ import sentry_sdk
4
+ from sentry_sdk.utils import (
5
+ capture_internal_exceptions,
6
+ event_from_exception,
7
+ )
5
8
  from sentry_sdk.integrations import Integration
6
9
 
7
- from sentry_sdk._types import MYPY
10
+ from typing import TYPE_CHECKING
8
11
 
9
- if MYPY:
12
+ if TYPE_CHECKING:
10
13
  from typing import Callable
11
14
  from typing import Any
12
15
  from typing import Type
16
+ from typing import Optional
13
17
 
14
18
  from types import TracebackType
15
19
 
16
20
  Excepthook = Callable[
17
- [Type[BaseException], BaseException, TracebackType],
21
+ [Type[BaseException], BaseException, Optional[TracebackType]],
18
22
  Any,
19
23
  ]
20
24
 
@@ -43,21 +47,23 @@ class ExcepthookIntegration(Integration):
43
47
  def _make_excepthook(old_excepthook):
44
48
  # type: (Excepthook) -> Excepthook
45
49
  def sentry_sdk_excepthook(type_, value, traceback):
46
- # type: (Type[BaseException], BaseException, TracebackType) -> None
47
- hub = Hub.current
48
- integration = hub.get_integration(ExcepthookIntegration)
50
+ # type: (Type[BaseException], BaseException, Optional[TracebackType]) -> None
51
+ integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration)
49
52
 
50
- if integration is not None and _should_send(integration.always_run):
51
- # If an integration is there, a client has to be there.
52
- client = hub.client # type: Any
53
+ # Note: If we replace this with ensure_integration_enabled then
54
+ # we break the exceptiongroup backport;
55
+ # See: https://github.com/getsentry/sentry-python/issues/3097
56
+ if integration is None:
57
+ return old_excepthook(type_, value, traceback)
53
58
 
59
+ if _should_send(integration.always_run):
54
60
  with capture_internal_exceptions():
55
61
  event, hint = event_from_exception(
56
62
  (type_, value, traceback),
57
- client_options=client.options,
63
+ client_options=sentry_sdk.get_client().options,
58
64
  mechanism={"type": "excepthook", "handled": False},
59
65
  )
60
- hub.capture_event(event, hint=hint)
66
+ sentry_sdk.capture_event(event, hint=hint)
61
67
 
62
68
  return old_excepthook(type_, value, traceback)
63
69