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,293 @@
1
+ import sentry_sdk
2
+ from sentry_sdk.crons import capture_checkin, MonitorStatus
3
+ from sentry_sdk.integrations import DidNotEnable
4
+ from sentry_sdk.integrations.celery.utils import (
5
+ _get_humanized_interval,
6
+ _now_seconds_since_epoch,
7
+ )
8
+ from sentry_sdk.utils import (
9
+ logger,
10
+ match_regex_list,
11
+ )
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Callable
17
+ from typing import Any, Optional, TypeVar, Union
18
+ from sentry_sdk._types import (
19
+ MonitorConfig,
20
+ MonitorConfigScheduleType,
21
+ MonitorConfigScheduleUnit,
22
+ )
23
+
24
+ F = TypeVar("F", bound=Callable[..., Any])
25
+
26
+
27
+ try:
28
+ from celery import Task, Celery # type: ignore
29
+ from celery.beat import Scheduler # type: ignore
30
+ from celery.schedules import crontab, schedule # type: ignore
31
+ from celery.signals import ( # type: ignore
32
+ task_failure,
33
+ task_success,
34
+ task_retry,
35
+ )
36
+ except ImportError:
37
+ raise DidNotEnable("Celery not installed")
38
+
39
+ try:
40
+ from redbeat.schedulers import RedBeatScheduler # type: ignore
41
+ except ImportError:
42
+ RedBeatScheduler = None
43
+
44
+
45
+ def _get_headers(task):
46
+ # type: (Task) -> dict[str, Any]
47
+ headers = task.request.get("headers") or {}
48
+
49
+ # flatten nested headers
50
+ if "headers" in headers:
51
+ headers.update(headers["headers"])
52
+ del headers["headers"]
53
+
54
+ headers.update(task.request.get("properties") or {})
55
+
56
+ return headers
57
+
58
+
59
+ def _get_monitor_config(celery_schedule, app, monitor_name):
60
+ # type: (Any, Celery, str) -> MonitorConfig
61
+ monitor_config = {} # type: MonitorConfig
62
+ schedule_type = None # type: Optional[MonitorConfigScheduleType]
63
+ schedule_value = None # type: Optional[Union[str, int]]
64
+ schedule_unit = None # type: Optional[MonitorConfigScheduleUnit]
65
+
66
+ if isinstance(celery_schedule, crontab):
67
+ schedule_type = "crontab"
68
+ schedule_value = (
69
+ "{0._orig_minute} "
70
+ "{0._orig_hour} "
71
+ "{0._orig_day_of_month} "
72
+ "{0._orig_month_of_year} "
73
+ "{0._orig_day_of_week}".format(celery_schedule)
74
+ )
75
+ elif isinstance(celery_schedule, schedule):
76
+ schedule_type = "interval"
77
+ (schedule_value, schedule_unit) = _get_humanized_interval(
78
+ celery_schedule.seconds
79
+ )
80
+
81
+ if schedule_unit == "second":
82
+ logger.warning(
83
+ "Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.",
84
+ monitor_name,
85
+ schedule_value,
86
+ )
87
+ return {}
88
+
89
+ else:
90
+ logger.warning(
91
+ "Celery schedule type '%s' not supported by Sentry Crons.",
92
+ type(celery_schedule),
93
+ )
94
+ return {}
95
+
96
+ monitor_config["schedule"] = {}
97
+ monitor_config["schedule"]["type"] = schedule_type
98
+ monitor_config["schedule"]["value"] = schedule_value
99
+
100
+ if schedule_unit is not None:
101
+ monitor_config["schedule"]["unit"] = schedule_unit
102
+
103
+ monitor_config["timezone"] = (
104
+ (
105
+ hasattr(celery_schedule, "tz")
106
+ and celery_schedule.tz is not None
107
+ and str(celery_schedule.tz)
108
+ )
109
+ or app.timezone
110
+ or "UTC"
111
+ )
112
+
113
+ return monitor_config
114
+
115
+
116
+ def _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration):
117
+ # type: (Any, Any, sentry_sdk.integrations.celery.CeleryIntegration) -> None
118
+ """
119
+ Add Sentry Crons information to the schedule_entry headers.
120
+ """
121
+ if not integration.monitor_beat_tasks:
122
+ return
123
+
124
+ monitor_name = schedule_entry.name
125
+
126
+ task_should_be_excluded = match_regex_list(
127
+ monitor_name, integration.exclude_beat_tasks
128
+ )
129
+ if task_should_be_excluded:
130
+ return
131
+
132
+ celery_schedule = schedule_entry.schedule
133
+ app = scheduler.app
134
+
135
+ monitor_config = _get_monitor_config(celery_schedule, app, monitor_name)
136
+
137
+ is_supported_schedule = bool(monitor_config)
138
+ if not is_supported_schedule:
139
+ return
140
+
141
+ headers = schedule_entry.options.pop("headers", {})
142
+ headers.update(
143
+ {
144
+ "sentry-monitor-slug": monitor_name,
145
+ "sentry-monitor-config": monitor_config,
146
+ }
147
+ )
148
+
149
+ check_in_id = capture_checkin(
150
+ monitor_slug=monitor_name,
151
+ monitor_config=monitor_config,
152
+ status=MonitorStatus.IN_PROGRESS,
153
+ )
154
+ headers.update({"sentry-monitor-check-in-id": check_in_id})
155
+
156
+ # Set the Sentry configuration in the options of the ScheduleEntry.
157
+ # Those will be picked up in `apply_async` and added to the headers.
158
+ schedule_entry.options["headers"] = headers
159
+
160
+
161
+ def _wrap_beat_scheduler(original_function):
162
+ # type: (Callable[..., Any]) -> Callable[..., Any]
163
+ """
164
+ Makes sure that:
165
+ - a new Sentry trace is started for each task started by Celery Beat and
166
+ it is propagated to the task.
167
+ - the Sentry Crons information is set in the Celery Beat task's
168
+ headers so that is is monitored with Sentry Crons.
169
+
170
+ After the patched function is called,
171
+ Celery Beat will call apply_async to put the task in the queue.
172
+ """
173
+ # Patch only once
174
+ # Can't use __name__ here, because some of our tests mock original_apply_entry
175
+ already_patched = "sentry_patched_scheduler" in str(original_function)
176
+ if already_patched:
177
+ return original_function
178
+
179
+ from sentry_sdk.integrations.celery import CeleryIntegration
180
+
181
+ def sentry_patched_scheduler(*args, **kwargs):
182
+ # type: (*Any, **Any) -> None
183
+ integration = sentry_sdk.get_client().get_integration(CeleryIntegration)
184
+ if integration is None:
185
+ return original_function(*args, **kwargs)
186
+
187
+ # Tasks started by Celery Beat start a new Trace
188
+ scope = sentry_sdk.get_isolation_scope()
189
+ scope.set_new_propagation_context()
190
+ scope._name = "celery-beat"
191
+
192
+ scheduler, schedule_entry = args
193
+ _apply_crons_data_to_schedule_entry(scheduler, schedule_entry, integration)
194
+
195
+ return original_function(*args, **kwargs)
196
+
197
+ return sentry_patched_scheduler
198
+
199
+
200
+ def _patch_beat_apply_entry():
201
+ # type: () -> None
202
+ Scheduler.apply_entry = _wrap_beat_scheduler(Scheduler.apply_entry)
203
+
204
+
205
+ def _patch_redbeat_apply_async():
206
+ # type: () -> None
207
+ if RedBeatScheduler is None:
208
+ return
209
+
210
+ RedBeatScheduler.apply_async = _wrap_beat_scheduler(RedBeatScheduler.apply_async)
211
+
212
+
213
+ def _setup_celery_beat_signals(monitor_beat_tasks):
214
+ # type: (bool) -> None
215
+ if monitor_beat_tasks:
216
+ task_success.connect(crons_task_success)
217
+ task_failure.connect(crons_task_failure)
218
+ task_retry.connect(crons_task_retry)
219
+
220
+
221
+ def crons_task_success(sender, **kwargs):
222
+ # type: (Task, dict[Any, Any]) -> None
223
+ logger.debug("celery_task_success %s", sender)
224
+ headers = _get_headers(sender)
225
+
226
+ if "sentry-monitor-slug" not in headers:
227
+ return
228
+
229
+ monitor_config = headers.get("sentry-monitor-config", {})
230
+
231
+ start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s")
232
+
233
+ capture_checkin(
234
+ monitor_slug=headers["sentry-monitor-slug"],
235
+ monitor_config=monitor_config,
236
+ check_in_id=headers["sentry-monitor-check-in-id"],
237
+ duration=(
238
+ _now_seconds_since_epoch() - float(start_timestamp_s)
239
+ if start_timestamp_s
240
+ else None
241
+ ),
242
+ status=MonitorStatus.OK,
243
+ )
244
+
245
+
246
+ def crons_task_failure(sender, **kwargs):
247
+ # type: (Task, dict[Any, Any]) -> None
248
+ logger.debug("celery_task_failure %s", sender)
249
+ headers = _get_headers(sender)
250
+
251
+ if "sentry-monitor-slug" not in headers:
252
+ return
253
+
254
+ monitor_config = headers.get("sentry-monitor-config", {})
255
+
256
+ start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s")
257
+
258
+ capture_checkin(
259
+ monitor_slug=headers["sentry-monitor-slug"],
260
+ monitor_config=monitor_config,
261
+ check_in_id=headers["sentry-monitor-check-in-id"],
262
+ duration=(
263
+ _now_seconds_since_epoch() - float(start_timestamp_s)
264
+ if start_timestamp_s
265
+ else None
266
+ ),
267
+ status=MonitorStatus.ERROR,
268
+ )
269
+
270
+
271
+ def crons_task_retry(sender, **kwargs):
272
+ # type: (Task, dict[Any, Any]) -> None
273
+ logger.debug("celery_task_retry %s", sender)
274
+ headers = _get_headers(sender)
275
+
276
+ if "sentry-monitor-slug" not in headers:
277
+ return
278
+
279
+ monitor_config = headers.get("sentry-monitor-config", {})
280
+
281
+ start_timestamp_s = headers.get("sentry-monitor-start-timestamp-s")
282
+
283
+ capture_checkin(
284
+ monitor_slug=headers["sentry-monitor-slug"],
285
+ monitor_config=monitor_config,
286
+ check_in_id=headers["sentry-monitor-check-in-id"],
287
+ duration=(
288
+ _now_seconds_since_epoch() - float(start_timestamp_s)
289
+ if start_timestamp_s
290
+ else None
291
+ ),
292
+ status=MonitorStatus.ERROR,
293
+ )
@@ -0,0 +1,43 @@
1
+ import time
2
+ from typing import TYPE_CHECKING, cast
3
+
4
+ if TYPE_CHECKING:
5
+ from typing import Any, Tuple
6
+ from sentry_sdk._types import MonitorConfigScheduleUnit
7
+
8
+
9
+ def _now_seconds_since_epoch():
10
+ # type: () -> float
11
+ # We cannot use `time.perf_counter()` when dealing with the duration
12
+ # of a Celery task, because the start of a Celery task and
13
+ # the end are recorded in different processes.
14
+ # Start happens in the Celery Beat process,
15
+ # the end in a Celery Worker process.
16
+ return time.time()
17
+
18
+
19
+ def _get_humanized_interval(seconds):
20
+ # type: (float) -> Tuple[int, MonitorConfigScheduleUnit]
21
+ TIME_UNITS = ( # noqa: N806
22
+ ("day", 60 * 60 * 24.0),
23
+ ("hour", 60 * 60.0),
24
+ ("minute", 60.0),
25
+ )
26
+
27
+ seconds = float(seconds)
28
+ for unit, divider in TIME_UNITS:
29
+ if seconds >= divider:
30
+ interval = int(seconds / divider)
31
+ return (interval, cast("MonitorConfigScheduleUnit", unit))
32
+
33
+ return (int(seconds), "second")
34
+
35
+
36
+ class NoOpMgr:
37
+ def __enter__(self):
38
+ # type: () -> None
39
+ return None
40
+
41
+ def __exit__(self, exc_type, exc_value, traceback):
42
+ # type: (Any, Any, Any) -> None
43
+ return None
@@ -1,40 +1,42 @@
1
1
  import sys
2
+ from functools import wraps
2
3
 
3
- from sentry_sdk._compat import reraise
4
- from sentry_sdk.hub import Hub
4
+ import sentry_sdk
5
5
  from sentry_sdk.integrations import Integration, DidNotEnable
6
6
  from sentry_sdk.integrations.aws_lambda import _make_request_event_processor
7
+ from sentry_sdk.tracing import TransactionSource
7
8
  from sentry_sdk.utils import (
8
9
  capture_internal_exceptions,
9
10
  event_from_exception,
11
+ parse_version,
12
+ reraise,
10
13
  )
11
- from sentry_sdk._types import MYPY
12
- from sentry_sdk._functools import wraps
13
14
 
14
- import chalice # type: ignore
15
- from chalice import Chalice, ChaliceViewError
16
- from chalice.app import EventSourceHandler as ChaliceEventSourceHandler # type: ignore
15
+ try:
16
+ import chalice # type: ignore
17
+ from chalice import __version__ as CHALICE_VERSION
18
+ from chalice import Chalice, ChaliceViewError
19
+ from chalice.app import EventSourceHandler as ChaliceEventSourceHandler # type: ignore
20
+ except ImportError:
21
+ raise DidNotEnable("Chalice is not installed")
17
22
 
18
- if MYPY:
23
+ from typing import TYPE_CHECKING
24
+
25
+ if TYPE_CHECKING:
19
26
  from typing import Any
27
+ from typing import Dict
20
28
  from typing import TypeVar
21
29
  from typing import Callable
22
30
 
23
31
  F = TypeVar("F", bound=Callable[..., Any])
24
32
 
25
- try:
26
- from chalice import __version__ as CHALICE_VERSION
27
- except ImportError:
28
- raise DidNotEnable("Chalice is not installed")
29
-
30
33
 
31
34
  class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore
32
35
  def __call__(self, event, context):
33
36
  # type: (Any, Any) -> Any
34
- hub = Hub.current
35
- client = hub.client # type: Any
37
+ client = sentry_sdk.get_client()
36
38
 
37
- with hub.push_scope() as scope:
39
+ with sentry_sdk.isolation_scope() as scope:
38
40
  with capture_internal_exceptions():
39
41
  configured_time = context.get_remaining_time_in_millis()
40
42
  scope.add_event_processor(
@@ -49,8 +51,8 @@ class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore
49
51
  client_options=client.options,
50
52
  mechanism={"type": "chalice", "handled": False},
51
53
  )
52
- hub.capture_event(event, hint=hint)
53
- hub.flush()
54
+ sentry_sdk.capture_event(event, hint=hint)
55
+ client.flush()
54
56
  reraise(*exc_info)
55
57
 
56
58
 
@@ -59,12 +61,15 @@ def _get_view_function_response(app, view_function, function_args):
59
61
  @wraps(view_function)
60
62
  def wrapped_view_function(**function_args):
61
63
  # type: (**Any) -> Any
62
- hub = Hub.current
63
- client = hub.client # type: Any
64
- with hub.push_scope() as scope:
64
+ client = sentry_sdk.get_client()
65
+ with sentry_sdk.isolation_scope() as scope:
65
66
  with capture_internal_exceptions():
66
67
  configured_time = app.lambda_context.get_remaining_time_in_millis()
67
- scope.transaction = app.lambda_context.function_name
68
+ scope.set_transaction_name(
69
+ app.lambda_context.function_name,
70
+ source=TransactionSource.COMPONENT,
71
+ )
72
+
68
73
  scope.add_event_processor(
69
74
  _make_request_event_processor(
70
75
  app.current_request.to_dict(),
@@ -83,8 +88,8 @@ def _get_view_function_response(app, view_function, function_args):
83
88
  client_options=client.options,
84
89
  mechanism={"type": "chalice", "handled": False},
85
90
  )
86
- hub.capture_event(event, hint=hint)
87
- hub.flush()
91
+ sentry_sdk.capture_event(event, hint=hint)
92
+ client.flush()
88
93
  raise
89
94
 
90
95
  return wrapped_view_function # type: ignore
@@ -96,10 +101,12 @@ class ChaliceIntegration(Integration):
96
101
  @staticmethod
97
102
  def setup_once():
98
103
  # type: () -> None
99
- try:
100
- version = tuple(map(int, CHALICE_VERSION.split(".")[:3]))
101
- except (ValueError, TypeError):
104
+
105
+ version = parse_version(CHALICE_VERSION)
106
+
107
+ if version is None:
102
108
  raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION))
109
+
103
110
  if version < (1, 20):
104
111
  old_get_view_function_response = Chalice._get_view_function_response
105
112
  else:
@@ -110,7 +117,7 @@ class ChaliceIntegration(Integration):
110
117
  )
111
118
 
112
119
  def sentry_event_response(app, view_function, function_args):
113
- # type: (Any, F, **Any) -> Any
120
+ # type: (Any, F, Dict[str, Any]) -> Any
114
121
  wrapped_view_function = _get_view_function_response(
115
122
  app, view_function, function_args
116
123
  )
@@ -0,0 +1,177 @@
1
+ import sentry_sdk
2
+ from sentry_sdk.consts import OP, SPANDATA
3
+ from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
4
+ from sentry_sdk.tracing import Span
5
+ from sentry_sdk.scope import should_send_default_pii
6
+ from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled
7
+
8
+ from typing import TYPE_CHECKING, TypeVar
9
+
10
+ # Hack to get new Python features working in older versions
11
+ # without introducing a hard dependency on `typing_extensions`
12
+ # from: https://stackoverflow.com/a/71944042/300572
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Iterator
15
+ from typing import Any, ParamSpec, Callable
16
+ else:
17
+ # Fake ParamSpec
18
+ class ParamSpec:
19
+ def __init__(self, _):
20
+ self.args = None
21
+ self.kwargs = None
22
+
23
+ # Callable[anything] will return None
24
+ class _Callable:
25
+ def __getitem__(self, _):
26
+ return None
27
+
28
+ # Make instances
29
+ Callable = _Callable()
30
+
31
+
32
+ try:
33
+ import clickhouse_driver # type: ignore[import-not-found]
34
+
35
+ except ImportError:
36
+ raise DidNotEnable("clickhouse-driver not installed.")
37
+
38
+
39
+ class ClickhouseDriverIntegration(Integration):
40
+ identifier = "clickhouse_driver"
41
+ origin = f"auto.db.{identifier}"
42
+
43
+ @staticmethod
44
+ def setup_once() -> None:
45
+ _check_minimum_version(ClickhouseDriverIntegration, clickhouse_driver.VERSION)
46
+
47
+ # Every query is done using the Connection's `send_query` function
48
+ clickhouse_driver.connection.Connection.send_query = _wrap_start(
49
+ clickhouse_driver.connection.Connection.send_query
50
+ )
51
+
52
+ # If the query contains parameters then the send_data function is used to send those parameters to clickhouse
53
+ _wrap_send_data()
54
+
55
+ # Every query ends either with the Client's `receive_end_of_query` (no result expected)
56
+ # or its `receive_result` (result expected)
57
+ clickhouse_driver.client.Client.receive_end_of_query = _wrap_end(
58
+ clickhouse_driver.client.Client.receive_end_of_query
59
+ )
60
+ if hasattr(clickhouse_driver.client.Client, "receive_end_of_insert_query"):
61
+ # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query`
62
+ clickhouse_driver.client.Client.receive_end_of_insert_query = _wrap_end(
63
+ clickhouse_driver.client.Client.receive_end_of_insert_query
64
+ )
65
+ clickhouse_driver.client.Client.receive_result = _wrap_end(
66
+ clickhouse_driver.client.Client.receive_result
67
+ )
68
+
69
+
70
+ P = ParamSpec("P")
71
+ T = TypeVar("T")
72
+
73
+
74
+ def _wrap_start(f: Callable[P, T]) -> Callable[P, T]:
75
+ @ensure_integration_enabled(ClickhouseDriverIntegration, f)
76
+ def _inner(*args: P.args, **kwargs: P.kwargs) -> T:
77
+ connection = args[0]
78
+ query = args[1]
79
+ query_id = args[2] if len(args) > 2 else kwargs.get("query_id")
80
+ params = args[3] if len(args) > 3 else kwargs.get("params")
81
+
82
+ span = sentry_sdk.start_span(
83
+ op=OP.DB,
84
+ name=query,
85
+ origin=ClickhouseDriverIntegration.origin,
86
+ )
87
+
88
+ connection._sentry_span = span # type: ignore[attr-defined]
89
+
90
+ _set_db_data(span, connection)
91
+
92
+ span.set_data("query", query)
93
+
94
+ if query_id:
95
+ span.set_data("db.query_id", query_id)
96
+
97
+ if params and should_send_default_pii():
98
+ span.set_data("db.params", params)
99
+
100
+ # run the original code
101
+ ret = f(*args, **kwargs)
102
+
103
+ return ret
104
+
105
+ return _inner
106
+
107
+
108
+ def _wrap_end(f: Callable[P, T]) -> Callable[P, T]:
109
+ def _inner_end(*args: P.args, **kwargs: P.kwargs) -> T:
110
+ res = f(*args, **kwargs)
111
+ instance = args[0]
112
+ span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined]
113
+
114
+ if span is not None:
115
+ if res is not None and should_send_default_pii():
116
+ span.set_data("db.result", res)
117
+
118
+ with capture_internal_exceptions():
119
+ span.scope.add_breadcrumb(
120
+ message=span._data.pop("query"), category="query", data=span._data
121
+ )
122
+
123
+ span.finish()
124
+
125
+ return res
126
+
127
+ return _inner_end
128
+
129
+
130
+ def _wrap_send_data() -> None:
131
+ original_send_data = clickhouse_driver.client.Client.send_data
132
+
133
+ def _inner_send_data( # type: ignore[no-untyped-def] # clickhouse-driver does not type send_data
134
+ self, sample_block, data, types_check=False, columnar=False, *args, **kwargs
135
+ ):
136
+ span = getattr(self.connection, "_sentry_span", None)
137
+
138
+ if span is not None:
139
+ _set_db_data(span, self.connection)
140
+
141
+ if should_send_default_pii():
142
+ db_params = span._data.get("db.params", [])
143
+
144
+ if isinstance(data, (list, tuple)):
145
+ db_params.extend(data)
146
+
147
+ else: # data is a generic iterator
148
+ orig_data = data
149
+
150
+ # Wrap the generator to add items to db.params as they are yielded.
151
+ # This allows us to send the params to Sentry without needing to allocate
152
+ # memory for the entire generator at once.
153
+ def wrapped_generator() -> "Iterator[Any]":
154
+ for item in orig_data:
155
+ db_params.append(item)
156
+ yield item
157
+
158
+ # Replace the original iterator with the wrapped one.
159
+ data = wrapped_generator()
160
+
161
+ span.set_data("db.params", db_params)
162
+
163
+ return original_send_data(
164
+ self, sample_block, data, types_check, columnar, *args, **kwargs
165
+ )
166
+
167
+ clickhouse_driver.client.Client.send_data = _inner_send_data
168
+
169
+
170
+ def _set_db_data(
171
+ span: Span, connection: clickhouse_driver.connection.Connection
172
+ ) -> None:
173
+ span.set_data(SPANDATA.DB_SYSTEM, "clickhouse")
174
+ span.set_data(SPANDATA.SERVER_ADDRESS, connection.host)
175
+ span.set_data(SPANDATA.SERVER_PORT, connection.port)
176
+ span.set_data(SPANDATA.DB_NAME, connection.database)
177
+ span.set_data(SPANDATA.DB_USER, connection.user)