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,246 @@
1
+ import sentry_sdk
2
+ from sentry_sdk.ai.utils import set_data_normalized
3
+ from sentry_sdk.consts import OP, SPANDATA
4
+ from sentry_sdk.utils import safe_serialize
5
+
6
+ from ..consts import SPAN_ORIGIN
7
+ from ..utils import (
8
+ _set_agent_data,
9
+ _set_available_tools,
10
+ _set_model_data,
11
+ _should_send_prompts,
12
+ _get_model_name,
13
+ get_current_agent,
14
+ get_is_streaming,
15
+ )
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from typing import Any, List, Dict
21
+ from pydantic_ai.usage import RequestUsage # type: ignore
22
+
23
+ try:
24
+ from pydantic_ai.messages import ( # type: ignore
25
+ BaseToolCallPart,
26
+ BaseToolReturnPart,
27
+ SystemPromptPart,
28
+ UserPromptPart,
29
+ TextPart,
30
+ ThinkingPart,
31
+ )
32
+ except ImportError:
33
+ # Fallback if these classes are not available
34
+ BaseToolCallPart = None
35
+ BaseToolReturnPart = None
36
+ SystemPromptPart = None
37
+ UserPromptPart = None
38
+ TextPart = None
39
+ ThinkingPart = None
40
+
41
+
42
+ def _set_usage_data(span, usage):
43
+ # type: (sentry_sdk.tracing.Span, RequestUsage) -> None
44
+ """Set token usage data on a span."""
45
+ if usage is None:
46
+ return
47
+
48
+ if hasattr(usage, "input_tokens") and usage.input_tokens is not None:
49
+ span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens)
50
+
51
+ if hasattr(usage, "output_tokens") and usage.output_tokens is not None:
52
+ span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage.output_tokens)
53
+
54
+ if hasattr(usage, "total_tokens") and usage.total_tokens is not None:
55
+ span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens)
56
+
57
+
58
+ def _set_input_messages(span, messages):
59
+ # type: (sentry_sdk.tracing.Span, Any) -> None
60
+ """Set input messages data on a span."""
61
+ if not _should_send_prompts():
62
+ return
63
+
64
+ if not messages:
65
+ return
66
+
67
+ try:
68
+ formatted_messages = []
69
+ system_prompt = None
70
+
71
+ # Extract system prompt from any ModelRequest with instructions
72
+ for msg in messages:
73
+ if hasattr(msg, "instructions") and msg.instructions:
74
+ system_prompt = msg.instructions
75
+ break
76
+
77
+ # Add system prompt as first message if present
78
+ if system_prompt:
79
+ formatted_messages.append(
80
+ {"role": "system", "content": [{"type": "text", "text": system_prompt}]}
81
+ )
82
+
83
+ for msg in messages:
84
+ if hasattr(msg, "parts"):
85
+ for part in msg.parts:
86
+ role = "user"
87
+ # Use isinstance checks with proper base classes
88
+ if SystemPromptPart and isinstance(part, SystemPromptPart):
89
+ role = "system"
90
+ elif (
91
+ (TextPart and isinstance(part, TextPart))
92
+ or (ThinkingPart and isinstance(part, ThinkingPart))
93
+ or (BaseToolCallPart and isinstance(part, BaseToolCallPart))
94
+ ):
95
+ role = "assistant"
96
+ elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart):
97
+ role = "tool"
98
+
99
+ content = [] # type: List[Dict[str, Any] | str]
100
+ tool_calls = None
101
+ tool_call_id = None
102
+
103
+ # Handle ToolCallPart (assistant requesting tool use)
104
+ if BaseToolCallPart and isinstance(part, BaseToolCallPart):
105
+ tool_call_data = {}
106
+ if hasattr(part, "tool_name"):
107
+ tool_call_data["name"] = part.tool_name
108
+ if hasattr(part, "args"):
109
+ tool_call_data["arguments"] = safe_serialize(part.args)
110
+ if tool_call_data:
111
+ tool_calls = [tool_call_data]
112
+ # Handle ToolReturnPart (tool result)
113
+ elif BaseToolReturnPart and isinstance(part, BaseToolReturnPart):
114
+ if hasattr(part, "tool_name"):
115
+ tool_call_id = part.tool_name
116
+ if hasattr(part, "content"):
117
+ content.append({"type": "text", "text": str(part.content)})
118
+ # Handle regular content
119
+ elif hasattr(part, "content"):
120
+ if isinstance(part.content, str):
121
+ content.append({"type": "text", "text": part.content})
122
+ elif isinstance(part.content, list):
123
+ for item in part.content:
124
+ if isinstance(item, str):
125
+ content.append({"type": "text", "text": item})
126
+ else:
127
+ content.append(safe_serialize(item))
128
+ else:
129
+ content.append({"type": "text", "text": str(part.content)})
130
+
131
+ # Add message if we have content or tool calls
132
+ if content or tool_calls:
133
+ message = {"role": role} # type: Dict[str, Any]
134
+ if content:
135
+ message["content"] = content
136
+ if tool_calls:
137
+ message["tool_calls"] = tool_calls
138
+ if tool_call_id:
139
+ message["tool_call_id"] = tool_call_id
140
+ formatted_messages.append(message)
141
+
142
+ if formatted_messages:
143
+ set_data_normalized(
144
+ span, SPANDATA.GEN_AI_REQUEST_MESSAGES, formatted_messages, unpack=False
145
+ )
146
+ except Exception:
147
+ # If we fail to format messages, just skip it
148
+ pass
149
+
150
+
151
+ def _set_output_data(span, response):
152
+ # type: (sentry_sdk.tracing.Span, Any) -> None
153
+ """Set output data on a span."""
154
+ if not _should_send_prompts():
155
+ return
156
+
157
+ if not response:
158
+ return
159
+
160
+ set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_MODEL, response.model_name)
161
+ try:
162
+ # Extract text from ModelResponse
163
+ if hasattr(response, "parts"):
164
+ texts = []
165
+ tool_calls = []
166
+
167
+ for part in response.parts:
168
+ if TextPart and isinstance(part, TextPart) and hasattr(part, "content"):
169
+ texts.append(part.content)
170
+ elif BaseToolCallPart and isinstance(part, BaseToolCallPart):
171
+ tool_call_data = {
172
+ "type": "function",
173
+ }
174
+ if hasattr(part, "tool_name"):
175
+ tool_call_data["name"] = part.tool_name
176
+ if hasattr(part, "args"):
177
+ tool_call_data["arguments"] = safe_serialize(part.args)
178
+ tool_calls.append(tool_call_data)
179
+
180
+ if texts:
181
+ set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, texts)
182
+
183
+ if tool_calls:
184
+ span.set_data(
185
+ SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS, safe_serialize(tool_calls)
186
+ )
187
+
188
+ except Exception:
189
+ # If we fail to format output, just skip it
190
+ pass
191
+
192
+
193
+ def ai_client_span(messages, agent, model, model_settings):
194
+ # type: (Any, Any, Any, Any) -> sentry_sdk.tracing.Span
195
+ """Create a span for an AI client call (model request).
196
+
197
+ Args:
198
+ messages: Full conversation history (list of messages)
199
+ agent: Agent object
200
+ model: Model object
201
+ model_settings: Model settings
202
+ """
203
+ # Determine model name for span name
204
+ model_obj = model
205
+ if agent and hasattr(agent, "model"):
206
+ model_obj = agent.model
207
+
208
+ model_name = _get_model_name(model_obj) or "unknown"
209
+
210
+ span = sentry_sdk.start_span(
211
+ op=OP.GEN_AI_CHAT,
212
+ name=f"chat {model_name}",
213
+ origin=SPAN_ORIGIN,
214
+ )
215
+
216
+ span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
217
+
218
+ _set_agent_data(span, agent)
219
+ _set_model_data(span, model, model_settings)
220
+
221
+ # Set streaming flag from contextvar
222
+ span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, get_is_streaming())
223
+
224
+ # Add available tools if agent is available
225
+ agent_obj = agent or get_current_agent()
226
+ _set_available_tools(span, agent_obj)
227
+
228
+ # Set input messages (full conversation history)
229
+ if messages:
230
+ _set_input_messages(span, messages)
231
+
232
+ return span
233
+
234
+
235
+ def update_ai_client_span(span, model_response):
236
+ # type: (sentry_sdk.tracing.Span, Any) -> None
237
+ """Update the AI client span with response data."""
238
+ if not span:
239
+ return
240
+
241
+ # Set usage data if available
242
+ if model_response and hasattr(model_response, "usage"):
243
+ _set_usage_data(span, model_response.usage)
244
+
245
+ # Set output data
246
+ _set_output_data(span, model_response)
@@ -0,0 +1,49 @@
1
+ import sentry_sdk
2
+ from sentry_sdk.consts import OP, SPANDATA
3
+ from sentry_sdk.utils import safe_serialize
4
+
5
+ from ..consts import SPAN_ORIGIN
6
+ from ..utils import _set_agent_data, _should_send_prompts
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any, Optional
12
+
13
+
14
+ def execute_tool_span(tool_name, tool_args, agent, tool_type="function"):
15
+ # type: (str, Any, Any, str) -> sentry_sdk.tracing.Span
16
+ """Create a span for tool execution.
17
+
18
+ Args:
19
+ tool_name: The name of the tool being executed
20
+ tool_args: The arguments passed to the tool
21
+ agent: The agent executing the tool
22
+ tool_type: The type of tool ("function" for regular tools, "mcp" for MCP services)
23
+ """
24
+ span = sentry_sdk.start_span(
25
+ op=OP.GEN_AI_EXECUTE_TOOL,
26
+ name=f"execute_tool {tool_name}",
27
+ origin=SPAN_ORIGIN,
28
+ )
29
+
30
+ span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool")
31
+ span.set_data(SPANDATA.GEN_AI_TOOL_TYPE, tool_type)
32
+ span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name)
33
+
34
+ _set_agent_data(span, agent)
35
+
36
+ if _should_send_prompts() and tool_args is not None:
37
+ span.set_data(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args))
38
+
39
+ return span
40
+
41
+
42
+ def update_execute_tool_span(span, result):
43
+ # type: (sentry_sdk.tracing.Span, Any) -> None
44
+ """Update the execute tool span with the result."""
45
+ if not span:
46
+ return
47
+
48
+ if _should_send_prompts() and result is not None:
49
+ span.set_data(SPANDATA.GEN_AI_TOOL_OUTPUT, safe_serialize(result))
@@ -0,0 +1,112 @@
1
+ import sentry_sdk
2
+ from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized
3
+ from sentry_sdk.consts import OP, SPANDATA
4
+
5
+ from ..consts import SPAN_ORIGIN
6
+ from ..utils import (
7
+ _set_agent_data,
8
+ _set_available_tools,
9
+ _set_model_data,
10
+ _should_send_prompts,
11
+ )
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any
17
+
18
+
19
+ def invoke_agent_span(user_prompt, agent, model, model_settings, is_streaming=False):
20
+ # type: (Any, Any, Any, Any, bool) -> sentry_sdk.tracing.Span
21
+ """Create a span for invoking the agent."""
22
+ # Determine agent name for span
23
+ name = "agent"
24
+ if agent and getattr(agent, "name", None):
25
+ name = agent.name
26
+
27
+ span = get_start_span_function()(
28
+ op=OP.GEN_AI_INVOKE_AGENT,
29
+ name=f"invoke_agent {name}",
30
+ origin=SPAN_ORIGIN,
31
+ )
32
+
33
+ span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")
34
+
35
+ _set_agent_data(span, agent)
36
+ _set_model_data(span, model, model_settings)
37
+ _set_available_tools(span, agent)
38
+
39
+ # Add user prompt and system prompts if available and prompts are enabled
40
+ if _should_send_prompts():
41
+ messages = []
42
+
43
+ # Add system prompts (both instructions and system_prompt)
44
+ system_texts = []
45
+
46
+ if agent:
47
+ # Check for system_prompt
48
+ system_prompts = getattr(agent, "_system_prompts", None) or []
49
+ for prompt in system_prompts:
50
+ if isinstance(prompt, str):
51
+ system_texts.append(prompt)
52
+
53
+ # Check for instructions (stored in _instructions)
54
+ instructions = getattr(agent, "_instructions", None)
55
+ if instructions:
56
+ if isinstance(instructions, str):
57
+ system_texts.append(instructions)
58
+ elif isinstance(instructions, (list, tuple)):
59
+ for instr in instructions:
60
+ if isinstance(instr, str):
61
+ system_texts.append(instr)
62
+ elif callable(instr):
63
+ # Skip dynamic/callable instructions
64
+ pass
65
+
66
+ # Add all system texts as system messages
67
+ for system_text in system_texts:
68
+ messages.append(
69
+ {
70
+ "content": [{"text": system_text, "type": "text"}],
71
+ "role": "system",
72
+ }
73
+ )
74
+
75
+ # Add user prompt
76
+ if user_prompt:
77
+ if isinstance(user_prompt, str):
78
+ messages.append(
79
+ {
80
+ "content": [{"text": user_prompt, "type": "text"}],
81
+ "role": "user",
82
+ }
83
+ )
84
+ elif isinstance(user_prompt, list):
85
+ # Handle list of user content
86
+ content = []
87
+ for item in user_prompt:
88
+ if isinstance(item, str):
89
+ content.append({"text": item, "type": "text"})
90
+ if content:
91
+ messages.append(
92
+ {
93
+ "content": content,
94
+ "role": "user",
95
+ }
96
+ )
97
+
98
+ if messages:
99
+ set_data_normalized(
100
+ span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False
101
+ )
102
+
103
+ return span
104
+
105
+
106
+ def update_invoke_agent_span(span, output):
107
+ # type: (sentry_sdk.tracing.Span, Any) -> None
108
+ """Update and close the invoke agent span."""
109
+ if span and _should_send_prompts() and output:
110
+ set_data_normalized(
111
+ span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False
112
+ )
@@ -0,0 +1,223 @@
1
+ import sentry_sdk
2
+ from contextvars import ContextVar
3
+ from sentry_sdk.consts import SPANDATA
4
+ from sentry_sdk.scope import should_send_default_pii
5
+ from sentry_sdk.tracing_utils import set_span_errored
6
+ from sentry_sdk.utils import event_from_exception, safe_serialize
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any, Optional
12
+
13
+
14
+ # Store the current agent context in a contextvar for re-entrant safety
15
+ # Using a list as a stack to support nested agent calls
16
+ _agent_context_stack = ContextVar("pydantic_ai_agent_context_stack", default=[]) # type: ContextVar[list[dict[str, Any]]]
17
+
18
+
19
+ def push_agent(agent, is_streaming=False):
20
+ # type: (Any, bool) -> None
21
+ """Push an agent context onto the stack along with its streaming flag."""
22
+ stack = _agent_context_stack.get().copy()
23
+ stack.append({"agent": agent, "is_streaming": is_streaming})
24
+ _agent_context_stack.set(stack)
25
+
26
+
27
+ def pop_agent():
28
+ # type: () -> None
29
+ """Pop an agent context from the stack."""
30
+ stack = _agent_context_stack.get().copy()
31
+ if stack:
32
+ stack.pop()
33
+ _agent_context_stack.set(stack)
34
+
35
+
36
+ def get_current_agent():
37
+ # type: () -> Any
38
+ """Get the current agent from the contextvar stack."""
39
+ stack = _agent_context_stack.get()
40
+ if stack:
41
+ return stack[-1]["agent"]
42
+ return None
43
+
44
+
45
+ def get_is_streaming():
46
+ # type: () -> bool
47
+ """Get the streaming flag from the contextvar stack."""
48
+ stack = _agent_context_stack.get()
49
+ if stack:
50
+ return stack[-1].get("is_streaming", False)
51
+ return False
52
+
53
+
54
+ def _should_send_prompts():
55
+ # type: () -> bool
56
+ """
57
+ Check if prompts should be sent to Sentry.
58
+
59
+ This checks both send_default_pii and the include_prompts integration setting.
60
+ """
61
+ if not should_send_default_pii():
62
+ return False
63
+
64
+ from . import PydanticAIIntegration
65
+
66
+ # Get the integration instance from the client
67
+ integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration)
68
+
69
+ if integration is None:
70
+ return False
71
+
72
+ return getattr(integration, "include_prompts", False)
73
+
74
+
75
+ def _set_agent_data(span, agent):
76
+ # type: (sentry_sdk.tracing.Span, Any) -> None
77
+ """Set agent-related data on a span.
78
+
79
+ Args:
80
+ span: The span to set data on
81
+ agent: Agent object (can be None, will try to get from contextvar if not provided)
82
+ """
83
+ # Extract agent name from agent object or contextvar
84
+ agent_obj = agent
85
+ if not agent_obj:
86
+ # Try to get from contextvar
87
+ agent_obj = get_current_agent()
88
+
89
+ if agent_obj and hasattr(agent_obj, "name") and agent_obj.name:
90
+ span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_obj.name)
91
+
92
+
93
+ def _get_model_name(model_obj):
94
+ # type: (Any) -> Optional[str]
95
+ """Extract model name from a model object.
96
+
97
+ Args:
98
+ model_obj: Model object to extract name from
99
+
100
+ Returns:
101
+ Model name string or None if not found
102
+ """
103
+ if not model_obj:
104
+ return None
105
+
106
+ if hasattr(model_obj, "model_name"):
107
+ return model_obj.model_name
108
+ elif hasattr(model_obj, "name"):
109
+ try:
110
+ return model_obj.name()
111
+ except Exception:
112
+ return str(model_obj)
113
+ elif isinstance(model_obj, str):
114
+ return model_obj
115
+ else:
116
+ return str(model_obj)
117
+
118
+
119
+ def _set_model_data(span, model, model_settings):
120
+ # type: (sentry_sdk.tracing.Span, Any, Any) -> None
121
+ """Set model-related data on a span.
122
+
123
+ Args:
124
+ span: The span to set data on
125
+ model: Model object (can be None, will try to get from agent if not provided)
126
+ model_settings: Model settings (can be None, will try to get from agent if not provided)
127
+ """
128
+ # Try to get agent from contextvar if we need it
129
+ agent_obj = get_current_agent()
130
+
131
+ # Extract model information
132
+ model_obj = model
133
+ if not model_obj and agent_obj and hasattr(agent_obj, "model"):
134
+ model_obj = agent_obj.model
135
+
136
+ if model_obj:
137
+ # Set system from model
138
+ if hasattr(model_obj, "system"):
139
+ span.set_data(SPANDATA.GEN_AI_SYSTEM, model_obj.system)
140
+
141
+ # Set model name
142
+ model_name = _get_model_name(model_obj)
143
+ if model_name:
144
+ span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model_name)
145
+
146
+ # Extract model settings
147
+ settings = model_settings
148
+ if not settings and agent_obj and hasattr(agent_obj, "model_settings"):
149
+ settings = agent_obj.model_settings
150
+
151
+ if settings:
152
+ settings_map = {
153
+ "max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS,
154
+ "temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE,
155
+ "top_p": SPANDATA.GEN_AI_REQUEST_TOP_P,
156
+ "frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY,
157
+ "presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY,
158
+ }
159
+
160
+ # ModelSettings is a TypedDict (dict at runtime), so use dict access
161
+ if isinstance(settings, dict):
162
+ for setting_name, spandata_key in settings_map.items():
163
+ value = settings.get(setting_name)
164
+ if value is not None:
165
+ span.set_data(spandata_key, value)
166
+ else:
167
+ # Fallback for object-style settings
168
+ for setting_name, spandata_key in settings_map.items():
169
+ if hasattr(settings, setting_name):
170
+ value = getattr(settings, setting_name)
171
+ if value is not None:
172
+ span.set_data(spandata_key, value)
173
+
174
+
175
+ def _set_available_tools(span, agent):
176
+ # type: (sentry_sdk.tracing.Span, Any) -> None
177
+ """Set available tools data on a span from an agent's function toolset.
178
+
179
+ Args:
180
+ span: The span to set data on
181
+ agent: Agent object with _function_toolset attribute
182
+ """
183
+ if not agent or not hasattr(agent, "_function_toolset"):
184
+ return
185
+
186
+ try:
187
+ tools = []
188
+ # Get tools from the function toolset
189
+ if hasattr(agent._function_toolset, "tools"):
190
+ for tool_name, tool in agent._function_toolset.tools.items():
191
+ tool_info = {"name": tool_name}
192
+
193
+ # Add description from function_schema if available
194
+ if hasattr(tool, "function_schema"):
195
+ schema = tool.function_schema
196
+ if getattr(schema, "description", None):
197
+ tool_info["description"] = schema.description
198
+
199
+ # Add parameters from json_schema
200
+ if getattr(schema, "json_schema", None):
201
+ tool_info["parameters"] = schema.json_schema
202
+
203
+ tools.append(tool_info)
204
+
205
+ if tools:
206
+ span.set_data(
207
+ SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)
208
+ )
209
+ except Exception:
210
+ # If we can't extract tools, just skip it
211
+ pass
212
+
213
+
214
+ def _capture_exception(exc):
215
+ # type: (Any) -> None
216
+ set_span_errored()
217
+
218
+ event, hint = event_from_exception(
219
+ exc,
220
+ client_options=sentry_sdk.get_client().options,
221
+ mechanism={"type": "pydantic_ai", "handled": False},
222
+ )
223
+ sentry_sdk.capture_event(event, hint=hint)