Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
465aab0
test(pydantic-ai): Consolidate binary blob redaction tests to stop re…
alexander-alderman-webb Apr 7, 2026
f590e32
.
alexander-alderman-webb Apr 7, 2026
d95d78d
fix(pydantic-ai): Set system instructions after instructions are set …
alexander-alderman-webb Apr 7, 2026
de56c78
.
alexander-alderman-webb Apr 7, 2026
2bafd3c
.
alexander-alderman-webb Apr 7, 2026
2d03ece
.
alexander-alderman-webb Apr 7, 2026
371c1bc
add a streaming test
alexander-alderman-webb Apr 7, 2026
b771009
add pydantic-ai to linting requirements
alexander-alderman-webb Apr 7, 2026
13da396
re-add type ignore
alexander-alderman-webb Apr 7, 2026
49dc6f4
move type ignore
alexander-alderman-webb Apr 7, 2026
1f1cc44
add docstring
alexander-alderman-webb Apr 7, 2026
cb41f97
delay messages in stream path
alexander-alderman-webb Apr 7, 2026
3a04323
use hooks
alexander-alderman-webb Apr 7, 2026
6bd8586
type ignores
alexander-alderman-webb Apr 8, 2026
d65fdb1
remove test
alexander-alderman-webb Apr 8, 2026
0c4e443
.
alexander-alderman-webb Apr 8, 2026
ba53213
add annotations
alexander-alderman-webb Apr 8, 2026
1c4fb70
.
alexander-alderman-webb Apr 8, 2026
9a02158
restore image url tests
alexander-alderman-webb Apr 8, 2026
7cf1dc8
add error hook
alexander-alderman-webb Apr 8, 2026
b5b4d63
flip bool
alexander-alderman-webb Apr 8, 2026
d0cb35b
.
alexander-alderman-webb Apr 9, 2026
87bfb08
document
alexander-alderman-webb Apr 9, 2026
80c2ee6
document 2
alexander-alderman-webb Apr 9, 2026
f84ffae
linting
alexander-alderman-webb Apr 9, 2026
2fb567c
fix run sync test
alexander-alderman-webb Apr 9, 2026
1059be9
update
alexander-alderman-webb Apr 9, 2026
2606eef
use early return
alexander-alderman-webb Apr 9, 2026
3452e52
remove print from test
alexander-alderman-webb Apr 9, 2026
396f322
set metadata in agent.__init__
alexander-alderman-webb Apr 9, 2026
622a48f
docstring
alexander-alderman-webb Apr 9, 2026
b72be0f
check none instead of falsy
alexander-alderman-webb Apr 9, 2026
ef7802a
create metadata dict in agent run methods again
alexander-alderman-webb Apr 9, 2026
ffcb5f3
move sentry.init to start of tests
alexander-alderman-webb Apr 10, 2026
a11d942
Merge branch 'master' into webb/pydantic-ai/move-instruction-fetching
alexander-alderman-webb Apr 10, 2026
84a4c11
switch falsy check to is None
alexander-alderman-webb Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions sentry_sdk/integrations/pydantic_ai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from sentry_sdk.integrations import DidNotEnable, Integration
import functools

from sentry_sdk.integrations import DidNotEnable, Integration

try:
import pydantic_ai # type: ignore # noqa: F401
from pydantic_ai import Agent
except ImportError:
raise DidNotEnable("pydantic-ai not installed")

Expand All @@ -14,10 +16,20 @@
_patch_tool_execution,
)

from .spans.ai_client import ai_client_span, update_ai_client_span

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any
from pydantic_ai import ModelRequestContext, RunContext
from pydantic_ai.messages import ModelResponse # type: ignore


class PydanticAIIntegration(Integration):
identifier = "pydantic_ai"
origin = f"auto.ai.{identifier}"
are_request_hooks_available = True

def __init__(
self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True
Expand Down Expand Up @@ -45,6 +57,71 @@
- Tool executions
"""
_patch_agent_run()
_patch_graph_nodes()
_patch_model_request()

try:
from pydantic_ai.capabilities import Hooks # type: ignore
except ImportError:
Hooks = None
# Save to populate ctx.metadata
PydanticAIIntegration.are_request_hooks_available = True

Check warning on line 66 in sentry_sdk/integrations/pydantic_ai/__init__.py

View check run for this annotation

@sentry/warden / warden: code-review

are_request_hooks_available incorrectly set to True when Hooks import fails

On line 66, when the `Hooks` import fails (meaning hooks are NOT available), the code sets `are_request_hooks_available = True`. This is logically incorrect - it should be `False` since hooks are unavailable. While the class attribute defaults to `True` (line 32), the exception handler should flip it to `False`. This causes unnecessary metadata initialization (`{"_sentry_span": None}`) in `agent_run.py` even when hooks aren't being used, which is wasteful and potentially confusing for debugging.

Check warning on line 66 in sentry_sdk/integrations/pydantic_ai/__init__.py

View check run for this annotation

@sentry/warden / warden: find-bugs

are_request_hooks_available incorrectly set to True when hooks are unavailable

At line 66, when `ImportError` is raised (meaning `pydantic_ai.capabilities.Hooks` is not available), the code sets `PydanticAIIntegration.are_request_hooks_available = True`. This is incorrect - it should be `False` since the hooks mechanism is not available. This causes the agent_run wrapper to incorrectly add `metadata` to kwargs when using the fallback patch approach.

if Hooks is None:
_patch_graph_nodes()
_patch_model_request()
return

_patch_tool_execution()

hooks = Hooks()

@hooks.on.before_model_request # type: ignore
async def on_request(
ctx: "RunContext[None]", request_context: "ModelRequestContext"
) -> "ModelRequestContext":
span = ai_client_span(
messages=request_context.messages,
agent=None,
model=request_context.model,
model_settings=request_context.model_settings,
)
run_context_metadata = ctx.metadata
if isinstance(run_context_metadata, dict):
run_context_metadata["_sentry_span"] = span

span.__enter__()

Check warning on line 91 in sentry_sdk/integrations/pydantic_ai/__init__.py

View check run for this annotation

@sentry/warden / warden: find-bugs

Span entered but never exited when ctx.metadata is not a dict

In `on_request`, `span.__enter__()` is called unconditionally at line 91, but the span is only stored in metadata if `run_context_metadata` is a dict (lines 88-89). When metadata is not a dict, `on_response` returns early at line 103-104 without calling `span.__exit__()`. This causes a span leak - the span will remain open indefinitely, potentially corrupting the span hierarchy for subsequent operations.

return request_context

@hooks.on.after_model_request # type: ignore
async def on_response(
ctx: "RunContext[None]",
*,
request_context: "ModelRequestContext",
response: "ModelResponse",
) -> "ModelResponse":
run_context_metadata = ctx.metadata
if not isinstance(run_context_metadata, dict):
return response

span = run_context_metadata["_sentry_span"]

Check warning on line 106 in sentry_sdk/integrations/pydantic_ai/__init__.py

View check run for this annotation

@sentry/warden / warden: find-bugs

[9QU-Q26] Span entered but never exited when ctx.metadata is not a dict (additional location)

In `on_request`, `span.__enter__()` is called unconditionally at line 91, but the span is only stored in metadata if `run_context_metadata` is a dict (lines 88-89). When metadata is not a dict, `on_response` returns early at line 103-104 without calling `span.__exit__()`. This causes a span leak - the span will remain open indefinitely, potentially corrupting the span hierarchy for subsequent operations.
if span is None:
return response

update_ai_client_span(span, response)
span.__exit__(None, None, None)
del run_context_metadata["_sentry_span"]

return response

original_init = Agent.__init__

@functools.wraps(original_init)
def patched_init(
self: "Agent[Any, Any]", *args: "Any", **kwargs: "Any"
) -> None:
caps = list(kwargs.get("capabilities") or [])
caps.append(hooks)
kwargs["capabilities"] = caps
return original_init(self, *args, **kwargs)

Agent.__init__ = patched_init
12 changes: 12 additions & 0 deletions sentry_sdk/integrations/pydantic_ai/patches/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def _create_run_wrapper(
original_func: The original run method
is_streaming: Whether this is a streaming method (for future use)
"""
from sentry_sdk.integrations.pydantic_ai import PydanticAIIntegration

@wraps(original_func)
async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
Expand All @@ -107,6 +108,11 @@ async def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
model = kwargs.get("model")
model_settings = kwargs.get("model_settings")

if PydanticAIIntegration.are_request_hooks_available:
metadata = kwargs.get("metadata")
if not metadata:
kwargs["metadata"] = {"_sentry_span": None}

# Create invoke_agent span
with invoke_agent_span(
user_prompt, self, model, model_settings, is_streaming
Expand Down Expand Up @@ -140,6 +146,7 @@ def _create_streaming_wrapper(
"""
Wraps run_stream method that returns an async context manager.
"""
from sentry_sdk.integrations.pydantic_ai import PydanticAIIntegration

@wraps(original_func)
def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
Expand All @@ -148,6 +155,11 @@ def wrapper(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
model = kwargs.get("model")
model_settings = kwargs.get("model_settings")

if PydanticAIIntegration.are_request_hooks_available:
metadata = kwargs.get("metadata")
if not metadata:
kwargs["metadata"] = {"_sentry_span": None}

# Call original function to get the context manager
original_ctx_manager = original_func(self, *args, **kwargs)

Expand Down
Loading
Loading