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,391 @@
1
+ from datetime import datetime, timezone
2
+ from time import time
3
+ from typing import TYPE_CHECKING, cast
4
+
5
+ from opentelemetry.context import get_value
6
+ from opentelemetry.sdk.trace import SpanProcessor, ReadableSpan as OTelSpan
7
+ from opentelemetry.semconv.trace import SpanAttributes
8
+ from opentelemetry.trace import (
9
+ format_span_id,
10
+ format_trace_id,
11
+ get_current_span,
12
+ SpanKind,
13
+ )
14
+ from opentelemetry.trace.span import (
15
+ INVALID_SPAN_ID,
16
+ INVALID_TRACE_ID,
17
+ )
18
+ from sentry_sdk import get_client, start_transaction
19
+ from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS
20
+ from sentry_sdk.integrations.opentelemetry.consts import (
21
+ SENTRY_BAGGAGE_KEY,
22
+ SENTRY_TRACE_KEY,
23
+ )
24
+ from sentry_sdk.scope import add_global_event_processor
25
+ from sentry_sdk.tracing import Transaction, Span as SentrySpan
26
+ from sentry_sdk.utils import Dsn
27
+
28
+ from urllib3.util import parse_url as urlparse
29
+
30
+ if TYPE_CHECKING:
31
+ from typing import Any, Optional, Union
32
+ from opentelemetry import context as context_api
33
+ from sentry_sdk._types import Event, Hint
34
+
35
+ OPEN_TELEMETRY_CONTEXT = "otel"
36
+ SPAN_MAX_TIME_OPEN_MINUTES = 10
37
+ SPAN_ORIGIN = "auto.otel"
38
+
39
+
40
+ def link_trace_context_to_error_event(event, otel_span_map):
41
+ # type: (Event, dict[str, Union[Transaction, SentrySpan]]) -> Event
42
+ client = get_client()
43
+
44
+ if client.options["instrumenter"] != INSTRUMENTER.OTEL:
45
+ return event
46
+
47
+ if hasattr(event, "type") and event["type"] == "transaction":
48
+ return event
49
+
50
+ otel_span = get_current_span()
51
+ if not otel_span:
52
+ return event
53
+
54
+ ctx = otel_span.get_span_context()
55
+
56
+ if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID:
57
+ return event
58
+
59
+ sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None)
60
+ if not sentry_span:
61
+ return event
62
+
63
+ contexts = event.setdefault("contexts", {})
64
+ contexts.setdefault("trace", {}).update(sentry_span.get_trace_context())
65
+
66
+ return event
67
+
68
+
69
+ class SentrySpanProcessor(SpanProcessor):
70
+ """
71
+ Converts OTel spans into Sentry spans so they can be sent to the Sentry backend.
72
+ """
73
+
74
+ # The mapping from otel span ids to sentry spans
75
+ otel_span_map = {} # type: dict[str, Union[Transaction, SentrySpan]]
76
+
77
+ # The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES
78
+ open_spans = {} # type: dict[int, set[str]]
79
+
80
+ def __new__(cls):
81
+ # type: () -> SentrySpanProcessor
82
+ if not hasattr(cls, "instance"):
83
+ cls.instance = super().__new__(cls)
84
+
85
+ return cls.instance
86
+
87
+ def __init__(self):
88
+ # type: () -> None
89
+ @add_global_event_processor
90
+ def global_event_processor(event, hint):
91
+ # type: (Event, Hint) -> Event
92
+ return link_trace_context_to_error_event(event, self.otel_span_map)
93
+
94
+ def _prune_old_spans(self):
95
+ # type: (SentrySpanProcessor) -> None
96
+ """
97
+ Prune spans that have been open for too long.
98
+ """
99
+ current_time_minutes = int(time() / 60)
100
+ for span_start_minutes in list(
101
+ self.open_spans.keys()
102
+ ): # making a list because we change the dict
103
+ # prune empty open spans buckets
104
+ if self.open_spans[span_start_minutes] == set():
105
+ self.open_spans.pop(span_start_minutes)
106
+
107
+ # prune old buckets
108
+ elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES:
109
+ for span_id in self.open_spans.pop(span_start_minutes):
110
+ self.otel_span_map.pop(span_id, None)
111
+
112
+ def on_start(self, otel_span, parent_context=None):
113
+ # type: (OTelSpan, Optional[context_api.Context]) -> None
114
+ client = get_client()
115
+
116
+ if not client.dsn:
117
+ return
118
+
119
+ try:
120
+ _ = Dsn(client.dsn)
121
+ except Exception:
122
+ return
123
+
124
+ if client.options["instrumenter"] != INSTRUMENTER.OTEL:
125
+ return
126
+
127
+ if not otel_span.get_span_context().is_valid:
128
+ return
129
+
130
+ if self._is_sentry_span(otel_span):
131
+ return
132
+
133
+ trace_data = self._get_trace_data(otel_span, parent_context)
134
+
135
+ parent_span_id = trace_data["parent_span_id"]
136
+ sentry_parent_span = (
137
+ self.otel_span_map.get(parent_span_id) if parent_span_id else None
138
+ )
139
+
140
+ start_timestamp = None
141
+ if otel_span.start_time is not None:
142
+ start_timestamp = datetime.fromtimestamp(
143
+ otel_span.start_time / 1e9, timezone.utc
144
+ ) # OTel spans have nanosecond precision
145
+
146
+ sentry_span = None
147
+ if sentry_parent_span:
148
+ sentry_span = sentry_parent_span.start_child(
149
+ span_id=trace_data["span_id"],
150
+ name=otel_span.name,
151
+ start_timestamp=start_timestamp,
152
+ instrumenter=INSTRUMENTER.OTEL,
153
+ origin=SPAN_ORIGIN,
154
+ )
155
+ else:
156
+ sentry_span = start_transaction(
157
+ name=otel_span.name,
158
+ span_id=trace_data["span_id"],
159
+ parent_span_id=parent_span_id,
160
+ trace_id=trace_data["trace_id"],
161
+ baggage=trace_data["baggage"],
162
+ start_timestamp=start_timestamp,
163
+ instrumenter=INSTRUMENTER.OTEL,
164
+ origin=SPAN_ORIGIN,
165
+ )
166
+
167
+ self.otel_span_map[trace_data["span_id"]] = sentry_span
168
+
169
+ if otel_span.start_time is not None:
170
+ span_start_in_minutes = int(
171
+ otel_span.start_time / 1e9 / 60
172
+ ) # OTel spans have nanosecond precision
173
+ self.open_spans.setdefault(span_start_in_minutes, set()).add(
174
+ trace_data["span_id"]
175
+ )
176
+
177
+ self._prune_old_spans()
178
+
179
+ def on_end(self, otel_span):
180
+ # type: (OTelSpan) -> None
181
+ client = get_client()
182
+
183
+ if client.options["instrumenter"] != INSTRUMENTER.OTEL:
184
+ return
185
+
186
+ span_context = otel_span.get_span_context()
187
+ if not span_context.is_valid:
188
+ return
189
+
190
+ span_id = format_span_id(span_context.span_id)
191
+ sentry_span = self.otel_span_map.pop(span_id, None)
192
+ if not sentry_span:
193
+ return
194
+
195
+ sentry_span.op = otel_span.name
196
+
197
+ self._update_span_with_otel_status(sentry_span, otel_span)
198
+
199
+ if isinstance(sentry_span, Transaction):
200
+ sentry_span.name = otel_span.name
201
+ sentry_span.set_context(
202
+ OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span)
203
+ )
204
+ self._update_transaction_with_otel_data(sentry_span, otel_span)
205
+
206
+ else:
207
+ self._update_span_with_otel_data(sentry_span, otel_span)
208
+
209
+ end_timestamp = None
210
+ if otel_span.end_time is not None:
211
+ end_timestamp = datetime.fromtimestamp(
212
+ otel_span.end_time / 1e9, timezone.utc
213
+ ) # OTel spans have nanosecond precision
214
+
215
+ sentry_span.finish(end_timestamp=end_timestamp)
216
+
217
+ if otel_span.start_time is not None:
218
+ span_start_in_minutes = int(
219
+ otel_span.start_time / 1e9 / 60
220
+ ) # OTel spans have nanosecond precision
221
+ self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id)
222
+
223
+ self._prune_old_spans()
224
+
225
+ def _is_sentry_span(self, otel_span):
226
+ # type: (OTelSpan) -> bool
227
+ """
228
+ Break infinite loop:
229
+ HTTP requests to Sentry are caught by OTel and send again to Sentry.
230
+ """
231
+ otel_span_url = None
232
+ if otel_span.attributes is not None:
233
+ otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL)
234
+ otel_span_url = cast("Optional[str]", otel_span_url)
235
+
236
+ dsn_url = None
237
+ client = get_client()
238
+ if client.dsn:
239
+ dsn_url = Dsn(client.dsn).netloc
240
+
241
+ if otel_span_url and dsn_url and dsn_url in otel_span_url:
242
+ return True
243
+
244
+ return False
245
+
246
+ def _get_otel_context(self, otel_span):
247
+ # type: (OTelSpan) -> dict[str, Any]
248
+ """
249
+ Returns the OTel context for Sentry.
250
+ See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context
251
+ """
252
+ ctx = {}
253
+
254
+ if otel_span.attributes:
255
+ ctx["attributes"] = dict(otel_span.attributes)
256
+
257
+ if otel_span.resource.attributes:
258
+ ctx["resource"] = dict(otel_span.resource.attributes)
259
+
260
+ return ctx
261
+
262
+ def _get_trace_data(self, otel_span, parent_context):
263
+ # type: (OTelSpan, Optional[context_api.Context]) -> dict[str, Any]
264
+ """
265
+ Extracts tracing information from one OTel span and its parent OTel context.
266
+ """
267
+ trace_data = {} # type: dict[str, Any]
268
+ span_context = otel_span.get_span_context()
269
+
270
+ span_id = format_span_id(span_context.span_id)
271
+ trace_data["span_id"] = span_id
272
+
273
+ trace_id = format_trace_id(span_context.trace_id)
274
+ trace_data["trace_id"] = trace_id
275
+
276
+ parent_span_id = (
277
+ format_span_id(otel_span.parent.span_id) if otel_span.parent else None
278
+ )
279
+ trace_data["parent_span_id"] = parent_span_id
280
+
281
+ sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context)
282
+ sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data)
283
+ trace_data["parent_sampled"] = (
284
+ sentry_trace_data["parent_sampled"] if sentry_trace_data else None
285
+ )
286
+
287
+ baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context)
288
+ trace_data["baggage"] = baggage
289
+
290
+ return trace_data
291
+
292
+ def _update_span_with_otel_status(self, sentry_span, otel_span):
293
+ # type: (SentrySpan, OTelSpan) -> None
294
+ """
295
+ Set the Sentry span status from the OTel span
296
+ """
297
+ if otel_span.status.is_unset:
298
+ return
299
+
300
+ if otel_span.status.is_ok:
301
+ sentry_span.set_status(SPANSTATUS.OK)
302
+ return
303
+
304
+ sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR)
305
+
306
+ def _update_span_with_otel_data(self, sentry_span, otel_span):
307
+ # type: (SentrySpan, OTelSpan) -> None
308
+ """
309
+ Convert OTel span data and update the Sentry span with it.
310
+ This should eventually happen on the server when ingesting the spans.
311
+ """
312
+ sentry_span.set_data("otel.kind", otel_span.kind)
313
+
314
+ op = otel_span.name
315
+ description = otel_span.name
316
+
317
+ if otel_span.attributes is not None:
318
+ for key, val in otel_span.attributes.items():
319
+ sentry_span.set_data(key, val)
320
+
321
+ http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
322
+ http_method = cast("Optional[str]", http_method)
323
+
324
+ db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM)
325
+
326
+ if http_method:
327
+ op = "http"
328
+
329
+ if otel_span.kind == SpanKind.SERVER:
330
+ op += ".server"
331
+ elif otel_span.kind == SpanKind.CLIENT:
332
+ op += ".client"
333
+
334
+ description = http_method
335
+
336
+ peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None)
337
+ if peer_name:
338
+ description += " {}".format(peer_name)
339
+
340
+ target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None)
341
+ if target:
342
+ description += " {}".format(target)
343
+
344
+ if not peer_name and not target:
345
+ url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None)
346
+ url = cast("Optional[str]", url)
347
+ if url:
348
+ parsed_url = urlparse(url)
349
+ url = "{}://{}{}".format(
350
+ parsed_url.scheme, parsed_url.netloc, parsed_url.path
351
+ )
352
+ description += " {}".format(url)
353
+
354
+ status_code = otel_span.attributes.get(
355
+ SpanAttributes.HTTP_STATUS_CODE, None
356
+ )
357
+ status_code = cast("Optional[int]", status_code)
358
+ if status_code:
359
+ sentry_span.set_http_status(status_code)
360
+
361
+ elif db_query:
362
+ op = "db"
363
+ statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None)
364
+ statement = cast("Optional[str]", statement)
365
+ if statement:
366
+ description = statement
367
+
368
+ sentry_span.op = op
369
+ sentry_span.description = description
370
+
371
+ def _update_transaction_with_otel_data(self, sentry_span, otel_span):
372
+ # type: (SentrySpan, OTelSpan) -> None
373
+ if otel_span.attributes is None:
374
+ return
375
+
376
+ http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
377
+
378
+ if http_method:
379
+ status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
380
+ status_code = cast("Optional[int]", status_code)
381
+ if status_code:
382
+ sentry_span.set_http_status(status_code)
383
+
384
+ op = "http"
385
+
386
+ if otel_span.kind == SpanKind.SERVER:
387
+ op += ".server"
388
+ elif otel_span.kind == SpanKind.CLIENT:
389
+ op += ".client"
390
+
391
+ sentry_span.op = op
@@ -0,0 +1,82 @@
1
+ from sentry_sdk.integrations import Integration, DidNotEnable
2
+ from sentry_sdk.scope import register_external_propagation_context
3
+ from sentry_sdk.utils import logger, Dsn
4
+ from sentry_sdk.consts import VERSION, EndpointType
5
+
6
+ try:
7
+ from opentelemetry import trace
8
+ from opentelemetry.propagate import set_global_textmap
9
+ from opentelemetry.sdk.trace import TracerProvider
10
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
11
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
12
+
13
+ from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
14
+ except ImportError:
15
+ raise DidNotEnable("opentelemetry-distro[otlp] is not installed")
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from typing import Optional, Dict, Any, Tuple
21
+
22
+
23
+ def otel_propagation_context():
24
+ # type: () -> Optional[Tuple[str, str]]
25
+ """
26
+ Get the (trace_id, span_id) from opentelemetry if exists.
27
+ """
28
+ ctx = trace.get_current_span().get_span_context()
29
+
30
+ if ctx.trace_id == trace.INVALID_TRACE_ID or ctx.span_id == trace.INVALID_SPAN_ID:
31
+ return None
32
+
33
+ return (trace.format_trace_id(ctx.trace_id), trace.format_span_id(ctx.span_id))
34
+
35
+
36
+ def setup_otlp_exporter(dsn=None):
37
+ # type: (Optional[str]) -> None
38
+ tracer_provider = trace.get_tracer_provider()
39
+
40
+ if not isinstance(tracer_provider, TracerProvider):
41
+ logger.debug("[OTLP] No TracerProvider configured by user, creating a new one")
42
+ tracer_provider = TracerProvider()
43
+ trace.set_tracer_provider(tracer_provider)
44
+
45
+ endpoint = None
46
+ headers = None
47
+ if dsn:
48
+ auth = Dsn(dsn).to_auth(f"sentry.python/{VERSION}")
49
+ endpoint = auth.get_api_url(EndpointType.OTLP_TRACES)
50
+ headers = {"X-Sentry-Auth": auth.to_header()}
51
+ logger.debug(f"[OTLP] Sending traces to {endpoint}")
52
+
53
+ otlp_exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
54
+ span_processor = BatchSpanProcessor(otlp_exporter)
55
+ tracer_provider.add_span_processor(span_processor)
56
+
57
+
58
+ class OTLPIntegration(Integration):
59
+ identifier = "otlp"
60
+
61
+ def __init__(self, setup_otlp_exporter=True, setup_propagator=True):
62
+ # type: (bool, bool) -> None
63
+ self.setup_otlp_exporter = setup_otlp_exporter
64
+ self.setup_propagator = setup_propagator
65
+
66
+ @staticmethod
67
+ def setup_once():
68
+ # type: () -> None
69
+ logger.debug("[OTLP] Setting up trace linking for all events")
70
+ register_external_propagation_context(otel_propagation_context)
71
+
72
+ def setup_once_with_options(self, options=None):
73
+ # type: (Optional[Dict[str, Any]]) -> None
74
+ if self.setup_otlp_exporter:
75
+ logger.debug("[OTLP] Setting up OTLP exporter")
76
+ dsn = options.get("dsn") if options else None # type: Optional[str]
77
+ setup_otlp_exporter(dsn)
78
+
79
+ if self.setup_propagator:
80
+ logger.debug("[OTLP] Setting up propagator for distributed tracing")
81
+ # TODO-neel better propagator support, chain with existing ones if possible instead of replacing
82
+ set_global_textmap(SentryPropagator())
@@ -1,14 +1,14 @@
1
- from __future__ import absolute_import
2
-
3
1
  import ast
4
2
 
5
- from sentry_sdk import Hub, serializer
6
- from sentry_sdk._types import MYPY
3
+ import sentry_sdk
4
+ from sentry_sdk import serializer
7
5
  from sentry_sdk.integrations import Integration, DidNotEnable
8
6
  from sentry_sdk.scope import add_global_event_processor
9
7
  from sentry_sdk.utils import walk_exception_chain, iter_stacks
10
8
 
11
- if MYPY:
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
12
  from typing import Optional, Dict, Any, Tuple, List
13
13
  from types import FrameType
14
14
 
@@ -41,7 +41,7 @@ class PureEvalIntegration(Integration):
41
41
  @add_global_event_processor
42
42
  def add_executing_info(event, hint):
43
43
  # type: (Event, Optional[Hint]) -> Optional[Event]
44
- if Hub.current.get_integration(PureEvalIntegration) is None:
44
+ if sentry_sdk.get_client().get_integration(PureEvalIntegration) is None:
45
45
  return event
46
46
 
47
47
  if hint is None:
@@ -104,29 +104,38 @@ def pure_eval_frame(frame):
104
104
  expressions = evaluator.interesting_expressions_grouped(scope)
105
105
 
106
106
  def closeness(expression):
107
- # type: (Tuple[List[Any], Any]) -> int
107
+ # type: (Tuple[List[Any], Any]) -> Tuple[int, int]
108
108
  # Prioritise expressions with a node closer to the statement executed
109
109
  # without being after that statement
110
110
  # A higher return value is better - the expression will appear
111
111
  # earlier in the list of values and is less likely to be trimmed
112
112
  nodes, _value = expression
113
+
114
+ def start(n):
115
+ # type: (ast.expr) -> Tuple[int, int]
116
+ return (n.lineno, n.col_offset)
117
+
113
118
  nodes_before_stmt = [
114
- node for node in nodes if node.first_token.startpos < stmt.last_token.endpos
119
+ node
120
+ for node in nodes
121
+ if start(node) < stmt.last_token.end # type: ignore
115
122
  ]
116
123
  if nodes_before_stmt:
117
124
  # The position of the last node before or in the statement
118
- return max(node.first_token.startpos for node in nodes_before_stmt)
125
+ return max(start(node) for node in nodes_before_stmt)
119
126
  else:
120
127
  # The position of the first node after the statement
121
128
  # Negative means it's always lower priority than nodes that come before
122
129
  # Less negative means closer to the statement and higher priority
123
- return -min(node.first_token.startpos for node in nodes)
130
+ lineno, col_offset = min(start(node) for node in nodes)
131
+ return (-lineno, -col_offset)
124
132
 
125
133
  # This adds the first_token and last_token attributes to nodes
126
134
  atok = source.asttokens()
127
135
 
128
136
  expressions.sort(key=closeness, reverse=True)
129
- return {
137
+ vars = {
130
138
  atok.get_text(nodes[0]): value
131
139
  for nodes, value in expressions[: serializer.MAX_DATABAG_BREADTH]
132
140
  }
141
+ return serializer.serialize(vars, is_vars=True)
@@ -0,0 +1,47 @@
1
+ from sentry_sdk.integrations import DidNotEnable, Integration
2
+
3
+
4
+ try:
5
+ import pydantic_ai # type: ignore
6
+ except ImportError:
7
+ raise DidNotEnable("pydantic-ai not installed")
8
+
9
+
10
+ from .patches import (
11
+ _patch_agent_run,
12
+ _patch_graph_nodes,
13
+ _patch_model_request,
14
+ _patch_tool_execution,
15
+ )
16
+
17
+
18
+ class PydanticAIIntegration(Integration):
19
+ identifier = "pydantic_ai"
20
+ origin = f"auto.ai.{identifier}"
21
+
22
+ def __init__(self, include_prompts=True):
23
+ # type: (bool) -> None
24
+ """
25
+ Initialize the Pydantic AI integration.
26
+
27
+ Args:
28
+ include_prompts: Whether to include prompts and messages in span data.
29
+ Requires send_default_pii=True. Defaults to True.
30
+ """
31
+ self.include_prompts = include_prompts
32
+
33
+ @staticmethod
34
+ def setup_once():
35
+ # type: () -> None
36
+ """
37
+ Set up the pydantic-ai integration.
38
+
39
+ This patches the key methods in pydantic-ai to create Sentry spans for:
40
+ - Agent invocations (Agent.run methods)
41
+ - Model requests (AI client calls)
42
+ - Tool executions
43
+ """
44
+ _patch_agent_run()
45
+ _patch_graph_nodes()
46
+ _patch_model_request()
47
+ _patch_tool_execution()
@@ -0,0 +1 @@
1
+ SPAN_ORIGIN = "auto.ai.pydantic_ai"
@@ -0,0 +1,4 @@
1
+ from .agent_run import _patch_agent_run # noqa: F401
2
+ from .graph_nodes import _patch_graph_nodes # noqa: F401
3
+ from .model_request import _patch_model_request # noqa: F401
4
+ from .tools import _patch_tool_execution # noqa: F401