Skip to content

Commit 34e7e0a

Browse files
feat(openai-agents): Inject propagation headers for HostedMCPTool
1 parent 4c4f260 commit 34e7e0a

File tree

2 files changed

+176
-1
lines changed

2 files changed

+176
-1
lines changed

sentry_sdk/integrations/openai_agents/patches/models.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,57 @@
44
from sentry_sdk.integrations import DidNotEnable
55

66
from ..spans import ai_client_span, update_ai_client_span
7+
8+
import sentry_sdk
79
from sentry_sdk.consts import SPANDATA
10+
from sentry_sdk.utils import logger
11+
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME
12+
from sentry_sdk.tracing_utils import (
13+
should_propagate_trace,
14+
add_sentry_baggage_to_headers,
15+
)
816

917
from typing import TYPE_CHECKING
1018

1119
if TYPE_CHECKING:
1220
from typing import Any, Callable
13-
21+
from sentry_sdk.tracing import Span
1422

1523
try:
1624
import agents
25+
from agents.tool import HostedMCPTool
1726
except ImportError:
1827
raise DidNotEnable("OpenAI Agents not installed")
1928

2029

30+
def _inject_trace_propagation_headers(
31+
hosted_tool: "HostedMCPTool", span: "Span"
32+
) -> None:
33+
headers = hosted_tool.tool_config.get("headers")
34+
if headers is None:
35+
headers = {}
36+
hosted_tool.tool_config["headers"] = headers
37+
38+
mcp_url = hosted_tool.tool_config.get("server_url")
39+
if not mcp_url:
40+
return
41+
42+
if should_propagate_trace(sentry_sdk.get_client(), mcp_url):
43+
for (
44+
key,
45+
value,
46+
) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(span=span):
47+
logger.debug(
48+
"[Tracing] Adding `{key}` header {value} to outgoing request to {mcp_url}.".format(
49+
key=key, value=value, mcp_url=mcp_url
50+
)
51+
)
52+
if key == BAGGAGE_HEADER_NAME:
53+
add_sentry_baggage_to_headers(headers, value)
54+
else:
55+
headers[key] = value
56+
57+
2158
def _create_get_model_wrapper(
2259
original_get_model: "Callable[..., Any]",
2360
) -> "Callable[..., Any]":
@@ -54,7 +91,17 @@ async def wrapped_fetch_response(*args: "Any", **kwargs: "Any") -> "Any":
5491

5592
@wraps(original_get_response)
5693
async def wrapped_get_response(*args: "Any", **kwargs: "Any") -> "Any":
94+
mcp_tools = kwargs.get("tools")
95+
hosted_tools = []
96+
if mcp_tools is not None:
97+
hosted_tools = [
98+
tool for tool in mcp_tools if isinstance(tool, HostedMCPTool)
99+
]
100+
57101
with ai_client_span(agent, kwargs) as span:
102+
for hosted_tool in hosted_tools:
103+
_inject_trace_propagation_headers(hosted_tool, span=span)
104+
58105
result = await original_get_response(*args, **kwargs)
59106

60107
response_model = getattr(agent, "_sentry_raw_response_model", None)

tests/integrations/openai_agents/test_openai_agents.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
from sentry_sdk.integrations.openai_agents.utils import _set_input_data, safe_serialize
1313
from sentry_sdk.utils import parse_version
1414

15+
from openai import AsyncOpenAI
16+
from agents.models.openai_responses import OpenAIResponsesModel
17+
18+
from unittest import mock
19+
from unittest.mock import AsyncMock
20+
1521
import agents
1622
from agents import (
1723
Agent,
@@ -25,16 +31,54 @@
2531
ResponseOutputText,
2632
ResponseFunctionToolCall,
2733
)
34+
from agents.tool import HostedMCPTool
2835
from agents.exceptions import MaxTurnsExceeded, ModelBehaviorError
2936
from agents.version import __version__ as OPENAI_AGENTS_VERSION
3037

38+
from openai.types.responses import Response, ResponseUsage
3139
from openai.types.responses.response_usage import (
3240
InputTokensDetails,
3341
OutputTokensDetails,
3442
)
3543

3644
test_run_config = agents.RunConfig(tracing_disabled=True)
3745

46+
EXAMPLE_RESPONSE = Response(
47+
id="chat-id",
48+
output=[
49+
ResponseOutputMessage(
50+
id="message-id",
51+
content=[
52+
ResponseOutputText(
53+
annotations=[],
54+
text="the model response",
55+
type="output_text",
56+
),
57+
],
58+
role="assistant",
59+
status="completed",
60+
type="message",
61+
),
62+
],
63+
parallel_tool_calls=False,
64+
tool_choice="none",
65+
tools=[],
66+
created_at=10000000,
67+
model="response-model-id",
68+
object="response",
69+
usage=ResponseUsage(
70+
input_tokens=20,
71+
input_tokens_details=InputTokensDetails(
72+
cached_tokens=5,
73+
),
74+
output_tokens=10,
75+
output_tokens_details=OutputTokensDetails(
76+
reasoning_tokens=8,
77+
),
78+
total_tokens=30,
79+
),
80+
)
81+
3882

3983
@pytest.fixture
4084
def mock_usage():
@@ -137,6 +181,7 @@ async def test_agent_invocation_span(
137181

138182
(transaction,) = events
139183
spans = transaction["spans"]
184+
print(spans)
140185
invoke_agent_span, ai_client_span = spans
141186

142187
assert transaction["transaction"] == "test_agent workflow"
@@ -695,6 +740,89 @@ def simple_test_tool(message: str) -> str:
695740
assert ai_client_span2["data"]["gen_ai.usage.total_tokens"] == 25
696741

697742

743+
@pytest.mark.asyncio
744+
async def test_hosted_mcp_tool_propagation_headers(sentry_init, test_agent):
745+
"""
746+
Test responses API is given trace propagation headers with HostedMCPTool.
747+
"""
748+
749+
hosted_tool = HostedMCPTool(
750+
tool_config={
751+
"type": "mcp",
752+
"server_label": "...",
753+
"server_url": "...",
754+
"require_approval": "never",
755+
"headers": {
756+
"baggage": "custom=data",
757+
},
758+
},
759+
on_approval_request=None,
760+
)
761+
762+
client = AsyncOpenAI(api_key="z")
763+
client.responses._post = AsyncMock(return_value=EXAMPLE_RESPONSE)
764+
765+
model = OpenAIResponsesModel(model="gpt-4", openai_client=client)
766+
767+
agent_with_tool = test_agent.clone(
768+
tools=[hosted_tool],
769+
model=model,
770+
)
771+
772+
sentry_init(
773+
integrations=[OpenAIAgentsIntegration()],
774+
traces_sample_rate=1.0,
775+
release="d08ebdb9309e1b004c6f52202de58a09c2268e42",
776+
)
777+
778+
with patch.object(
779+
model._client.responses,
780+
"create",
781+
wraps=model._client.responses.create,
782+
) as create, mock.patch(
783+
"sentry_sdk.tracing_utils.Random.randrange", return_value=500000
784+
):
785+
with sentry_sdk.start_transaction(
786+
name="/interactions/other-dogs/new-dog",
787+
op="greeting.sniff",
788+
trace_id="01234567890123456789012345678901",
789+
) as transaction:
790+
await agents.Runner.run(
791+
agent_with_tool,
792+
"Please use the simple test tool",
793+
run_config=test_run_config,
794+
)
795+
796+
ai_client_span = transaction._span_recorder.spans[-1]
797+
798+
args, kwargs = create.call_args
799+
800+
assert "tools" in kwargs
801+
assert len(kwargs["tools"]) == 1
802+
hosted_mcp_tool = kwargs["tools"][0]
803+
804+
assert hosted_mcp_tool["headers"][
805+
"sentry-trace"
806+
] == "{trace_id}-{parent_span_id}-{sampled}".format(
807+
trace_id=transaction.trace_id,
808+
parent_span_id=ai_client_span.span_id,
809+
sampled=1,
810+
)
811+
812+
expected_outgoing_baggage = (
813+
"custom=data,"
814+
"sentry-trace_id=01234567890123456789012345678901,"
815+
"sentry-sample_rand=0.500000,"
816+
"sentry-environment=production,"
817+
"sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,"
818+
"sentry-transaction=/interactions/other-dogs/new-dog,"
819+
"sentry-sample_rate=1.0,"
820+
"sentry-sampled=true"
821+
)
822+
823+
assert hosted_mcp_tool["headers"]["baggage"] == expected_outgoing_baggage
824+
825+
698826
@pytest.mark.asyncio
699827
async def test_model_behavior_error(sentry_init, capture_events, test_agent):
700828
"""

0 commit comments

Comments
 (0)