fix: propagate EnableSensitiveData to auto-wired inner OpenTelemetryChatClient

When _autoWireChatClient=true and the caller sets EnableSensitiveData=true on
the outer OpenTelemetryAgent, the auto-wired inner OpenTelemetryChatClient now
also has EnableSensitiveData propagated, so the inner chat span correctly
captures message content (gen_ai.input.messages / gen_ai.output.messages).

Red test added first to reproduce the bug, then the fix applied (green).

Fixes #5873

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/fda69dd4-9576-4f3f-b954-514321652ea9

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-26 15:44:05 +00:00
committed by GitHub
Unverified
parent bd4fc64b4d
commit ce242ebeb2
2 changed files with 60 additions and 4 deletions
@@ -242,10 +242,11 @@ public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
}
string sourceName = this._sourceName;
static IChatClient WrapIfNeeded(IChatClient cc, string sourceName) =>
bool enableSensitiveData = this.EnableSensitiveData;
static IChatClient WrapIfNeeded(IChatClient cc, string sourceName, bool enableSensitiveData) =>
cc.GetService(typeof(OpenTelemetryChatClient)) is not null
? cc
: cc.AsBuilder().UseOpenTelemetry(sourceName: sourceName).Build();
: cc.AsBuilder().UseOpenTelemetry(sourceName: sourceName, configure: o => o.EnableSensitiveData = enableSensitiveData).Build();
if (options is ChatClientAgentRunOptions ccOptions)
{
@@ -253,7 +254,7 @@ public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
// If the user factory already returns an OpenTelemetry-instrumented client, don't double-wrap.
var clone = (ChatClientAgentRunOptions)ccOptions.Clone();
var userFactory = clone.ChatClientFactory;
clone.ChatClientFactory = cc => WrapIfNeeded(userFactory is null ? cc : userFactory(cc), sourceName);
clone.ChatClientFactory = cc => WrapIfNeeded(userFactory is null ? cc : userFactory(cc), sourceName, enableSensitiveData);
return clone;
}
@@ -261,7 +262,7 @@ public sealed class OpenTelemetryAgent : DelegatingAIAgent, IDisposable
// any base AgentRunOptions properties from the caller so they reach the inner agent.
var newOptions = new ChatClientAgentRunOptions
{
ChatClientFactory = cc => WrapIfNeeded(cc, sourceName),
ChatClientFactory = cc => WrapIfNeeded(cc, sourceName, enableSensitiveData),
};
if (options is not null)
@@ -1054,6 +1054,61 @@ public class OpenTelemetryAgentTests
Assert.Contains(activities, a => string.Equals(a.GetTagItem("gen_ai.operation.name") as string, "chat", StringComparison.Ordinal));
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public async Task AutoWireChatClient_EnableSensitiveData_PropagatedToInnerChatClient_Async(bool enableSensitiveData, bool streaming)
{
// Regression test for: when EnableSensitiveData is set on OpenTelemetryAgent, the auto-wired
// inner OpenTelemetryChatClient must also have EnableSensitiveData propagated to it. Previously,
// GetRunOptionsWithChatClientWiring created the inner client without passing EnableSensitiveData,
// so the inner chat span would never emit gen_ai.input.messages / gen_ai.output.messages even
// when the caller explicitly set EnableSensitiveData = true.
var sourceName = Guid.NewGuid().ToString();
var activities = new List<Activity>();
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.AddSource(sourceName)
.AddInMemoryExporter(activities)
.Build();
var fakeChatClient = new AutoWireTestChatClient();
var inner = new ChatClientAgent(fakeChatClient);
using var agent = new OpenTelemetryAgent(inner, sourceName) { EnableSensitiveData = enableSensitiveData };
if (streaming)
{
await foreach (var _ in agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "hello")]))
{
}
}
else
{
_ = await agent.RunAsync([new ChatMessage(ChatRole.User, "hello")]);
}
// There should be 2 activities: the invoke_agent span and the inner chat span.
Assert.Equal(2, activities.Count);
var chatSpan = activities.Single(a => string.Equals(a.GetTagItem("gen_ai.operation.name") as string, "chat", StringComparison.Ordinal));
var chatTags = chatSpan.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (enableSensitiveData)
{
// When EnableSensitiveData=true on the outer agent, the auto-wired inner client must also
// capture message content in the chat span.
Assert.True(chatTags.ContainsKey("gen_ai.input.messages"), "gen_ai.input.messages must be present in the inner chat span when EnableSensitiveData=true");
Assert.True(chatTags.ContainsKey("gen_ai.output.messages"), "gen_ai.output.messages must be present in the inner chat span when EnableSensitiveData=true");
}
else
{
// By default (EnableSensitiveData=false) message content must NOT be captured.
Assert.False(chatTags.ContainsKey("gen_ai.input.messages"), "gen_ai.input.messages must NOT be present in the inner chat span when EnableSensitiveData=false");
Assert.False(chatTags.ContainsKey("gen_ai.output.messages"), "gen_ai.output.messages must NOT be present in the inner chat span when EnableSensitiveData=false");
}
}
private sealed class AutoWireTestChatClient : IChatClient
{
public Action<IEnumerable<ChatMessage>, ChatOptions?>? OnGetResponseAsync { get; set; }