From 9896852a682992fffbcf1e90b17fe9f0231d1255 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Tue, 14 Oct 2025 11:56:53 -0400 Subject: [PATCH 01/22] Basic Functionality working --- .generated.NoMobile.sln | 60 ++++ Sentry-CI-Build-Linux-NoMobile.slnf | 4 + Sentry-CI-Build-Linux.slnf | 4 + Sentry-CI-Build-Windows-arm64.slnf | 4 + Sentry-CI-Build-Windows.slnf | 4 + Sentry-CI-Build-macOS.slnf | 4 + Sentry-CI-CodeQL.slnf | 1 + Sentry.sln | 60 ++++ SentryAspNetCore.slnf | 1 + SentryNoMobile.slnf | 4 + SentryNoSamples.slnf | 2 + .../Program.cs | 271 ++++++++++++++++++ .../Sentry.Samples.ME.AI.AspNetCore.csproj | 25 ++ .../Sentry.Samples.ME.AI.Console/Program.cs | 204 +++++++++++++ .../Sentry.Samples.ME.AI.Console.csproj | 25 ++ .../Extensions/ServiceCollectionExtensions.cs | 33 +++ .../Sentry.Extensions.AI.csproj | 21 ++ .../SentryAIFunctionWrapper.cs | 24 ++ .../SentryAISpanEnricher.cs | 121 ++++++++ src/Sentry.Extensions.AI/SentryChatClient.cs | 38 +++ .../Sentry.Extensions.AI.Tests.csproj | 17 ++ .../SentryChatClientTests.cs | 59 ++++ 22 files changed, 986 insertions(+) create mode 100644 samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs create mode 100644 samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj create mode 100644 samples/Sentry.Samples.ME.AI.Console/Program.cs create mode 100644 samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj create mode 100644 src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Sentry.Extensions.AI/Sentry.Extensions.AI.csproj create mode 100644 src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs create mode 100644 src/Sentry.Extensions.AI/SentryAISpanEnricher.cs create mode 100644 src/Sentry.Extensions.AI/SentryChatClient.cs create mode 100644 test/Sentry.Extensions.AI.Tests/Sentry.Extensions.AI.Tests.csproj create mode 100644 test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs diff --git a/.generated.NoMobile.sln b/.generated.NoMobile.sln index 3730377a46..4dd8dc1c11 100644 --- a/.generated.NoMobile.sln +++ b/.generated.NoMobile.sln @@ -209,6 +209,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.SourceGenerators.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Maui.CommunityToolkit.Mvvm.Tests", "test\Sentry.Maui.CommunityToolkit.Mvvm.Tests\Sentry.Maui.CommunityToolkit.Mvvm.Tests.csproj", "{ADC91A84-6054-42EC-8241-0D717E4C7194}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.ME.AI.Console", "samples\Sentry.Samples.ME.AI.Console\Sentry.Samples.ME.AI.Console.csproj", "{CE591593-E51C-498E-BC0D-5083C8197544}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.ME.AI.AspNetCore", "samples\Sentry.Samples.ME.AI.AspNetCore\Sentry.Samples.ME.AI.AspNetCore.csproj", "{3D91128A-5695-4BAE-B939-5F5F7C56A679}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Extensions.AI", "src\Sentry.Extensions.AI\Sentry.Extensions.AI.csproj", "{AE461926-00B8-4FDD-A41D-3C83C2D9A045}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Extensions.AI.Tests", "test\Sentry.Extensions.AI.Tests\Sentry.Extensions.AI.Tests.csproj", "{28D6E004-DC64-464F-91BE-BC0B3A8E543F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1252,6 +1260,54 @@ Global {ADC91A84-6054-42EC-8241-0D717E4C7194}.Release|x64.Build.0 = Release|Any CPU {ADC91A84-6054-42EC-8241-0D717E4C7194}.Release|x86.ActiveCfg = Release|Any CPU {ADC91A84-6054-42EC-8241-0D717E4C7194}.Release|x86.Build.0 = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x64.Build.0 = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x86.Build.0 = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|Any CPU.Build.0 = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x64.ActiveCfg = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x64.Build.0 = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x86.ActiveCfg = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x86.Build.0 = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x64.Build.0 = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x86.Build.0 = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|Any CPU.Build.0 = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x64.ActiveCfg = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x64.Build.0 = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x86.ActiveCfg = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x86.Build.0 = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x64.Build.0 = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x86.ActiveCfg = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x86.Build.0 = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|Any CPU.Build.0 = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x64.ActiveCfg = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x64.Build.0 = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x86.ActiveCfg = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x86.Build.0 = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x64.ActiveCfg = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x64.Build.0 = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x86.ActiveCfg = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x86.Build.0 = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|Any CPU.Build.0 = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x64.ActiveCfg = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x64.Build.0 = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x86.ActiveCfg = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1343,5 +1399,9 @@ Global {C3CDF61C-3E28-441C-A9CE-011C89D11719} = {230B9384-90FD-4551-A5DE-1A5C197F25B6} {3A76FF7D-2F32-4EA5-8999-2FFE3C7CB893} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} {ADC91A84-6054-42EC-8241-0D717E4C7194} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} + {CE591593-E51C-498E-BC0D-5083C8197544} = {21B42F60-5802-404E-90F0-AEBCC56760C0} + {3D91128A-5695-4BAE-B939-5F5F7C56A679} = {21B42F60-5802-404E-90F0-AEBCC56760C0} + {AE461926-00B8-4FDD-A41D-3C83C2D9A045} = {230B9384-90FD-4551-A5DE-1A5C197F25B6} + {28D6E004-DC64-464F-91BE-BC0B3A8E543F} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} EndGlobalSection EndGlobal diff --git a/Sentry-CI-Build-Linux-NoMobile.slnf b/Sentry-CI-Build-Linux-NoMobile.slnf index f29fd0a74e..c1d20419d3 100644 --- a/Sentry-CI-Build-Linux-NoMobile.slnf +++ b/Sentry-CI-Build-Linux-NoMobile.slnf @@ -24,6 +24,8 @@ "samples\\Sentry.Samples.GraphQL.Server\\Sentry.Samples.GraphQL.Server.csproj", "samples\\Sentry.Samples.Hangfire\\Sentry.Samples.Hangfire.csproj", "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", + "samples\\Sentry.Samples.ME.AI.AspNetCore\\Sentry.Samples.ME.AI.AspNetCore.csproj", + "samples\\Sentry.Samples.ME.AI.Console\\Sentry.Samples.ME.AI.Console.csproj", "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", @@ -36,6 +38,7 @@ "src\\Sentry.Azure.Functions.Worker\\Sentry.Azure.Functions.Worker.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", @@ -54,6 +57,7 @@ "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", "test\\Sentry.EntityFramework.Tests\\Sentry.EntityFramework.Tests.csproj", + "test\\Sentry.Extensions.AI.Tests\\Sentry.Extensions.AI.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Google.Cloud.Functions.Tests\\Sentry.Google.Cloud.Functions.Tests.csproj", "test\\Sentry.Hangfire.Tests\\Sentry.Hangfire.Tests.csproj", diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf index b31c819bc8..974d654f39 100644 --- a/Sentry-CI-Build-Linux.slnf +++ b/Sentry-CI-Build-Linux.slnf @@ -26,6 +26,8 @@ "samples\\Sentry.Samples.Hangfire\\Sentry.Samples.Hangfire.csproj", "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", "samples\\Sentry.Samples.Maui\\Sentry.Samples.Maui.csproj", + "samples\\Sentry.Samples.ME.AI.AspNetCore\\Sentry.Samples.ME.AI.AspNetCore.csproj", + "samples\\Sentry.Samples.ME.AI.Console\\Sentry.Samples.ME.AI.Console.csproj", "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", @@ -40,6 +42,7 @@ "src\\Sentry.Bindings.Android\\Sentry.Bindings.Android.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", @@ -61,6 +64,7 @@ "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", "test\\Sentry.EntityFramework.Tests\\Sentry.EntityFramework.Tests.csproj", + "test\\Sentry.Extensions.AI.Tests\\Sentry.Extensions.AI.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Google.Cloud.Functions.Tests\\Sentry.Google.Cloud.Functions.Tests.csproj", "test\\Sentry.Hangfire.Tests\\Sentry.Hangfire.Tests.csproj", diff --git a/Sentry-CI-Build-Windows-arm64.slnf b/Sentry-CI-Build-Windows-arm64.slnf index baac3fedfb..7f3b934be2 100644 --- a/Sentry-CI-Build-Windows-arm64.slnf +++ b/Sentry-CI-Build-Windows-arm64.slnf @@ -27,6 +27,8 @@ "samples\\Sentry.Samples.Hangfire\\Sentry.Samples.Hangfire.csproj", "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", "samples\\Sentry.Samples.Maui\\Sentry.Samples.Maui.csproj", + "samples\\Sentry.Samples.ME.AI.AspNetCore\\Sentry.Samples.ME.AI.AspNetCore.csproj", + "samples\\Sentry.Samples.ME.AI.Console\\Sentry.Samples.ME.AI.Console.csproj", "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", @@ -42,6 +44,7 @@ "src\\Sentry.Bindings.Android\\Sentry.Bindings.Android.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", @@ -60,6 +63,7 @@ "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", "test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj", "test\\Sentry.Azure.Functions.Worker.Tests\\Sentry.Azure.Functions.Worker.Tests.csproj", + "test\\Sentry.Extensions.AI.Tests\\Sentry.Extensions.AI.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Google.Cloud.Functions.Tests\\Sentry.Google.Cloud.Functions.Tests.csproj", "test\\Sentry.Hangfire.Tests\\Sentry.Hangfire.Tests.csproj", diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf index 677a8f0608..e994826b2d 100644 --- a/Sentry-CI-Build-Windows.slnf +++ b/Sentry-CI-Build-Windows.slnf @@ -27,6 +27,8 @@ "samples\\Sentry.Samples.Hangfire\\Sentry.Samples.Hangfire.csproj", "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", "samples\\Sentry.Samples.Maui\\Sentry.Samples.Maui.csproj", + "samples\\Sentry.Samples.ME.AI.AspNetCore\\Sentry.Samples.ME.AI.AspNetCore.csproj", + "samples\\Sentry.Samples.ME.AI.Console\\Sentry.Samples.ME.AI.Console.csproj", "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", @@ -42,6 +44,7 @@ "src\\Sentry.Bindings.Android\\Sentry.Bindings.Android.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", @@ -63,6 +66,7 @@ "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", "test\\Sentry.EntityFramework.Tests\\Sentry.EntityFramework.Tests.csproj", + "test\\Sentry.Extensions.AI.Tests\\Sentry.Extensions.AI.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Google.Cloud.Functions.Tests\\Sentry.Google.Cloud.Functions.Tests.csproj", "test\\Sentry.Hangfire.Tests\\Sentry.Hangfire.Tests.csproj", diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index 778a9a13db..8d5227346c 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -31,6 +31,8 @@ "samples\\Sentry.Samples.MacCatalyst\\Sentry.Samples.MacCatalyst.csproj", "samples\\Sentry.Samples.MacOS\\Sentry.Samples.MacOS.csproj", "samples\\Sentry.Samples.Maui\\Sentry.Samples.Maui.csproj", + "samples\\Sentry.Samples.ME.AI.AspNetCore\\Sentry.Samples.ME.AI.AspNetCore.csproj", + "samples\\Sentry.Samples.ME.AI.Console\\Sentry.Samples.ME.AI.Console.csproj", "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", @@ -47,6 +49,7 @@ "src\\Sentry.Bindings.Cocoa\\Sentry.Bindings.Cocoa.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", @@ -68,6 +71,7 @@ "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", "test\\Sentry.EntityFramework.Tests\\Sentry.EntityFramework.Tests.csproj", + "test\\Sentry.Extensions.AI.Tests\\Sentry.Extensions.AI.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Google.Cloud.Functions.Tests\\Sentry.Google.Cloud.Functions.Tests.csproj", "test\\Sentry.Hangfire.Tests\\Sentry.Hangfire.Tests.csproj", diff --git a/Sentry-CI-CodeQL.slnf b/Sentry-CI-CodeQL.slnf index 0dbf4453dd..d843499f8f 100644 --- a/Sentry-CI-CodeQL.slnf +++ b/Sentry-CI-CodeQL.slnf @@ -11,6 +11,7 @@ "src\\Sentry.Azure.Functions.Worker\\Sentry.Azure.Functions.Worker.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", diff --git a/Sentry.sln b/Sentry.sln index 3730377a46..4dd8dc1c11 100644 --- a/Sentry.sln +++ b/Sentry.sln @@ -209,6 +209,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.SourceGenerators.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Maui.CommunityToolkit.Mvvm.Tests", "test\Sentry.Maui.CommunityToolkit.Mvvm.Tests\Sentry.Maui.CommunityToolkit.Mvvm.Tests.csproj", "{ADC91A84-6054-42EC-8241-0D717E4C7194}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.ME.AI.Console", "samples\Sentry.Samples.ME.AI.Console\Sentry.Samples.ME.AI.Console.csproj", "{CE591593-E51C-498E-BC0D-5083C8197544}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.ME.AI.AspNetCore", "samples\Sentry.Samples.ME.AI.AspNetCore\Sentry.Samples.ME.AI.AspNetCore.csproj", "{3D91128A-5695-4BAE-B939-5F5F7C56A679}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Extensions.AI", "src\Sentry.Extensions.AI\Sentry.Extensions.AI.csproj", "{AE461926-00B8-4FDD-A41D-3C83C2D9A045}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Extensions.AI.Tests", "test\Sentry.Extensions.AI.Tests\Sentry.Extensions.AI.Tests.csproj", "{28D6E004-DC64-464F-91BE-BC0B3A8E543F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1252,6 +1260,54 @@ Global {ADC91A84-6054-42EC-8241-0D717E4C7194}.Release|x64.Build.0 = Release|Any CPU {ADC91A84-6054-42EC-8241-0D717E4C7194}.Release|x86.ActiveCfg = Release|Any CPU {ADC91A84-6054-42EC-8241-0D717E4C7194}.Release|x86.Build.0 = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x64.Build.0 = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Debug|x86.Build.0 = Debug|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|Any CPU.Build.0 = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x64.ActiveCfg = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x64.Build.0 = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x86.ActiveCfg = Release|Any CPU + {CE591593-E51C-498E-BC0D-5083C8197544}.Release|x86.Build.0 = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x64.Build.0 = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Debug|x86.Build.0 = Debug|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|Any CPU.Build.0 = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x64.ActiveCfg = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x64.Build.0 = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x86.ActiveCfg = Release|Any CPU + {3D91128A-5695-4BAE-B939-5F5F7C56A679}.Release|x86.Build.0 = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x64.Build.0 = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x86.ActiveCfg = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Debug|x86.Build.0 = Debug|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|Any CPU.Build.0 = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x64.ActiveCfg = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x64.Build.0 = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x86.ActiveCfg = Release|Any CPU + {AE461926-00B8-4FDD-A41D-3C83C2D9A045}.Release|x86.Build.0 = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x64.ActiveCfg = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x64.Build.0 = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x86.ActiveCfg = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Debug|x86.Build.0 = Debug|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|Any CPU.Build.0 = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x64.ActiveCfg = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x64.Build.0 = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x86.ActiveCfg = Release|Any CPU + {28D6E004-DC64-464F-91BE-BC0B3A8E543F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1343,5 +1399,9 @@ Global {C3CDF61C-3E28-441C-A9CE-011C89D11719} = {230B9384-90FD-4551-A5DE-1A5C197F25B6} {3A76FF7D-2F32-4EA5-8999-2FFE3C7CB893} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} {ADC91A84-6054-42EC-8241-0D717E4C7194} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} + {CE591593-E51C-498E-BC0D-5083C8197544} = {21B42F60-5802-404E-90F0-AEBCC56760C0} + {3D91128A-5695-4BAE-B939-5F5F7C56A679} = {21B42F60-5802-404E-90F0-AEBCC56760C0} + {AE461926-00B8-4FDD-A41D-3C83C2D9A045} = {230B9384-90FD-4551-A5DE-1A5C197F25B6} + {28D6E004-DC64-464F-91BE-BC0B3A8E543F} = {6987A1CC-608E-4868-A02C-09D30C8B7B2D} EndGlobalSection EndGlobal diff --git a/SentryAspNetCore.slnf b/SentryAspNetCore.slnf index 543d1de30d..7b89d9e91c 100644 --- a/SentryAspNetCore.slnf +++ b/SentryAspNetCore.slnf @@ -11,6 +11,7 @@ "samples\\Sentry.Samples.AspNetCore.WebAPI.Profiling\\Sentry.Samples.AspNetCore.WebAPI.Profiling.csproj", "samples\\Sentry.Samples.Aws.Lambda.AspNetCoreServer\\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", "samples\\Sentry.Samples.Hangfire\\Sentry.Samples.Hangfire.csproj", + "samples\\Sentry.Samples.ME.AI.AspNetCore\\Sentry.Samples.ME.AI.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "src\\Sentry.Analyzers\\Sentry.Analyzers.csproj", "src\\Sentry.AspNetCore.Blazor.WebAssembly\\Sentry.AspNetCore.Blazor.WebAssembly.csproj", diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf index 1592f77697..004e7be77b 100644 --- a/SentryNoMobile.slnf +++ b/SentryNoMobile.slnf @@ -26,6 +26,8 @@ "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", "samples\\Sentry.Samples.MacCatalyst\\Sentry.Samples.MacCatalyst.csproj", "samples\\Sentry.Samples.MacOS\\Sentry.Samples.MacOS.csproj", + "samples\\Sentry.Samples.ME.AI.AspNetCore\\Sentry.Samples.ME.AI.AspNetCore.csproj", + "samples\\Sentry.Samples.ME.AI.Console\\Sentry.Samples.ME.AI.Console.csproj", "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", @@ -39,6 +41,7 @@ "src\\Sentry.Azure.Functions.Worker\\Sentry.Azure.Functions.Worker.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", @@ -58,6 +61,7 @@ "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", "test\\Sentry.EntityFramework.Tests\\Sentry.EntityFramework.Tests.csproj", + "test\\Sentry.Extensions.AI.Tests\\Sentry.Extensions.AI.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Google.Cloud.Functions.Tests\\Sentry.Google.Cloud.Functions.Tests.csproj", "test\\Sentry.Hangfire.Tests\\Sentry.Hangfire.Tests.csproj", diff --git a/SentryNoSamples.slnf b/SentryNoSamples.slnf index d3b4e187ea..f442fa2534 100644 --- a/SentryNoSamples.slnf +++ b/SentryNoSamples.slnf @@ -12,6 +12,7 @@ "src\\Sentry.Azure.Functions.Worker\\Sentry.Azure.Functions.Worker.csproj", "src\\Sentry.DiagnosticSource\\Sentry.DiagnosticSource.csproj", "src\\Sentry.EntityFramework\\Sentry.EntityFramework.csproj", + "src\\Sentry.Extensions.AI\\Sentry.Extensions.AI.csproj", "src\\Sentry.Extensions.Logging\\Sentry.Extensions.Logging.csproj", "src\\Sentry.Google.Cloud.Functions\\Sentry.Google.Cloud.Functions.csproj", "src\\Sentry.Hangfire\\Sentry.Hangfire.csproj", @@ -34,6 +35,7 @@ "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", "test\\Sentry.EntityFramework.Tests\\Sentry.EntityFramework.Tests.csproj", + "test\\Sentry.Extensions.AI.Tests\\Sentry.Extensions.AI.Tests.csproj", "test\\Sentry.Extensions.Logging.Tests\\Sentry.Extensions.Logging.Tests.csproj", "test\\Sentry.Google.Cloud.Functions.Tests\\Sentry.Google.Cloud.Functions.Tests.csproj", "test\\Sentry.Hangfire.Tests\\Sentry.Hangfire.Tests.csproj", diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs new file mode 100644 index 0000000000..0826045622 --- /dev/null +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs @@ -0,0 +1,271 @@ +#nullable enable +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using Anthropic.SDK; +using Anthropic.SDK.Constants; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Sentry.Extensions.AI; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.UseSentry(options => +{ +#if !SENTRY_DSN_DEFINED_IN_ENV + // A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable. + // See https://docs.sentry.io/product/sentry-basics/dsn-explainer/ + options.Dsn = SamplesShared.Dsn; +#endif + + options.Debug = true; + options.DiagnosticLevel = SentryLevel.Debug; + options.SampleRate = 1; + options.TracesSampleRate = 1.0; + options.Experimental.EnableLogs = true; +}); + +var client = new AnthropicClient().Messages + .AsBuilder() + .UseFunctionInvocation() + .Build() + .WithSentry(agentName: "Anthropic", system: "anthropic"); + +// Register the Claude API client and Sentry-instrumented chat client +builder.Services.AddKeyedSingleton("claude3_5", client); + +var app = builder.Build(); + +// Endpoint for regular AI chat +app.MapPost("/chat", async (ChatRequest request, IChatClient chatClient, ILogger logger) => +{ + logger.LogInformation("Handling chat request with message: {Message}", request.Message); + + try + { + var response = await chatClient.GetResponseAsync([ + new ChatMessage(ChatRole.User, request.Message) + ]); + + return Results.Ok(new { response = response.Messages?.FirstOrDefault()?.Text ?? "No response" }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing chat request"); + return Results.Problem("An error occurred while processing your request"); + } +}); + +// Endpoint for streaming AI chat +// app.MapPost("/chat/stream", async (ChatRequest request, IChatClient chatClient, ILogger logger) => +// { +// logger.LogInformation("Handling streaming chat request with message: {Message}", request.Message); +// +// return Results.Stream(async stream => +// { +// try +// { +// await foreach (var update in chatClient.GetStreamingResponseAsync([ +// new ChatMessage(ChatRole.User, request.Message) +// ])) +// { +// if (!string.IsNullOrEmpty(update.Text)) +// { +// var bytes = Encoding.UTF8.GetBytes(update.Text); +// await stream.WriteAsync(bytes); +// await stream.FlushAsync(); +// } +// } +// } +// catch (Exception ex) +// { +// logger.LogError(ex, "Error processing streaming chat request"); +// var errorBytes = Encoding.UTF8.GetBytes("\n[Error occurred while streaming response]"); +// await stream.WriteAsync(errorBytes); +// } +// }, "text/plain"); +// }); + +// Health check endpoint +app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow })); + +// Simple test endpoint that demonstrates AI integration +app.MapGet("/test", async (IChatClient chatClient, ILogger logger) => +{ + logger.LogInformation("Running AI test endpoint"); + ChatOptions options = new() + { + ModelId = AnthropicModels.Claude3Haiku, + MaxOutputTokens = 512, + Tools = [AIFunctionFactory.Create((string personName) => personName switch { + "Alice" => "25", + _ => "40" + }, "GetPersonAge", "Gets the age of the person whose name is specified.")] + }; + + try + { + var response = await chatClient.GetResponseAsync("How old is Alice?", options); + + return Results.Ok(new { + message = "AI test completed successfully", + response = response.Messages?.FirstOrDefault()?.Text ?? "No response", + timestamp = DateTime.UtcNow + }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error in AI test endpoint"); + return Results.Problem("An error occurred during the AI test"); + } +}); + +app.Run(); + +public record ChatRequest(string Message); + +// Claude API client using HttpClient without third-party dependencies +internal class ClaudeChatClient : IChatClient +{ + private readonly HttpClient _httpClient; + private const string ApiBaseUrl = "https://api.anthropic.com/v1/messages"; + + public ClaudeChatClient() + { + var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? + throw new InvalidOperationException("ANTHROPIC_API_KEY environment variable is required"); + + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("x-api-key", apiKey); + _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); + } + + public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + var requestBody = CreateRequestBody(messages, options, false); + var json = JsonSerializer.Serialize(requestBody); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); + response.EnsureSuccessStatusCode(); + + var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); + using var doc = JsonDocument.Parse(responseJson); + + var usage = new UsageDetails(); + var responseText = "No response"; + if (doc.RootElement.TryGetProperty("content", out var contentArray) && + contentArray.ValueKind == JsonValueKind.Array) + { + var firstContent = contentArray.EnumerateArray().FirstOrDefault(); + if (firstContent.TryGetProperty("text", out var textProperty)) + { + responseText = textProperty.GetString() ?? "No response"; + } + } + if (doc.RootElement.TryGetProperty("usage", out var usageProperty)) + { + if (usageProperty.TryGetProperty("input_tokens", out var inputTokens) && inputTokens.TryGetInt64(out var inputTokenCount)) + { + usage.InputTokenCount = inputTokenCount; + } + if (usageProperty.TryGetProperty("output_tokens", out var outputTokens) && outputTokens.TryGetInt64(out var outputTokenCount)) + { + usage.OutputTokenCount = outputTokenCount; + } + } + + var responseMessage = new ChatMessage(ChatRole.Assistant, responseText); + var chatResponse = new ChatResponse(responseMessage) + { + Usage = usage + }; + + return chatResponse; + } + + public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, + ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var requestBody = CreateRequestBody(messages, options, true); + var json = JsonSerializer.Serialize(requestBody); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); + response.EnsureSuccessStatusCode(); + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + using var reader = new StreamReader(stream); + + while (await reader.ReadLineAsync(cancellationToken) is { } line) + { + if (!line.StartsWith("data: ") || line.Length <= 6) + { + continue; + } + + var eventData = line.Substring(6); + if (eventData == "[DONE]") + { + break; + } + + ClaudeStreamEvent? streamEvent = null; + try + { + streamEvent = JsonSerializer.Deserialize(eventData); + } + catch (JsonException) + { + // Skip malformed JSON + continue; + } + + if (streamEvent?.Type == "content_block_delta" && + streamEvent.Delta?.Type == "text_delta") + { + yield return new ChatResponseUpdate(null, streamEvent.Delta.Text); + } + } + } + + private object CreateRequestBody(IEnumerable messages, ChatOptions? options, bool stream) + { + var claudeMessages = messages + .Where(m => m.Role != ChatRole.System) + .Select(m => new + { + role = m.Role == ChatRole.User ? "user" : "assistant", + content = m.Text ?? "" + }) + .ToArray(); + + return new + { + model = "claude-3-5-sonnet-20241022", + max_tokens = options?.MaxOutputTokens ?? 1000, + messages = claudeMessages, + stream = stream + }; + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() + { + _httpClient?.Dispose(); + } +} + +internal class ClaudeStreamEvent +{ + public string? Type { get; set; } + public ClaudeStreamDelta? Delta { get; set; } +} + +internal class ClaudeStreamDelta +{ + public string? Type { get; set; } + public string? Text { get; set; } +} diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj b/samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj new file mode 100644 index 0000000000..ca28278952 --- /dev/null +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + + + + + + + + + + sentry-sdks + sentry-dotnet + true + true + + + + + + + + \ No newline at end of file diff --git a/samples/Sentry.Samples.ME.AI.Console/Program.cs b/samples/Sentry.Samples.ME.AI.Console/Program.cs new file mode 100644 index 0000000000..00fd45df72 --- /dev/null +++ b/samples/Sentry.Samples.ME.AI.Console/Program.cs @@ -0,0 +1,204 @@ +#nullable enable +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Sentry.Extensions.AI; + +SentrySdk.Init(options => + { +#if !SENTRY_DSN_DEFINED_IN_ENV + // A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable. + // See https://docs.sentry.io/product/sentry-basics/dsn-explainer/ + options.Dsn = SamplesShared.Dsn; +#endif + // Set to true to SDK debugging to see the internal messages through the logging library. + options.Debug = true; + // Configure the level of Sentry internal logging + options.DiagnosticLevel = SentryLevel.Debug; + options.SampleRate = 1; + options.TracesSampleRate = 1; + } +); + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger(); + +logger.LogInformation("Starting Microsoft.Extensions.AI sample with Sentry instrumentation"); + +// Create Claude API client and wrap it with Sentry instrumentation +var claudeClient = new ClaudeChatClient(); +var chat = claudeClient.WithSentry(agentName: "Anthropic ", system: "anthropic"); + +logger.LogInformation("Making AI call with Sentry instrumentation..."); + +var response = await chat.GetResponseAsync([ + new ChatMessage(ChatRole.User, "Say hello from Sentry sample") +]); + +logger.LogInformation("Response: {ResponseText}", response.Messages); + +// Demonstrate streaming with Sentry instrumentation +logger.LogInformation("Making streaming AI call with Sentry instrumentation..."); + +var streamingResponse = new List(); +await foreach (var update in chat.GetStreamingResponseAsync([ + new ChatMessage(ChatRole.User, "Say hello and goodbye with streaming") + ])) +{ + streamingResponse.Add(update.Text ?? ""); +} + +logger.LogInformation("Streaming Response: {StreamingText}", string.Join("", streamingResponse)); + +logger.LogInformation("Microsoft.Extensions.AI sample completed! Check your Sentry dashboard for the trace data."); + +// Flush Sentry to ensure all transactions are sent before the app exits +await SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)); + +// Claude API client using HttpClient without third-party dependencies +internal class ClaudeChatClient : IChatClient +{ + private readonly HttpClient _httpClient; + private const string ApiBaseUrl = "https://api.anthropic.com/v1/messages"; + + public ClaudeChatClient() + { + var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? + throw new InvalidOperationException("ANTHROPIC_API_KEY environment variable is required"); + + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("x-api-key", apiKey); + _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); + } + + public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + var requestBody = CreateRequestBody(messages, options, false); + var json = JsonSerializer.Serialize(requestBody); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); + response.EnsureSuccessStatusCode(); + + var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); + using var doc = JsonDocument.Parse(responseJson); + + var usage = new UsageDetails(); + var responseText = "No response"; + if (doc.RootElement.TryGetProperty("content", out var contentArray) && + contentArray.ValueKind == JsonValueKind.Array) + { + var firstContent = contentArray.EnumerateArray().FirstOrDefault(); + if (firstContent.TryGetProperty("text", out var textProperty)) + { + responseText = textProperty.GetString() ?? "No response"; + } + } + if (doc.RootElement.TryGetProperty("usage", out var usageProperty)) + { + if (usageProperty.TryGetProperty("input_tokens", out var inputTokens) && inputTokens.TryGetInt64(out var inputTokenCount)) + { + usage.InputTokenCount = inputTokenCount; + } + if (usageProperty.TryGetProperty("output_tokens", out var outputTokens) && outputTokens.TryGetInt64(out var outputTokenCount)) + { + usage.OutputTokenCount = outputTokenCount; + } + } + + var responseMessage = new ChatMessage(ChatRole.Assistant, responseText); + var chatResponse = new ChatResponse(responseMessage) + { + Usage = usage + }; + + return chatResponse; + } + + public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, + ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var requestBody = CreateRequestBody(messages, options, true); + var json = JsonSerializer.Serialize(requestBody); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); + response.EnsureSuccessStatusCode(); + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + using var reader = new StreamReader(stream); + + while (await reader.ReadLineAsync(cancellationToken) is { } line) + { + if (!line.StartsWith("data: ") || line.Length <= 6) + { + continue; + } + + var eventData = line.Substring(6); + if (eventData == "[DONE]") + { + break; + } + + ClaudeStreamEvent? streamEvent = null; + try + { + streamEvent = JsonSerializer.Deserialize(eventData); + } + catch (JsonException) + { + // Skip malformed JSON + continue; + } + + if (streamEvent?.Type == "content_block_delta" && + streamEvent.Delta?.Type == "text_delta") + { + yield return new ChatResponseUpdate(null, streamEvent.Delta.Text); + } + } + } + + private object CreateRequestBody(IEnumerable messages, ChatOptions? options, bool stream) + { + var claudeMessages = messages + .Where(m => m.Role != ChatRole.System) + .Select(m => new + { + role = m.Role == ChatRole.User ? "user" : "assistant", + content = m.Text ?? "" + }) + .ToArray(); + + return new + { + model = "claude-3-5-sonnet-20241022", + max_tokens = options?.MaxOutputTokens ?? 1000, + messages = claudeMessages, + stream = stream + }; + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() + { + _httpClient?.Dispose(); + } +} + +internal class ClaudeStreamEvent +{ + public string? Type { get; set; } + public ClaudeStreamDelta? Delta { get; set; } +} + +internal class ClaudeStreamDelta +{ + public string? Type { get; set; } + public string? Text { get; set; } +} diff --git a/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj b/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj new file mode 100644 index 0000000000..c9dc14143d --- /dev/null +++ b/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + + + + + + + + + sentry-sdks + sentry-dotnet + true + true + + + + + + + + diff --git a/src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs b/src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..a7917f2593 --- /dev/null +++ b/src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.AI; +using Sentry.Extensibility; + +// ReSharper disable once CheckNamespace -- Discoverability +namespace Sentry.Extensions.AI; + +/// +/// Extensions to instrument Microsoft.Extensions.AI builders with Sentry +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class SentryAIExtensions +{ + // + // Adds Sentry instrumentation to the ChatClientBuilder pipeline. + // + // public static ChatClientBuilder UseSentry(this ChatClientBuilder builder, string? agentName = null, string? model = null, string? system = null) + // { + // return builder.Use(inner => + // { + // // Try to get IHub from DI first, fallback to HubAdapter.Instance + // var hub = inner.GetService() ?? HubAdapter.Instance; + // return new SentryChatClient(inner, hub, agentName, model, system); + // }); + // } + + /// + /// Wraps an IChatClient with Sentry instrumentation. + /// + public static IChatClient WithSentry(this IChatClient client, string? agentName = null, string? system = null) + { + return new SentryChatClient(client, HubAdapter.Instance, agentName, system); + } +} diff --git a/src/Sentry.Extensions.AI/Sentry.Extensions.AI.csproj b/src/Sentry.Extensions.AI/Sentry.Extensions.AI.csproj new file mode 100644 index 0000000000..891c917097 --- /dev/null +++ b/src/Sentry.Extensions.AI/Sentry.Extensions.AI.csproj @@ -0,0 +1,21 @@ + + + + net9.0;net8.0 + $(PackageTags);Microsoft.Extensions.AI;AI;LLM + Microsoft.Extensions.AI integration for Sentry - captures AI Agent telemetry spans per Sentry AI Agents module. + + + + + + + + + + + + + + + diff --git a/src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs b/src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs new file mode 100644 index 0000000000..4f6154ed39 --- /dev/null +++ b/src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.AI; + +namespace Sentry.Extensions.AI; + +public static class SentryAIFunctionWrapper +{ + private static AITool WrapToolCall(AITool aiTool, IHub hub) + { + if (aiTool is not AIFunction tool) + { + return aiTool; + } + + tool.UnderlyingMethod = () => + { + const string operation = "gen_ai.execute_tool"; + var spanName = aiTool.Name; + var toolSpan = hub.GetSpan()?.StartChild(operation, spanName) ?? hub.StartTransaction(spanName, operation); + var returnVal = tool.UnderlyingMethod.Invoke(); + return returnVal; + } + } + +} diff --git a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs new file mode 100644 index 0000000000..07311d84f0 --- /dev/null +++ b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs @@ -0,0 +1,121 @@ +using Microsoft.Extensions.AI; + +namespace Sentry.Extensions.AI; + +/// +/// Populates various span attributes specific to AI +/// +public static class SentryAISpanEnricher +{ + /// + /// Enrich a span with request information + /// + /// Span to enrich + /// Messages + /// Options + /// Agent's name + /// The AI product (e.g. OpenAI, Anthropic, etc) + public static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatOptions? options, string? agentName, string? system) + { + span.SetData("gen_ai.operation.name", "chat"); + if (system is { Length: > 0 }) + { + span.SetData("gen_ai.system", system); + } + + if (options?.ModelId is { } modelId) + { + span.SetData("gen_ai.request.model", modelId); + } + else + { + span.SetData("gen_ai.request.model", "Unknown model"); + } + + if (messages is { Length: > 0 }) + { + span.SetData("gen_ai.request.messages", FormatRequestMessage(messages)); + } + + if (options?.Tools is { } tools) + { + span.SetData("gen_ai.request.available_tools", FormatAvailableTools(tools)); + } + + if (options?.Temperature is { } temperature) + { + span.SetData("gen_ai.request.temperature", temperature); + } + + if (options?.MaxOutputTokens is { } maxOutputTokens) + { + span.SetData("gen_ai.request.max_tokens", maxOutputTokens); + } + + if (options?.TopP is { } topP) + { + span.SetData("gen_ai.request.top_p", topP); + } + + if (options?.FrequencyPenalty is { } frequencyPenalty) + { + span.SetData("gen_ai.request.frequency_penalty", frequencyPenalty); + } + + if (options?.PresencePenalty is { } presencePenalty) + { + span.SetData("gen_ai.request.presence_penalty", presencePenalty); + } + + if (agentName is { Length: > 0 }) + { + span.SetData("gen_ai.agent.name", agentName); + } + } + + /// + /// Enriches the span using the response. + /// + public static void EnrichWithResponse(ISpan span, ChatResponse response) + { + if (response.Usage is { } usage) + { + var inputTokens = usage.InputTokenCount; + var outputTokens = usage.OutputTokenCount; + + if (inputTokens.HasValue) + { + span.SetData("gen_ai.usage.input_tokens", inputTokens.Value); + } + + if (outputTokens.HasValue) + { + span.SetData("gen_ai.usage.output_tokens", outputTokens.Value); + } + + if (inputTokens.HasValue && outputTokens.HasValue) + { + span.SetData("gen_ai.usage.total_tokens", inputTokens.Value + outputTokens.Value); + } + } + + if (response.Text is { } responseText) + { + span.SetData("gen_ai.response.text", responseText); + } + + if (response.ModelId is { } modelId) + { + span.SetData("gen_ai.response.model_id", modelId); + } + } + + private static string FormatAvailableTools(IList tools) => + FormatAsJson(tools, tool => new { name = tool.Name, description = tool.Description }); + + private static string FormatRequestMessage(ChatMessage[] messages) => + FormatAsJson(messages, message => new { role = message.Role, content = message.Text }); + + private static string FormatAsJson(IEnumerable items, Func selector) => + JsonSerializer.Serialize(items.Select(selector)); +} diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs new file mode 100644 index 0000000000..178bd034c5 --- /dev/null +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.AI; + +namespace Sentry.Extensions.AI; + +internal sealed class SentryChatClient( + IChatClient innerClient, + IHub hub, + string? agentName, + string? system) + : DelegatingChatClient(innerClient) +{ + public override async Task GetResponseAsync(IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = new()) + { + const string operation = "gen_ai.chat"; + var spanName = agentName is { Length: > 0 } ? $"chat {agentName}" : "chat"; + var initialSpan = hub.GetSpan()?.StartChild(operation, spanName) ?? hub.StartTransaction(spanName, operation); + + try + { + var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); + SentryAISpanEnricher.EnrichWithRequest(initialSpan, chatMessages, options, agentName, system); + + var response = await base.GetResponseAsync(chatMessages, options, cancellationToken).ConfigureAwait(false); + + SentryAISpanEnricher.EnrichWithResponse(initialSpan, response); + initialSpan.Finish(SpanStatus.Ok); + return response; + } + catch (Exception ex) + { + initialSpan.Finish(ex); + hub.CaptureException(ex); + throw; + } + } +} diff --git a/test/Sentry.Extensions.AI.Tests/Sentry.Extensions.AI.Tests.csproj b/test/Sentry.Extensions.AI.Tests/Sentry.Extensions.AI.Tests.csproj new file mode 100644 index 0000000000..601422bf85 --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/Sentry.Extensions.AI.Tests.csproj @@ -0,0 +1,17 @@ + + + + net9.0;net8.0 + + + + + + + + + + + + + diff --git a/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs b/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs new file mode 100644 index 0000000000..f9739e1f71 --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs @@ -0,0 +1,59 @@ +#nullable enable +using Sentry.Extensions.AI; +using Microsoft.Extensions.AI; + +namespace Sentry.Extensions.AI.Tests; + +public class SentryChatClientTests +{ + [Fact] + public async Task CompleteAsync_CallsInnerClient() + { + var inner = Substitute.For(); + var message = new ChatMessage(ChatRole.Assistant, "ok"); + var chatResponse = new ChatResponse(message); + inner.GetResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(chatResponse)); + + var hub = Substitute.For(); + var sentryChatClient = new SentryChatClient(inner, hub, agentName: "Agent", model: "gpt-4o-mini", system: "openai"); + + var res = await sentryChatClient.GetResponseAsync([new ChatMessage(ChatRole.User, "hi")], null); + + Assert.Equal([message], res.Messages); + await inner.Received(1).GetResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task CompleteStreamingAsync_CallsInnerClient() + { + var inner = Substitute.For(); + + inner.GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(CreateTestStreamingUpdates()); + + var hub = Substitute.For(); + var client = new SentryChatClient(inner, hub, agentName: "Agent", model: "gpt-4o-mini", system: "openai"); + + var results = new List(); + await foreach (var update in client.GetStreamingResponseAsync([new ChatMessage(ChatRole.User, "hi")], null)) + { + results.Add(update); + } + + Assert.Equal(2, results.Count); + Assert.Equal("Hello", results[0].Text); + Assert.Equal(" World!", results[1].Text); + + inner.Received(1).GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()); + } + + private static async IAsyncEnumerable CreateTestStreamingUpdates() + { + yield return new ChatResponseUpdate(ChatRole.System, "Hello"); + await Task.Yield(); // Make it actually async + yield return new ChatResponseUpdate(ChatRole.System, " World!"); + } +} + + From d063ab9265f93ec43bd1999058fed24759abe976 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Fri, 17 Oct 2025 10:32:53 -0400 Subject: [PATCH 02/22] basic tool call working --- .../Extensions/SentryAIExtensions.cs | 47 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 33 ------------- .../SentryAIFunctionWrapper.cs | 24 ---------- .../SentryAISpanEnricher.cs | 17 ++----- src/Sentry.Extensions.AI/SentryChatClient.cs | 8 ++-- .../SentryInstrumentedFunction.cs | 47 +++++++++++++++++++ 6 files changed, 101 insertions(+), 75 deletions(-) create mode 100644 src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs delete mode 100644 src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs create mode 100644 src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs diff --git a/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs new file mode 100644 index 0000000000..48047d17f4 --- /dev/null +++ b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.AI; +using Sentry.Extensibility; + +// ReSharper disable once CheckNamespace -- Discoverability +namespace Sentry.Extensions.AI; + +/// +/// Extensions to instrument Microsoft.Extensions.AI builders with Sentry +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class SentryAIExtensions +{ + /// + /// Wrap tool calls specified in with Sentry agent instrumentation + /// + public static ChatOptions WithSentry( + this ChatOptions options) + { + if (options.Tools is null || options.Tools.Count == 0) + { + return options; + } + + for (var i = 0; i < options.Tools.Count; i++) + { + var tool = options.Tools[i]; + if (tool is AIFunction fn and not SentryInstrumentedFunction) + { + options.Tools[i] = new SentryInstrumentedFunction(fn); + } + else + { + options.Tools[i] = tool; + } + } + + return options; + } + + /// + /// Wraps an IChatClient with Sentry instrumentation. + /// + public static IChatClient WithSentry(this IChatClient client) + { + return new SentryChatClient(client, HubAdapter.Instance); + } +} diff --git a/src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs b/src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index a7917f2593..0000000000 --- a/src/Sentry.Extensions.AI/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Extensions.AI; -using Sentry.Extensibility; - -// ReSharper disable once CheckNamespace -- Discoverability -namespace Sentry.Extensions.AI; - -/// -/// Extensions to instrument Microsoft.Extensions.AI builders with Sentry -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public static class SentryAIExtensions -{ - // - // Adds Sentry instrumentation to the ChatClientBuilder pipeline. - // - // public static ChatClientBuilder UseSentry(this ChatClientBuilder builder, string? agentName = null, string? model = null, string? system = null) - // { - // return builder.Use(inner => - // { - // // Try to get IHub from DI first, fallback to HubAdapter.Instance - // var hub = inner.GetService() ?? HubAdapter.Instance; - // return new SentryChatClient(inner, hub, agentName, model, system); - // }); - // } - - /// - /// Wraps an IChatClient with Sentry instrumentation. - /// - public static IChatClient WithSentry(this IChatClient client, string? agentName = null, string? system = null) - { - return new SentryChatClient(client, HubAdapter.Instance, agentName, system); - } -} diff --git a/src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs b/src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs deleted file mode 100644 index 4f6154ed39..0000000000 --- a/src/Sentry.Extensions.AI/SentryAIFunctionWrapper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.AI; - -namespace Sentry.Extensions.AI; - -public static class SentryAIFunctionWrapper -{ - private static AITool WrapToolCall(AITool aiTool, IHub hub) - { - if (aiTool is not AIFunction tool) - { - return aiTool; - } - - tool.UnderlyingMethod = () => - { - const string operation = "gen_ai.execute_tool"; - var spanName = aiTool.Name; - var toolSpan = hub.GetSpan()?.StartChild(operation, spanName) ?? hub.StartTransaction(spanName, operation); - var returnVal = tool.UnderlyingMethod.Invoke(); - return returnVal; - } - } - -} diff --git a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs index 07311d84f0..92e5060787 100644 --- a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs +++ b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs @@ -5,7 +5,7 @@ namespace Sentry.Extensions.AI; /// /// Populates various span attributes specific to AI /// -public static class SentryAISpanEnricher +internal static class SentryAISpanEnricher { /// /// Enrich a span with request information @@ -13,15 +13,11 @@ public static class SentryAISpanEnricher /// Span to enrich /// Messages /// Options - /// Agent's name - /// The AI product (e.g. OpenAI, Anthropic, etc) - public static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatOptions? options, string? agentName, string? system) + internal static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatOptions? options) { + // Currently, all top-level spans will start as "chat" + // The agent creation/invocation doesn't really work in Microsoft.Extensions.AI span.SetData("gen_ai.operation.name", "chat"); - if (system is { Length: > 0 }) - { - span.SetData("gen_ai.system", system); - } if (options?.ModelId is { } modelId) { @@ -66,11 +62,6 @@ public static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatOpt { span.SetData("gen_ai.request.presence_penalty", presencePenalty); } - - if (agentName is { Length: > 0 }) - { - span.SetData("gen_ai.agent.name", agentName); - } } /// diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index 178bd034c5..c4fe411af6 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -4,9 +4,7 @@ namespace Sentry.Extensions.AI; internal sealed class SentryChatClient( IChatClient innerClient, - IHub hub, - string? agentName, - string? system) + IHub hub) : DelegatingChatClient(innerClient) { public override async Task GetResponseAsync(IEnumerable messages, @@ -14,13 +12,13 @@ public override async Task GetResponseAsync(IEnumerable 0 } ? $"chat {agentName}" : "chat"; + var spanName = InnerClient.GetType().Name; var initialSpan = hub.GetSpan()?.StartChild(operation, spanName) ?? hub.StartTransaction(spanName, operation); try { var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); - SentryAISpanEnricher.EnrichWithRequest(initialSpan, chatMessages, options, agentName, system); + SentryAISpanEnricher.EnrichWithRequest(initialSpan, chatMessages, options); var response = await base.GetResponseAsync(chatMessages, options, cancellationToken).ConfigureAwait(false); diff --git a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs new file mode 100644 index 0000000000..8635c2e30e --- /dev/null +++ b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.AI; +using Sentry.Extensibility; + +namespace Sentry.Extensions.AI; + +internal sealed class SentryInstrumentedFunction(AIFunction innerFunction) : DelegatingAIFunction(innerFunction) +{ + private readonly HubAdapter _hub = HubAdapter.Instance; + + protected override async ValueTask InvokeCoreAsync( + AIFunctionArguments arguments, + CancellationToken cancellationToken) + { + var parentSpan = _hub.GetSpan(); + const string operation = "gen_ai.execute_tool"; + var spanName = $"execute_tool {Name}"; + var currSpan = parentSpan?.StartChild(operation, spanName) ?? _hub.StartTransaction(spanName, operation); + + currSpan.SetData("gen_ai.operation.name", "execute_tool"); + currSpan.SetData("gen_ai.tool.name", Name); + + if (Description is { } description) + { + currSpan.SetData("gen_ai.tool.description", description); + } + + currSpan.SetData("gen_ai.tool.input", arguments); + + try + { + var result = await base.InvokeCoreAsync(arguments, cancellationToken).ConfigureAwait(false); + + if (result?.ToString() is { } resultString) + { + currSpan.SetData("gen_ai.tool.output", resultString); + } + + currSpan.Finish(SpanStatus.Ok); + return result; + } + catch (Exception ex) + { + currSpan.Finish(ex); + throw; + } + } +} From eaea3905913fac51ad070b8b657b04fbb0d1f52c Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Mon, 20 Oct 2025 01:27:48 -0400 Subject: [PATCH 03/22] add tests and tool instrumentation --- .../Program.cs | 303 ++---- .../Sentry.Samples.ME.AI.Console/Program.cs | 252 ++--- .../Sentry.Samples.ME.AI.Console.csproj | 1 + .../Extensions/SentryAIExtensions.cs | 19 +- src/Sentry.Extensions.AI/SentryAIOptions.cs | 26 + .../SentryAISpanEnricher.cs | 58 +- src/Sentry.Extensions.AI/SentryChatClient.cs | 116 ++- .../SentryInstrumentedFunction.cs | 6 +- src/Sentry/Sentry.csproj | 4 +- .../SentryAIExtensionsTests.cs | 205 +++++ .../SentryAIOptionsTests.cs | 112 +++ .../SentryAISpanEnricherTests.cs | 318 +++++++ .../SentryChatClientTests.cs | 12 +- .../SentryInstrumentedFunctionTests.cs | 221 +++++ .../_CVDTQ2MGH4_2025-10-20_00_35_51.trx | 863 ++++++++++++++++++ .../_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx | 863 ++++++++++++++++++ 16 files changed, 2953 insertions(+), 426 deletions(-) create mode 100644 src/Sentry.Extensions.AI/SentryAIOptions.cs create mode 100644 test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs create mode 100644 test/Sentry.Extensions.AI.Tests/SentryAIOptionsTests.cs create mode 100644 test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs create mode 100644 test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs create mode 100644 test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx create mode 100644 test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs index 0826045622..0a0b0ca997 100644 --- a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs @@ -1,11 +1,7 @@ #nullable enable -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using Anthropic.SDK; using Anthropic.SDK.Constants; using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel; using Sentry.Extensions.AI; var builder = WebApplication.CreateBuilder(args); @@ -29,86 +25,105 @@ .AsBuilder() .UseFunctionInvocation() .Build() - .WithSentry(agentName: "Anthropic", system: "anthropic"); + .WithSentry(options => + { + options.IncludeAIRequestMessages = false; + options.IncludeAIResponseContent = false; + }); // Register the Claude API client and Sentry-instrumented chat client -builder.Services.AddKeyedSingleton("claude3_5", client); +builder.Services.AddSingleton(client); var app = builder.Build(); -// Endpoint for regular AI chat -app.MapPost("/chat", async (ChatRequest request, IChatClient chatClient, ILogger logger) => -{ - logger.LogInformation("Handling chat request with message: {Message}", request.Message); - - try - { - var response = await chatClient.GetResponseAsync([ - new ChatMessage(ChatRole.User, request.Message) - ]); - - return Results.Ok(new { response = response.Messages?.FirstOrDefault()?.Text ?? "No response" }); - } - catch (Exception ex) - { - logger.LogError(ex, "Error processing chat request"); - return Results.Problem("An error occurred while processing your request"); - } -}); - -// Endpoint for streaming AI chat -// app.MapPost("/chat/stream", async (ChatRequest request, IChatClient chatClient, ILogger logger) => -// { -// logger.LogInformation("Handling streaming chat request with message: {Message}", request.Message); -// -// return Results.Stream(async stream => -// { -// try -// { -// await foreach (var update in chatClient.GetStreamingResponseAsync([ -// new ChatMessage(ChatRole.User, request.Message) -// ])) -// { -// if (!string.IsNullOrEmpty(update.Text)) -// { -// var bytes = Encoding.UTF8.GetBytes(update.Text); -// await stream.WriteAsync(bytes); -// await stream.FlushAsync(); -// } -// } -// } -// catch (Exception ex) -// { -// logger.LogError(ex, "Error processing streaming chat request"); -// var errorBytes = Encoding.UTF8.GetBytes("\n[Error occurred while streaming response]"); -// await stream.WriteAsync(errorBytes); -// } -// }, "text/plain"); -// }); - -// Health check endpoint -app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow })); - -// Simple test endpoint that demonstrates AI integration +// Simple test endpoint that demonstrates AI integration with multiple tools app.MapGet("/test", async (IChatClient chatClient, ILogger logger) => { - logger.LogInformation("Running AI test endpoint"); - ChatOptions options = new() + logger.LogInformation("Running AI test endpoint with multiple tools"); + var options = new ChatOptions { ModelId = AnthropicModels.Claude3Haiku, - MaxOutputTokens = 512, - Tools = [AIFunctionFactory.Create((string personName) => personName switch { - "Alice" => "25", - _ => "40" - }, "GetPersonAge", "Gets the age of the person whose name is specified.")] - }; + MaxOutputTokens = 1024, + Tools = [ + // Tool 1: Quick response with minimal delay + AIFunctionFactory.Create(async (string personName) => + { + logger.LogInformation("GetPersonAge called for {PersonName}", personName); + await Task.Delay(100); // 100ms delay + return personName switch { + "Alice" => "25", + "Bob" => "30", + "Charlie" => "35", + _ => "40" + }; + }, "GetPersonAge", "Gets the age of the person whose name is specified. Takes about 100ms to complete."), + + // Tool 2: Medium delay tool for weather + AIFunctionFactory.Create(async (string location) => + { + logger.LogInformation("GetWeather called for {Location}", location); + await Task.Delay(500); // 500ms delay + return location.ToLower() switch { + "new york" => "Sunny, 72°F", + "london" => "Cloudy, 60°F", + "tokyo" => "Rainy, 68°F", + _ => "Unknown weather conditions" + }; + }, "GetWeather", "Gets the current weather for a location. Takes about 500ms to complete."), + + // Tool 3: Slow tool for complex calculation + AIFunctionFactory.Create(async (int number) => + { + logger.LogInformation("ComplexCalculation called with {Number}", number); + await Task.Delay(1000); // 1000ms delay + var result = (number * number) + (number * 10); + return $"Complex calculation result for {number}: {result}"; + }, "ComplexCalculation", "Performs a complex mathematical calculation. Takes about 1 second to complete."), + + // Tool 4: Tool that can reference other data + AIFunctionFactory.Create(async (string personName, string location) => + { + logger.LogInformation("GetPersonInfo called for {PersonName} in {Location}", personName, location); + await Task.Delay(300); // 300ms delay + var age = personName switch { + "Alice" => "25", + "Bob" => "30", + "Charlie" => "35", + _ => "40" + }; + var weather = location.ToLower() switch { + "new york" => "Sunny, 72°F", + "london" => "Cloudy, 60°F", + "tokyo" => "Rainy, 68°F", + _ => "Unknown weather conditions" + }; + return $"{personName} (age {age}) is experiencing {weather} weather in {location}"; + }, "GetPersonInfo", "Gets comprehensive info about a person in a specific location by combining age and weather data. Takes about 300ms."), + + // Tool 5: Data aggregation tool that requests individual ages + AIFunctionFactory.Create(async (int[] ages) => + { + logger.LogInformation("CalculateAverageAge called with ages: {Ages}", string.Join(", ", ages)); + await Task.Delay(200); // 200ms delay for calculation + if (ages.Length == 0) + { + return "No ages provided"; + } + + var average = ages.Average(); + return $"Average age calculated: {average:F1} years from {ages.Length} people. Individual ages: {string.Join(", ", ages)}"; + }, "CalculateAverageAge", "Calculates the average from a list of ages. You should first get individual ages using GetPersonAge, then use this tool to calculate the average. Takes about 200ms to complete.") + ] + }.WithSentry(); try { - var response = await chatClient.GetResponseAsync("How old is Alice?", options); + var response = await chatClient.GetResponseAsync( + "Please help me with the following tasks: 1) Find Alice's age, 2) Get weather in New York, 3) Calculate a complex result for number 15, 4) Get comprehensive info for Bob in London, and 5) Calculate average age for Alice, Bob, and Charlie (first get each person's age individually using GetPersonAge, then use CalculateAverageAge with those results). Please use the appropriate tools for each task and demonstrate tool chaining where needed.", + options); return Results.Ok(new { - message = "AI test completed successfully", + message = "AI test with multiple tools completed successfully", response = response.Messages?.FirstOrDefault()?.Text ?? "No response", timestamp = DateTime.UtcNow }); @@ -121,151 +136,3 @@ }); app.Run(); - -public record ChatRequest(string Message); - -// Claude API client using HttpClient without third-party dependencies -internal class ClaudeChatClient : IChatClient -{ - private readonly HttpClient _httpClient; - private const string ApiBaseUrl = "https://api.anthropic.com/v1/messages"; - - public ClaudeChatClient() - { - var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? - throw new InvalidOperationException("ANTHROPIC_API_KEY environment variable is required"); - - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.Add("x-api-key", apiKey); - _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); - } - - public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, - CancellationToken cancellationToken = default) - { - var requestBody = CreateRequestBody(messages, options, false); - var json = JsonSerializer.Serialize(requestBody); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); - - var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); - using var doc = JsonDocument.Parse(responseJson); - - var usage = new UsageDetails(); - var responseText = "No response"; - if (doc.RootElement.TryGetProperty("content", out var contentArray) && - contentArray.ValueKind == JsonValueKind.Array) - { - var firstContent = contentArray.EnumerateArray().FirstOrDefault(); - if (firstContent.TryGetProperty("text", out var textProperty)) - { - responseText = textProperty.GetString() ?? "No response"; - } - } - if (doc.RootElement.TryGetProperty("usage", out var usageProperty)) - { - if (usageProperty.TryGetProperty("input_tokens", out var inputTokens) && inputTokens.TryGetInt64(out var inputTokenCount)) - { - usage.InputTokenCount = inputTokenCount; - } - if (usageProperty.TryGetProperty("output_tokens", out var outputTokens) && outputTokens.TryGetInt64(out var outputTokenCount)) - { - usage.OutputTokenCount = outputTokenCount; - } - } - - var responseMessage = new ChatMessage(ChatRole.Assistant, responseText); - var chatResponse = new ChatResponse(responseMessage) - { - Usage = usage - }; - - return chatResponse; - } - - public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, - ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var requestBody = CreateRequestBody(messages, options, true); - var json = JsonSerializer.Serialize(requestBody); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); - - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - using var reader = new StreamReader(stream); - - while (await reader.ReadLineAsync(cancellationToken) is { } line) - { - if (!line.StartsWith("data: ") || line.Length <= 6) - { - continue; - } - - var eventData = line.Substring(6); - if (eventData == "[DONE]") - { - break; - } - - ClaudeStreamEvent? streamEvent = null; - try - { - streamEvent = JsonSerializer.Deserialize(eventData); - } - catch (JsonException) - { - // Skip malformed JSON - continue; - } - - if (streamEvent?.Type == "content_block_delta" && - streamEvent.Delta?.Type == "text_delta") - { - yield return new ChatResponseUpdate(null, streamEvent.Delta.Text); - } - } - } - - private object CreateRequestBody(IEnumerable messages, ChatOptions? options, bool stream) - { - var claudeMessages = messages - .Where(m => m.Role != ChatRole.System) - .Select(m => new - { - role = m.Role == ChatRole.User ? "user" : "assistant", - content = m.Text ?? "" - }) - .ToArray(); - - return new - { - model = "claude-3-5-sonnet-20241022", - max_tokens = options?.MaxOutputTokens ?? 1000, - messages = claudeMessages, - stream = stream - }; - } - - public object? GetService(Type serviceType, object? serviceKey = null) => null; - - public void Dispose() - { - _httpClient?.Dispose(); - } -} - -internal class ClaudeStreamEvent -{ - public string? Type { get; set; } - public ClaudeStreamDelta? Delta { get; set; } -} - -internal class ClaudeStreamDelta -{ - public string? Type { get; set; } - public string? Text { get; set; } -} diff --git a/samples/Sentry.Samples.ME.AI.Console/Program.cs b/samples/Sentry.Samples.ME.AI.Console/Program.cs index 00fd45df72..ff65168df2 100644 --- a/samples/Sentry.Samples.ME.AI.Console/Program.cs +++ b/samples/Sentry.Samples.ME.AI.Console/Program.cs @@ -1,204 +1,112 @@ -#nullable enable -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; +using Anthropic.SDK; +using Anthropic.SDK.Constants; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Sentry.Extensions.AI; -SentrySdk.Init(options => +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger(); + +logger.LogInformation("Starting Microsoft.Extensions.AI sample with Sentry instrumentation"); + +// Create Claude API client and wrap it with Sentry instrumentation +var client = new AnthropicClient().Messages + .AsBuilder() + .UseFunctionInvocation() + .Build() + .WithSentry(options => { #if !SENTRY_DSN_DEFINED_IN_ENV // A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable. // See https://docs.sentry.io/product/sentry-basics/dsn-explainer/ options.Dsn = SamplesShared.Dsn; #endif - // Set to true to SDK debugging to see the internal messages through the logging library. options.Debug = true; - // Configure the level of Sentry internal logging options.DiagnosticLevel = SentryLevel.Debug; options.SampleRate = 1; options.TracesSampleRate = 1; - } -); -using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); -var logger = loggerFactory.CreateLogger(); - -logger.LogInformation("Starting Microsoft.Extensions.AI sample with Sentry instrumentation"); + // AI-specific settings + options.IncludeAIRequestMessages = false; + options.IncludeAIResponseContent = false; + options.InitializeSdk = true; + }); -// Create Claude API client and wrap it with Sentry instrumentation -var claudeClient = new ClaudeChatClient(); -var chat = claudeClient.WithSentry(agentName: "Anthropic ", system: "anthropic"); +logger.LogInformation("Making AI call with Sentry instrumentation and tools..."); -logger.LogInformation("Making AI call with Sentry instrumentation..."); +var options = new ChatOptions +{ + ModelId = AnthropicModels.Claude3Haiku, + MaxOutputTokens = 1024, + Tools = [ + // Tool 1: Quick response with minimal delay + AIFunctionFactory.Create(async (string personName) => + { + logger.LogInformation("GetPersonAge called for {PersonName}", personName); + await Task.Delay(100); // 100ms delay + return personName switch { + "Alice" => "25", + "Bob" => "30", + "Charlie" => "35", + _ => "40" + }; + }, "GetPersonAge", "Gets the age of the person whose name is specified. Takes about 100ms to complete."), + + // Tool 2: Medium delay tool for weather + AIFunctionFactory.Create(async (string location) => + { + logger.LogInformation("GetWeather called for {Location}", location); + await Task.Delay(500); // 500ms delay + return location.ToLower() switch { + "new york" => "Sunny, 72°F", + "london" => "Cloudy, 60°F", + "tokyo" => "Rainy, 68°F", + _ => "Unknown weather conditions" + }; + }, "GetWeather", "Gets the current weather for a location. Takes about 500ms to complete."), + + // Tool 3: Slow tool for complex calculation + AIFunctionFactory.Create(async (int number) => + { + logger.LogInformation("ComplexCalculation called with {Number}", number); + await Task.Delay(1000); // 1000ms delay + var result = (number * number) + (number * 10); + return $"Complex calculation result for {number}: {result}"; + }, "ComplexCalculation", "Performs a complex mathematical calculation. Takes about 1 second to complete.") + ] +}.WithSentry(); -var response = await chat.GetResponseAsync([ - new ChatMessage(ChatRole.User, "Say hello from Sentry sample") -]); +var response = await client.GetResponseAsync( + "Please help me with the following tasks: 1) Find Alice's age, 2) Get weather in New York, and 3) Calculate a complex result for number 15. Please use the appropriate tools for each task.", + options); -logger.LogInformation("Response: {ResponseText}", response.Messages); +logger.LogInformation("Response: {ResponseText}", response.Messages?.FirstOrDefault()?.Text ?? "No response"); // Demonstrate streaming with Sentry instrumentation logger.LogInformation("Making streaming AI call with Sentry instrumentation..."); +var streamingOptions = new ChatOptions +{ + ModelId = AnthropicModels.Claude3Haiku, + MaxOutputTokens = 1024 +}.WithSentry(); + var streamingResponse = new List(); -await foreach (var update in chat.GetStreamingResponseAsync([ - new ChatMessage(ChatRole.User, "Say hello and goodbye with streaming") - ])) +await foreach (var update in client.GetStreamingResponseAsync([ + new ChatMessage(ChatRole.User, "Write a short poem about AI and monitoring. Keep it under 50 words.") + ], streamingOptions)) { - streamingResponse.Add(update.Text ?? ""); + if (!string.IsNullOrEmpty(update.Text)) + { + Console.Write(update.Text); + streamingResponse.Add(update.Text); + } } -logger.LogInformation("Streaming Response: {StreamingText}", string.Join("", streamingResponse)); +Console.WriteLine(); // New line after streaming +logger.LogInformation("Streaming Response completed: {StreamingText}", string.Concat(streamingResponse)); logger.LogInformation("Microsoft.Extensions.AI sample completed! Check your Sentry dashboard for the trace data."); // Flush Sentry to ensure all transactions are sent before the app exits await SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)); - -// Claude API client using HttpClient without third-party dependencies -internal class ClaudeChatClient : IChatClient -{ - private readonly HttpClient _httpClient; - private const string ApiBaseUrl = "https://api.anthropic.com/v1/messages"; - - public ClaudeChatClient() - { - var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? - throw new InvalidOperationException("ANTHROPIC_API_KEY environment variable is required"); - - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.Add("x-api-key", apiKey); - _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); - } - - public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, - CancellationToken cancellationToken = default) - { - var requestBody = CreateRequestBody(messages, options, false); - var json = JsonSerializer.Serialize(requestBody); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); - - var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); - using var doc = JsonDocument.Parse(responseJson); - - var usage = new UsageDetails(); - var responseText = "No response"; - if (doc.RootElement.TryGetProperty("content", out var contentArray) && - contentArray.ValueKind == JsonValueKind.Array) - { - var firstContent = contentArray.EnumerateArray().FirstOrDefault(); - if (firstContent.TryGetProperty("text", out var textProperty)) - { - responseText = textProperty.GetString() ?? "No response"; - } - } - if (doc.RootElement.TryGetProperty("usage", out var usageProperty)) - { - if (usageProperty.TryGetProperty("input_tokens", out var inputTokens) && inputTokens.TryGetInt64(out var inputTokenCount)) - { - usage.InputTokenCount = inputTokenCount; - } - if (usageProperty.TryGetProperty("output_tokens", out var outputTokens) && outputTokens.TryGetInt64(out var outputTokenCount)) - { - usage.OutputTokenCount = outputTokenCount; - } - } - - var responseMessage = new ChatMessage(ChatRole.Assistant, responseText); - var chatResponse = new ChatResponse(responseMessage) - { - Usage = usage - }; - - return chatResponse; - } - - public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, - ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var requestBody = CreateRequestBody(messages, options, true); - var json = JsonSerializer.Serialize(requestBody); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync(ApiBaseUrl, content, cancellationToken); - response.EnsureSuccessStatusCode(); - - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - using var reader = new StreamReader(stream); - - while (await reader.ReadLineAsync(cancellationToken) is { } line) - { - if (!line.StartsWith("data: ") || line.Length <= 6) - { - continue; - } - - var eventData = line.Substring(6); - if (eventData == "[DONE]") - { - break; - } - - ClaudeStreamEvent? streamEvent = null; - try - { - streamEvent = JsonSerializer.Deserialize(eventData); - } - catch (JsonException) - { - // Skip malformed JSON - continue; - } - - if (streamEvent?.Type == "content_block_delta" && - streamEvent.Delta?.Type == "text_delta") - { - yield return new ChatResponseUpdate(null, streamEvent.Delta.Text); - } - } - } - - private object CreateRequestBody(IEnumerable messages, ChatOptions? options, bool stream) - { - var claudeMessages = messages - .Where(m => m.Role != ChatRole.System) - .Select(m => new - { - role = m.Role == ChatRole.User ? "user" : "assistant", - content = m.Text ?? "" - }) - .ToArray(); - - return new - { - model = "claude-3-5-sonnet-20241022", - max_tokens = options?.MaxOutputTokens ?? 1000, - messages = claudeMessages, - stream = stream - }; - } - - public object? GetService(Type serviceType, object? serviceKey = null) => null; - - public void Dispose() - { - _httpClient?.Dispose(); - } -} - -internal class ClaudeStreamEvent -{ - public string? Type { get; set; } - public ClaudeStreamDelta? Delta { get; set; } -} - -internal class ClaudeStreamDelta -{ - public string? Type { get; set; } - public string? Text { get; set; } -} diff --git a/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj b/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj index c9dc14143d..83db5db8a8 100644 --- a/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj +++ b/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs index 48047d17f4..f05fac1822 100644 --- a/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs +++ b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs @@ -13,6 +13,7 @@ public static class SentryAIExtensions /// /// Wrap tool calls specified in with Sentry agent instrumentation /// + /// The that contains the to instrument public static ChatOptions WithSentry( this ChatOptions options) { @@ -28,20 +29,24 @@ public static ChatOptions WithSentry( { options.Tools[i] = new SentryInstrumentedFunction(fn); } - else - { - options.Tools[i] = tool; - } } return options; } /// - /// Wraps an IChatClient with Sentry instrumentation. + /// Wraps an IChatClient with Sentry agent instrumentation. /// - public static IChatClient WithSentry(this IChatClient client) + /// + /// This method can be used either with an existing Sentry setup or as a standalone integration. + /// If Sentry is already initialized, it will use the existing configuration. + /// If not, it will initialize Sentry with the provided options. + /// + /// The to be instrumented + /// The configuration + /// The instrumented + public static IChatClient WithSentry(this IChatClient client, Action? configure = null) { - return new SentryChatClient(client, HubAdapter.Instance); + return new SentryChatClient(client, configure); } } diff --git a/src/Sentry.Extensions.AI/SentryAIOptions.cs b/src/Sentry.Extensions.AI/SentryAIOptions.cs new file mode 100644 index 0000000000..4d839e2c57 --- /dev/null +++ b/src/Sentry.Extensions.AI/SentryAIOptions.cs @@ -0,0 +1,26 @@ +namespace Sentry.Extensions.AI; + +/// +/// Sentry AI instrumentation options +/// +/// +public class SentryAIOptions : SentryOptions +{ + /// + /// Whether to include LLM request messages in spans. + /// + public bool IncludeAIRequestMessages { get; set; } = true; + + /// + /// Whether to include LLM response content in spans. + /// + public bool IncludeAIResponseContent { get; set; } = true; + + /// + /// Whether to initialize the Sentry SDK through this integration. + /// + /// + /// If you have already set up Sentry in your application, there is no need to re-initialize the Sentry SDK + /// + public bool InitializeSdk { get; set; } = false; +} diff --git a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs index 92e5060787..6d8697e90d 100644 --- a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs +++ b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.AI; +using System.Text.Json; namespace Sentry.Extensions.AI; @@ -8,12 +9,13 @@ namespace Sentry.Extensions.AI; internal static class SentryAISpanEnricher { /// - /// Enrich a span with request information + /// Enriches a span with request information. /// /// Span to enrich /// Messages /// Options - internal static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatOptions? options) + /// AI-specific options + internal static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatOptions? options, SentryAIOptions? aiOptions = null) { // Currently, all top-level spans will start as "chat" // The agent creation/invocation doesn't really work in Microsoft.Extensions.AI @@ -28,7 +30,7 @@ internal static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatO span.SetData("gen_ai.request.model", "Unknown model"); } - if (messages is { Length: > 0 }) + if (messages is { Length: > 0 } && (aiOptions?.IncludeAIRequestMessages ?? true)) { span.SetData("gen_ai.request.messages", FormatRequestMessage(messages)); } @@ -65,9 +67,12 @@ internal static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatO } /// - /// Enriches the span using the response. + /// Enriches the span with response information. /// - public static void EnrichWithResponse(ISpan span, ChatResponse response) + /// Span to enrich + /// Chat response containing usage and content data + /// AI-specific options + internal static void EnrichWithResponse(ISpan span, ChatResponse response, SentryAIOptions? aiOptions = null) { if (response.Usage is { } usage) { @@ -90,7 +95,7 @@ public static void EnrichWithResponse(ISpan span, ChatResponse response) } } - if (response.Text is { } responseText) + if (response.Text is { } responseText && (aiOptions?.IncludeAIResponseContent ?? true)) { span.SetData("gen_ai.response.text", responseText); } @@ -101,6 +106,47 @@ public static void EnrichWithResponse(ISpan span, ChatResponse response) } } + /// + /// Enriches the span using the list of streamed in . + /// + /// span to enrich + /// a list of + /// AI-specific options + public static void EnrichWithStreamingResponse(ISpan span, List messages, SentryAIOptions? aiOptions = null) + { + var inputTokenCount = 0L; + var outputTokenCount = 0L; + var finalText = new StringBuilder(); + + foreach (var message in messages) + { + foreach (var content in message.Contents) + { + if (content is UsageContent {} usage) + { + inputTokenCount += usage.Details.InputTokenCount ?? 0; + outputTokenCount += usage.Details.OutputTokenCount ?? 0; + } + } + if (message.ModelId is { } modelId ) + { + span.SetData("gen_ai.response.model_id", modelId); + } + if (message.Text is { } responseText) + { + finalText.Append(responseText); + } + } + + if (aiOptions?.IncludeAIResponseContent ?? true) + { + span.SetData("gen_ai.response.text", finalText.ToString()); + } + span.SetData("gen_ai.usage.input_tokens", inputTokenCount); + span.SetData("gen_ai.usage.output_tokens", outputTokenCount); + span.SetData("gen_ai.usage.total_tokens", inputTokenCount + outputTokenCount); + } + private static string FormatAvailableTools(IList tools) => FormatAsJson(tools, tool => new { name = tool.Name, description = tool.Description }); diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index c4fe411af6..a59870c500 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -1,36 +1,132 @@ using Microsoft.Extensions.AI; +using Sentry.Extensibility; namespace Sentry.Extensions.AI; -internal sealed class SentryChatClient( - IChatClient innerClient, - IHub hub) - : DelegatingChatClient(innerClient) +internal sealed class SentryChatClient : DelegatingChatClient { + private readonly HubAdapter _hub; + private readonly SentryAIOptions _sentryAIOptions; + + public SentryChatClient(IChatClient client, Action? configure = null) : base(client) + { + _sentryAIOptions = new SentryAIOptions(); + configure?.Invoke(_sentryAIOptions); + + if (_sentryAIOptions.InitializeSdk) + { + if (!SentrySdk.IsEnabled || _sentryAIOptions.Dsn is not null) + { + // Initialize Sentry with our options/DSN + var hub = SentrySdk.InitHub(_sentryAIOptions); + SentrySdk.UseHub(hub); + } + } + + _hub = HubAdapter.Instance; + } + + /// public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = new()) { - const string operation = "gen_ai.chat"; - var spanName = InnerClient.GetType().Name; - var initialSpan = hub.GetSpan()?.StartChild(operation, spanName) ?? hub.StartTransaction(spanName, operation); + var invokeSpanName = $"invoke_agent {InnerClient.GetType().Name}"; + const string invokeOperation = "gen_ai.invoke_agent"; + var outerSpan = StartSpanOrTransaction(invokeOperation, invokeSpanName); + + const string chatOperation = "gen_ai.chat"; + var chatSpanName = options is null || string.IsNullOrEmpty(options.ModelId) ? "chat unknown model" : $"chat {options.ModelId}"; + var initialSpan = outerSpan.StartChild(chatOperation, chatSpanName); try { var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); - SentryAISpanEnricher.EnrichWithRequest(initialSpan, chatMessages, options); + SentryAISpanEnricher.EnrichWithRequest(initialSpan, chatMessages, options, _sentryAIOptions); var response = await base.GetResponseAsync(chatMessages, options, cancellationToken).ConfigureAwait(false); - SentryAISpanEnricher.EnrichWithResponse(initialSpan, response); + SentryAISpanEnricher.EnrichWithResponse(initialSpan, response, _sentryAIOptions); initialSpan.Finish(SpanStatus.Ok); + outerSpan.Finish(SpanStatus.Ok); return response; } catch (Exception ex) { initialSpan.Finish(ex); - hub.CaptureException(ex); + outerSpan.Finish(ex); + _hub.CaptureException(ex); + throw; + } + } + + /// + public override IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = new()) + { + var invokeSpanName = $"invoke_agent {InnerClient.GetType().Name}"; + const string invokeOperation = "gen_ai.invoke_agent"; + var outerSpan = StartSpanOrTransaction(invokeOperation, invokeSpanName); + + const string chatOperation = "gen_ai.chat"; + var chatSpanName = options is null || string.IsNullOrEmpty(options.ModelId) ? "chat unknown model" : $"chat {options.ModelId}"; + var initialSpan = outerSpan.StartChild(chatOperation, chatSpanName); + + try + { + return InstrumentStreamingResponseAsync(messages, options, outerSpan, initialSpan, cancellationToken); + } + catch (Exception ex) + { + initialSpan.Finish(ex); + outerSpan.Finish(ex); + _hub.CaptureException(ex); throw; } } + + private async IAsyncEnumerable InstrumentStreamingResponseAsync(IEnumerable messages, + ChatOptions? options, + ISpan outerSpan, + ISpan span, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); + SentryAISpanEnricher.EnrichWithRequest(span, chatMessages, options, _sentryAIOptions); + + var responses = new List(); + var originalStream = base.GetStreamingResponseAsync(chatMessages, options, cancellationToken); + + await foreach (var chunk in originalStream.ConfigureAwait(false)) + { + responses.Add(chunk); + + yield return chunk; + } + + SentryAISpanEnricher.EnrichWithStreamingResponse(span, responses, _sentryAIOptions); + span.Finish(SpanStatus.Ok); + outerSpan.Finish(SpanStatus.Ok); + } + + /// + /// Starts a span or transaction based on whether there's an active transaction context. + /// + /// The operation name + /// The span/transaction description + /// A child span of an existing transaction if available, else a new transaction + private ISpan StartSpanOrTransaction(string operation, string description) + { + var currentSpan = _hub.GetSpan(); + + if (currentSpan?.GetTransaction() != null) + { + return currentSpan.StartChild(operation, description); + } + + var newTransaction = _hub.StartTransaction(description, operation); + _hub.ConfigureScope(scope => scope.Transaction = newTransaction); + return newTransaction; + } } diff --git a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs index 8635c2e30e..4ab001e7c7 100644 --- a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs +++ b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs @@ -18,11 +18,7 @@ internal sealed class SentryInstrumentedFunction(AIFunction innerFunction) : Del currSpan.SetData("gen_ai.operation.name", "execute_tool"); currSpan.SetData("gen_ai.tool.name", Name); - - if (Description is { } description) - { - currSpan.SetData("gen_ai.tool.description", description); - } + currSpan.SetData("gen_ai.tool.description", Description); currSpan.SetData("gen_ai.tool.input", arguments); diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index 0384a1cebd..62e942401e 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -13,7 +13,7 @@ $(TargetFrameworks);net9.0-ios18.0;net8.0-ios17.0 $(TargetFrameworks);net9.0-maccatalyst18.0;net8.0-maccatalyst17.0 - + @@ -156,6 +156,8 @@ + + diff --git a/test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs b/test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs new file mode 100644 index 0000000000..d82b168f7e --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs @@ -0,0 +1,205 @@ +#nullable enable +using Microsoft.Extensions.AI; +using NSubstitute; +using Sentry.Extensions.AI; + +namespace Sentry.Extensions.AI.Tests; + +public class SentryAIExtensionsTests +{ + [Fact] + public void WithSentry_ChatOptions_WithNullTools_ReturnsOriginalOptions() + { + // Arrange + var options = new ChatOptions(); + + // Act + var result = options.WithSentry(); + + // Assert + Assert.Same(options, result); + } + + [Fact] + public void WithSentry_ChatOptions_WithEmptyTools_ReturnsOriginalOptions() + { + // Arrange + var options = new ChatOptions + { + Tools = new List() + }; + + // Act + var result = options.WithSentry(); + + // Assert + Assert.Same(options, result); + } + + [Fact] + public void WithSentry_ChatOptions_WrapsAIFunctionsWithSentryInstrumentedFunction() + { + // Arrange + var mockFunction = Substitute.For(); + mockFunction.Name.Returns("TestFunction"); + mockFunction.Description.Returns("Test Description"); + + var options = new ChatOptions + { + Tools = new List { mockFunction } + }; + + // Act + var result = options.WithSentry(); + + // Assert + Assert.Same(options, result); + Assert.Single(options.Tools); + Assert.IsType(options.Tools[0]); + + var instrumentedFunction = (SentryInstrumentedFunction)options.Tools[0]; + Assert.Equal("TestFunction", instrumentedFunction.Name); + Assert.Equal("Test Description", instrumentedFunction.Description); + } + + [Fact] + public void WithSentry_ChatOptions_DoesNotDoubleWrapSentryInstrumentedFunction() + { + // Arrange + var mockFunction = Substitute.For(); + var alreadyInstrumentedFunction = new SentryInstrumentedFunction(mockFunction); + + var options = new ChatOptions + { + Tools = new List { alreadyInstrumentedFunction } + }; + + // Act + var result = options.WithSentry(); + + // Assert + Assert.Same(options, result); + Assert.Single(options.Tools); + Assert.Same(alreadyInstrumentedFunction, options.Tools[0]); + } + + [Fact] + public void WithSentry_ChatOptions_HandlesMultipleFunctions() + { + // Arrange + var mockFunction1 = Substitute.For(); + mockFunction1.Name.Returns("Function1"); + + var mockFunction2 = Substitute.For(); + mockFunction2.Name.Returns("Function2"); + + var alreadyInstrumentedFunction = new SentryInstrumentedFunction(mockFunction1); + + var options = new ChatOptions + { + Tools = new List + { + mockFunction1, + mockFunction2, + alreadyInstrumentedFunction + } + }; + + // Act + var result = options.WithSentry(); + + // Assert + Assert.Same(options, result); + Assert.Equal(3, options.Tools.Count); + + // First function should be wrapped + Assert.IsType(options.Tools[0]); + Assert.Equal("Function1", options.Tools[0].Name); + + // Second function should be wrapped + Assert.IsType(options.Tools[1]); + Assert.Equal("Function2", options.Tools[1].Name); + + // Third function was already instrumented, should remain unchanged + Assert.Same(alreadyInstrumentedFunction, options.Tools[2]); + } + + [Fact] + public void WithSentry_ChatOptions_IgnoresNonAIFunctionTools() + { + // Arrange + var mockFunction = Substitute.For(); + mockFunction.Name.Returns("TestFunction"); + + var mockNonFunction = Substitute.For(); + mockNonFunction.Name.Returns("NonFunction"); + + var options = new ChatOptions + { + Tools = new List { mockFunction, mockNonFunction } + }; + + // Act + var result = options.WithSentry(); + + // Assert + Assert.Same(options, result); + Assert.Equal(2, options.Tools.Count); + + // AIFunction should be wrapped + Assert.IsType(options.Tools[0]); + Assert.Equal("TestFunction", options.Tools[0].Name); + + // Non-AIFunction should remain unchanged + Assert.Same(mockNonFunction, options.Tools[1]); + } + + [Fact] + public void WithSentry_IChatClient_ReturnsWrappedClient() + { + // Arrange + var mockClient = Substitute.For(); + + // Act + var result = mockClient.WithSentry(); + + // Assert + Assert.IsType(result); + } + + [Fact] + public void WithSentry_IChatClient_WithConfiguration_PassesConfigurationToWrapper() + { + // Arrange + var mockClient = Substitute.For(); + var configureWasCalled = false; + + // Act + var result = mockClient.WithSentry(Configure); + + // Assert + Assert.IsType(result); + Assert.True(configureWasCalled); + return; + + void Configure(SentryAIOptions options) + { + configureWasCalled = true; + options.IncludeAIRequestMessages = false; + options.IncludeAIResponseContent = false; + } + } + + [Fact] + public void WithSentry_IChatClient_WithNullConfiguration_UsesDefaultConfiguration() + { + // Arrange + var mockClient = Substitute.For(); + + // Act + var result = mockClient.WithSentry(null); + + // Assert + Assert.IsType(result); + } +} diff --git a/test/Sentry.Extensions.AI.Tests/SentryAIOptionsTests.cs b/test/Sentry.Extensions.AI.Tests/SentryAIOptionsTests.cs new file mode 100644 index 0000000000..5207f2221a --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/SentryAIOptionsTests.cs @@ -0,0 +1,112 @@ +#nullable enable +using Sentry.Extensions.AI; + +namespace Sentry.Extensions.AI.Tests; + +public class SentryAIOptionsTests +{ + [Fact] + public void Constructor_SetsDefaultValues() + { + // Act + var options = new SentryAIOptions(); + + // Assert + Assert.True(options.IncludeAIRequestMessages); + Assert.True(options.IncludeAIResponseContent); + Assert.False(options.InitializeSdk); + } + + [Fact] + public void IncludeRequestMessages_CanBeSet() + { + // Arrange + var options = new SentryAIOptions + { + // Act + IncludeAIRequestMessages = false + }; + + // Assert + Assert.False(options.IncludeAIRequestMessages); + } + + [Fact] + public void IncludeResponseContent_CanBeSet() + { + // Arrange + var options = new SentryAIOptions + { + // Act + IncludeAIResponseContent = false + }; + + // Assert + Assert.False(options.IncludeAIResponseContent); + } + + [Fact] + public void InitializeSdk_CanBeSet() + { + // Arrange + var options = new SentryAIOptions(); + + // Act + options.InitializeSdk = true; + + // Assert + Assert.True(options.InitializeSdk); + } + + [Fact] + public void InheritsFromSentryOptions() + { + // Arrange & Act + var options = new SentryAIOptions(); + + // Assert + Assert.IsType(options, exactMatch: false); + } + + [Fact] + public void CanSetSentryOptionsProperties() + { + // Arrange + var options = new SentryAIOptions(); + + // Act + options.Dsn = "https://key@sentry.io/project"; + options.Environment = "test"; + options.Release = "1.0.0"; + + // Assert + Assert.Equal("https://key@sentry.io/project", options.Dsn); + Assert.Equal("test", options.Environment); + Assert.Equal("1.0.0", options.Release); + } + + [Theory] + [InlineData(true, true, true)] + [InlineData(true, true, false)] + [InlineData(true, false, true)] + [InlineData(true, false, false)] + [InlineData(false, true, true)] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + [InlineData(false, false, false)] + public void AllPropertyCombinations_WorkCorrectly(bool includeRequest, bool includeResponse, bool initializeSdk) + { + // Arrange + var options = new SentryAIOptions(); + + // Act + options.IncludeAIRequestMessages = includeRequest; + options.IncludeAIResponseContent = includeResponse; + options.InitializeSdk = initializeSdk; + + // Assert + Assert.Equal(includeRequest, options.IncludeAIRequestMessages); + Assert.Equal(includeResponse, options.IncludeAIResponseContent); + Assert.Equal(initializeSdk, options.InitializeSdk); + } +} diff --git a/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs b/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs new file mode 100644 index 0000000000..aa50ae6485 --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs @@ -0,0 +1,318 @@ +#nullable enable +using Microsoft.Extensions.AI; +using NSubstitute; +using Sentry.Extensions.AI; +using System.Text.Json; + +namespace Sentry.Extensions.AI.Tests; + +public class SentryAISpanEnricherTests +{ + private readonly ISpan _mockSpan; + + public SentryAISpanEnricherTests() + { + _mockSpan = Substitute.For(); + } + + [Fact] + public void EnrichWithRequest_SetsBasicOperationName() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, null); + + _mockSpan.Received(1).SetData("gen_ai.operation.name", "chat"); + } + + [Fact] + public void EnrichWithRequest_SetsModelId_WhenProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var options = new ChatOptions { ModelId = "gpt-4" }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, options); + + _mockSpan.Received(1).SetData("gen_ai.request.model", "gpt-4"); + } + + [Fact] + public void EnrichWithRequest_SetsUnknownModel_WhenModelIdNotProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, null); + + _mockSpan.Received(1).SetData("gen_ai.request.model", "Unknown model"); + } + + [Fact] + public void EnrichWithRequest_SetsMessages_WhenIncludeRequestMessagesIsTrue() + { + var messages = new[] { + new ChatMessage(ChatRole.User, "Hello"), + new ChatMessage(ChatRole.Assistant, "Hi there") + }; + var aiOptions = new SentryAIOptions { IncludeAIRequestMessages = true }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, null, aiOptions); + + _mockSpan.Received(1).SetData("gen_ai.request.messages", Arg.Any()); + } + + [Fact] + public void EnrichWithRequest_DoesNotSetMessages_WhenIncludeRequestMessagesIsFalse() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var aiOptions = new SentryAIOptions { IncludeAIRequestMessages = false }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, null, aiOptions); + + _mockSpan.DidNotReceive().SetData("gen_ai.request.messages", Arg.Any()); + } + + [Fact] + public void EnrichWithRequest_SetsMessages_WhenAIOptionsIsNull() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, null, null); + + _mockSpan.Received(1).SetData("gen_ai.request.messages", Arg.Any()); + } + + [Fact] + public void EnrichWithRequest_DoesNotSetMessages_WhenMessagesArrayIsEmpty() + { + var messages = Array.Empty(); + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, null); + + _mockSpan.DidNotReceive().SetData("gen_ai.request.messages", Arg.Any()); + } + + [Fact] + public void EnrichWithRequest_SetsTools_WhenToolsProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var tools = new List + { + AIFunctionFactory.Create(() => "test", "TestTool", "A test tool") + }; + var options = new ChatOptions { Tools = tools }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, options); + + _mockSpan.Received(1).SetData("gen_ai.request.available_tools", Arg.Any()); + } + + [Fact] + public void EnrichWithRequest_SetsTemperature_WhenProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var options = new ChatOptions { Temperature = 0.7f }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, options); + + _mockSpan.Received(1).SetData("gen_ai.request.temperature", 0.7f); + } + + [Fact] + public void EnrichWithRequest_SetsMaxOutputTokens_WhenProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var options = new ChatOptions { MaxOutputTokens = 1000 }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, options); + + _mockSpan.Received(1).SetData("gen_ai.request.max_tokens", 1000); + } + + [Fact] + public void EnrichWithRequest_SetsTopP_WhenProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var options = new ChatOptions { TopP = 0.9f }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, options); + + _mockSpan.Received(1).SetData("gen_ai.request.top_p", 0.9f); + } + + [Fact] + public void EnrichWithRequest_SetsFrequencyPenalty_WhenProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var options = new ChatOptions { FrequencyPenalty = 0.5f }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, options); + + _mockSpan.Received(1).SetData("gen_ai.request.frequency_penalty", 0.5f); + } + + [Fact] + public void EnrichWithRequest_SetsPresencePenalty_WhenProvided() + { + var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; + var options = new ChatOptions { PresencePenalty = 0.3f }; + + SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, options); + + _mockSpan.Received(1).SetData("gen_ai.request.presence_penalty", 0.3f); + } + + [Fact] + public void EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() + { + var usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 20 }; + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello")) + { + Usage = usage + }; + + SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response); + + _mockSpan.Received(1).SetData("gen_ai.usage.input_tokens", 10L); + _mockSpan.Received(1).SetData("gen_ai.usage.output_tokens", 20L); + _mockSpan.Received(1).SetData("gen_ai.usage.total_tokens", 30L); + } + + [Fact] + public void EnrichWithResponse_DoesNotSetUsage_WhenUsageIsNull() + { + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello")); + + SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response); + + _mockSpan.DidNotReceive().SetData("gen_ai.usage.input_tokens", Arg.Any()); + _mockSpan.DidNotReceive().SetData("gen_ai.usage.output_tokens", Arg.Any()); + _mockSpan.DidNotReceive().SetData("gen_ai.usage.total_tokens", Arg.Any()); + } + + [Fact] + public void EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() + { + var usage = new UsageDetails { InputTokenCount = 10 }; + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello")) + { + Usage = usage + }; + + SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response); + + _mockSpan.Received().SetData("gen_ai.usage.input_tokens", 10L); + _mockSpan.DidNotReceive().SetData("gen_ai.usage.output_tokens", Arg.Any()); + _mockSpan.DidNotReceive().SetData("gen_ai.usage.total_tokens", Arg.Any()); + } + + [Fact] + public void EnrichWithResponse_SetsResponseText_WhenIncludeResponseContentIsTrue() + { + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello world")); + var aiOptions = new SentryAIOptions { IncludeAIResponseContent = true }; + + SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response, aiOptions); + + _mockSpan.Received(1).SetData("gen_ai.response.text", "Hello world"); + } + + [Fact] + public void EnrichWithResponse_DoesNotSetResponseText_WhenIncludeResponseContentIsFalse() + { + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello world")); + var aiOptions = new SentryAIOptions { IncludeAIResponseContent = false }; + + SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response, aiOptions); + + _mockSpan.DidNotReceive().SetData("gen_ai.response.text", Arg.Any()); + } + + [Fact] + public void EnrichWithResponse_SetsResponseText_WhenAIOptionsIsNull() + { + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello world")); + + SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response, null); + + _mockSpan.Received(1).SetData("gen_ai.response.text", "Hello world"); + } + + [Fact] + public void EnrichWithResponse_SetsModelId_WhenProvided() + { + var response = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello")) + { + ModelId = "gpt-4-turbo" + }; + + SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response); + + _mockSpan.Received(1).SetData("gen_ai.response.model_id", "gpt-4-turbo"); + } + + [Fact] + public void EnrichWithStreamingResponse_AccumulatesTokenUsage() + { + var messages = new List + { + new(ChatRole.Assistant, "Hello") + { + Contents = [new UsageContent(new UsageDetails { InputTokenCount = 5, OutputTokenCount = 10 })] + }, + new(ChatRole.Assistant, " world") + { + Contents = [new UsageContent(new UsageDetails { InputTokenCount = 3, OutputTokenCount = 5 })] + } + }; + + SentryAISpanEnricher.EnrichWithStreamingResponse(_mockSpan, messages); + + _mockSpan.Received(1).SetData("gen_ai.usage.input_tokens", 8L); + _mockSpan.Received(1).SetData("gen_ai.usage.output_tokens", 15L); + _mockSpan.Received(1).SetData("gen_ai.usage.total_tokens", 23L); + } + + [Fact] + public void EnrichWithStreamingResponse_ConcatenatesResponseText() + { + var messages = new List + { + new(ChatRole.Assistant, "Hello"), + new(ChatRole.Assistant, " world"), + new(ChatRole.Assistant, "!") + }; + + SentryAISpanEnricher.EnrichWithStreamingResponse(_mockSpan, messages); + + _mockSpan.Received(1).SetData("gen_ai.response.text", "Hello world!"); + } + + [Fact] + public void EnrichWithStreamingResponse_DoesNotSetResponseText_WhenIncludeResponseContentIsFalse() + { + var messages = new List + { + new(ChatRole.Assistant, "Hello world") + }; + var aiOptions = new SentryAIOptions { IncludeAIResponseContent = false }; + + SentryAISpanEnricher.EnrichWithStreamingResponse(_mockSpan, messages, aiOptions); + + _mockSpan.DidNotReceive().SetData("gen_ai.response.text", Arg.Any()); + } + + [Fact] + public void EnrichWithStreamingResponse_SetsModelId_FromLastMessageWithModelId() + { + var messages = new List + { + new(ChatRole.Assistant, "Hello") { ModelId = "gpt-3.5" }, + new(ChatRole.Assistant, " world") { ModelId = "gpt-4" }, + new(ChatRole.Assistant, "!") + }; + + SentryAISpanEnricher.EnrichWithStreamingResponse(_mockSpan, messages); + + _mockSpan.Received(1).SetData("gen_ai.response.model_id", "gpt-4"); + } +} diff --git a/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs b/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs index f9739e1f71..00d3992e09 100644 --- a/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs +++ b/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs @@ -15,8 +15,7 @@ public async Task CompleteAsync_CallsInnerClient() inner.GetResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(chatResponse)); - var hub = Substitute.For(); - var sentryChatClient = new SentryChatClient(inner, hub, agentName: "Agent", model: "gpt-4o-mini", system: "openai"); + var sentryChatClient = new SentryChatClient(inner); var res = await sentryChatClient.GetResponseAsync([new ChatMessage(ChatRole.User, "hi")], null); @@ -30,10 +29,9 @@ public async Task CompleteStreamingAsync_CallsInnerClient() var inner = Substitute.For(); inner.GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()) - .Returns(CreateTestStreamingUpdates()); + .Returns(CreateTestStreamingUpdatesAsync()); - var hub = Substitute.For(); - var client = new SentryChatClient(inner, hub, agentName: "Agent", model: "gpt-4o-mini", system: "openai"); + var client = new SentryChatClient(inner); var results = new List(); await foreach (var update in client.GetStreamingResponseAsync([new ChatMessage(ChatRole.User, "hi")], null)) @@ -48,10 +46,10 @@ public async Task CompleteStreamingAsync_CallsInnerClient() inner.Received(1).GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()); } - private static async IAsyncEnumerable CreateTestStreamingUpdates() + private static async IAsyncEnumerable CreateTestStreamingUpdatesAsync() { yield return new ChatResponseUpdate(ChatRole.System, "Hello"); - await Task.Yield(); // Make it actually async + await Task.Yield(); // Make it async yield return new ChatResponseUpdate(ChatRole.System, " World!"); } } diff --git a/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs b/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs new file mode 100644 index 0000000000..4a13ae10a7 --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs @@ -0,0 +1,221 @@ +#nullable enable +using Microsoft.Extensions.AI; +using NSubstitute; +using Sentry.Extensions.AI; +using Sentry.Testing; + +namespace Sentry.Extensions.AI.Tests; + +public class SentryInstrumentedFunctionTests +{ + [Fact] + public async Task InvokeCoreAsync_WithValidFunction_ReturnsResult() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var testFunction = AIFunctionFactory.Create(() => "test result", "TestFunction", "Test function description"); + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments(); + + // Act + var result = await sentryFunction.InvokeAsync(arguments); + + // Assert + // AIFunctionFactory returns JsonElement, so we need to check the actual content + Assert.NotNull(result); + if (result is JsonElement jsonElement) + { + Assert.Equal("test result", jsonElement.GetString()); + } + else + { + Assert.Equal("test result", result); + } + Assert.Equal("TestFunction", sentryFunction.Name); + Assert.Equal("Test function description", sentryFunction.Description); + } + + [Fact] + public async Task InvokeCoreAsync_WithNullResult_ReturnsNull() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var testFunction = AIFunctionFactory.Create(object? () => null, "TestFunction", "Test function description"); + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments(); + + // Act + var result = await sentryFunction.InvokeAsync(arguments); + + // Assert + // AIFunctionFactory may return JsonElement with ValueKind.Null instead of actual null + if (result is JsonElement jsonElement) + { + Assert.Equal(JsonValueKind.Null, jsonElement.ValueKind); + } + else + { + Assert.Null(result); + } + } + + [Fact] + public async Task InvokeCoreAsync_WithJsonNullResult_ReturnsJsonElement() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var jsonNullElement = JsonSerializer.Deserialize("null"); + var testFunction = AIFunctionFactory.Create(() => jsonNullElement, "TestFunction", "Test function description"); + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments(); + + // Act + var result = await sentryFunction.InvokeAsync(arguments); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + var jsonResult = (JsonElement)result; + Assert.Equal(JsonValueKind.Null, jsonResult.ValueKind); + } + + [Fact] + public async Task InvokeCoreAsync_WithJsonElementResult_CallsToStringForSpanOutput() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var jsonElement = JsonSerializer.Deserialize("\"test output\""); + var testFunction = AIFunctionFactory.Create(() => jsonElement, "TestFunction", "Test function description"); + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments(); + + // Act + var result = await sentryFunction.InvokeAsync(arguments); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + var jsonResult = (JsonElement)result; + Assert.Equal("test output", jsonResult.GetString()); + + // The span should have recorded the ToString() output of the JsonElement + // (This is testing the internal behavior that ToString() gets called for span data) + } + + [Fact] + public async Task InvokeCoreAsync_WithComplexResult_ReturnsObject() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var resultObject = new { message = "test", count = 42 }; + var testFunction = AIFunctionFactory.Create(() => resultObject, "TestFunction", "Test function description"); + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments(); + + // Act + var result = await sentryFunction.InvokeAsync(arguments); + + // Assert + Assert.NotNull(result); + if (result is JsonElement jsonElement) + { + // When AIFunction serializes objects, they become JsonElements + var message = jsonElement.GetProperty("message").GetString(); + var count = jsonElement.GetProperty("count").GetInt32(); + Assert.Equal("test", message); + Assert.Equal(42, count); + } + else + { + Assert.Equal(resultObject, result); + } + } + + [Fact] + public async Task InvokeCoreAsync_WhenFunctionThrows_PropagatesException() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var expectedException = new InvalidOperationException("Test exception"); + var testFunction = AIFunctionFactory.Create(new Func(() => throw expectedException), "TestFunction", "Test function description"); + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments(); + + // Act & Assert + var actualException = await Assert.ThrowsAsync(async () => + await sentryFunction.InvokeAsync(arguments)); + + Assert.Equal(expectedException.Message, actualException.Message); + } + + [Fact] + public async Task InvokeCoreAsync_WithCancellation_PropagatesCancellation() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var testFunction = AIFunctionFactory.Create((CancellationToken cancellationToken) => + { + cancellationToken.ThrowIfCancellationRequested(); + return "result"; + }, "TestFunction", "Test function description"); + + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync(async () => + await sentryFunction.InvokeAsync(arguments, cts.Token)); + } + + [Fact] + public async Task InvokeCoreAsync_WithParameters_PassesParametersCorrectly() + { + // Arrange + using var sentryDisposable = SentryHelpers.InitializeSdk(); + var receivedArguments = (AIFunctionArguments?)null; + var testFunction = AIFunctionFactory.Create((AIFunctionArguments args) => + { + receivedArguments = args; + return "result"; + }, "TestFunction", "Test function description"); + + var sentryFunction = new SentryInstrumentedFunction(testFunction); + var arguments = new AIFunctionArguments { ["param1"] = "value1" }; + + // Act + await sentryFunction.InvokeAsync(arguments); + + // Assert + Assert.NotNull(receivedArguments); + Assert.Equal("value1", receivedArguments["param1"]); + } + + [Fact] + public void Constructor_PreservesInnerFunctionProperties() + { + // Arrange + var testFunction = AIFunctionFactory.Create(() => "test", "TestFunction", "Test function description"); + + // Act + var sentryFunction = new SentryInstrumentedFunction(testFunction); + + // Assert + Assert.Equal("TestFunction", sentryFunction.Name); + Assert.Equal("Test function description", sentryFunction.Description); + } +} + +internal static class SentryHelpers +{ + public static IDisposable InitializeSdk() + { + return SentrySdk.Init(options => + { + options.Dsn = "https://3f3a29aa3a3aff@fake-sentry.io:65535/2147483647"; + options.TracesSampleRate = 1.0; + options.Debug = false; + }); + } +} diff --git a/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx b/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx new file mode 100644 index 0000000000..eaf4012297 --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx @@ -0,0 +1,863 @@ + + + + + + + + + + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 113 +--- End of stack trace from previous location --- + + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 45 +--- End of stack trace from previous location --- + + + + + + + + + + NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: + SetData("gen_ai.usage.input_tokens", 10) +Actually received no matching calls. +Received 4 non-matching calls (non-matching arguments indicated with '*' characters): + SetData("gen_ai.usage.input_tokens", *10*) + SetData(*"gen_ai.usage.output_tokens"*, *20*) + SetData(*"gen_ai.usage.total_tokens"*, *30*) + SetData(*"gen_ai.response.text"*, *"Hello"*) + + at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) + at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) + at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 175 + at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) + at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 135 +--- End of stack trace from previous location --- + + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 19 +--- End of stack trace from previous location --- + + + + + + + + + + + + + + + + + + NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: + SetData("gen_ai.usage.input_tokens", 10) +Actually received no matching calls. +Received 2 non-matching calls (non-matching arguments indicated with '*' characters): + SetData("gen_ai.usage.input_tokens", *10*) + SetData(*"gen_ai.response.text"*, *"Hello"*) + + at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) + at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) + at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 203 + at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) + at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 160 +--- End of stack trace from previous location --- + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 68 +--- End of stack trace from previous location --- + + + + + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 90 +--- End of stack trace from previous location --- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.1+bf6400fd51 (64-bit .NET 9.0.8) +[xUnit.net 00:00:00.05] Discovering: Sentry.Extensions.AI.Tests +[xUnit.net 00:00:00.07] Discovered: Sentry.Extensions.AI.Tests +[xUnit.net 00:00:00.08] Starting: Sentry.Extensions.AI.Tests +[xUnit.net 00:00:00.19] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.19] Method signature: +[xUnit.net 00:00:00.19] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.19] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.19] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.19] All queued specifications: +[xUnit.net 00:00:00.19] any AIFunctionArguments +[xUnit.net 00:00:00.19] any CancellationToken +[xUnit.net 00:00:00.19] Matched argument specifications: +[xUnit.net 00:00:00.19] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.19] +[xUnit.net 00:00:00.19] Stack Trace: +[xUnit.net 00:00:00.19] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.19] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.19] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.19] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.19] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.19] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.19] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.19] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.19] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.19] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.19] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.19] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(68,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() +[xUnit.net 00:00:00.19] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.20] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.20] Method signature: +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.20] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.20] All queued specifications: +[xUnit.net 00:00:00.20] any AIFunctionArguments +[xUnit.net 00:00:00.20] any CancellationToken +[xUnit.net 00:00:00.20] Matched argument specifications: +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.20] +[xUnit.net 00:00:00.20] Stack Trace: +[xUnit.net 00:00:00.20] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.20] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.20] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.20] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.20] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.20] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(135,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() +[xUnit.net 00:00:00.20] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.20] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.20] Method signature: +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.20] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.20] All queued specifications: +[xUnit.net 00:00:00.20] any AIFunctionArguments +[xUnit.net 00:00:00.20] any CancellationToken +[xUnit.net 00:00:00.20] Matched argument specifications: +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.20] +[xUnit.net 00:00:00.20] Stack Trace: +[xUnit.net 00:00:00.20] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.20] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.20] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.20] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.20] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.20] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(19,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() +[xUnit.net 00:00:00.20] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.20] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.20] Method signature: +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.20] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.20] All queued specifications: +[xUnit.net 00:00:00.20] any AIFunctionArguments +[xUnit.net 00:00:00.20] any CancellationToken +[xUnit.net 00:00:00.20] Matched argument specifications: +[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.20] +[xUnit.net 00:00:00.20] Stack Trace: +[xUnit.net 00:00:00.20] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.20] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.20] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.20] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.20] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.20] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(45,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() +[xUnit.net 00:00:00.20] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.21] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.21] Method signature: +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.21] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.21] All queued specifications: +[xUnit.net 00:00:00.21] any AIFunctionArguments +[xUnit.net 00:00:00.21] any CancellationToken +[xUnit.net 00:00:00.21] Matched argument specifications: +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.21] +[xUnit.net 00:00:00.21] Stack Trace: +[xUnit.net 00:00:00.21] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.21] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.21] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(113,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() +[xUnit.net 00:00:00.21] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.21] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.21] Method signature: +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.21] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.21] All queued specifications: +[xUnit.net 00:00:00.21] any AIFunctionArguments +[xUnit.net 00:00:00.21] any CancellationToken +[xUnit.net 00:00:00.21] Matched argument specifications: +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.21] +[xUnit.net 00:00:00.21] Stack Trace: +[xUnit.net 00:00:00.21] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.21] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.21] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(90,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() +[xUnit.net 00:00:00.21] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.21] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.21] Method signature: +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.21] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.21] All queued specifications: +[xUnit.net 00:00:00.21] any AIFunctionArguments +[xUnit.net 00:00:00.21] any CancellationToken +[xUnit.net 00:00:00.21] Matched argument specifications: +[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.21] +[xUnit.net 00:00:00.21] Stack Trace: +[xUnit.net 00:00:00.21] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.21] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.21] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(160,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() +[xUnit.net 00:00:00.21] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.27] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: +[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", 10) +[xUnit.net 00:00:00.27] Actually received no matching calls. +[xUnit.net 00:00:00.27] Received 4 non-matching calls (non-matching arguments indicated with '*' characters): +[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", *10*) +[xUnit.net 00:00:00.27] SetData(*"gen_ai.usage.output_tokens"*, *20*) +[xUnit.net 00:00:00.27] SetData(*"gen_ai.usage.total_tokens"*, *30*) +[xUnit.net 00:00:00.27] SetData(*"gen_ai.response.text"*, *"Hello"*) +[xUnit.net 00:00:00.27] +[xUnit.net 00:00:00.27] Stack Trace: +[xUnit.net 00:00:00.27] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) +[xUnit.net 00:00:00.27] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) +[xUnit.net 00:00:00.27] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.27] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.27] at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) +[xUnit.net 00:00:00.27] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(175,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() +[xUnit.net 00:00:00.27] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) +[xUnit.net 00:00:00.27] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) +[xUnit.net 00:00:00.27] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: +[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", 10) +[xUnit.net 00:00:00.27] Actually received no matching calls. +[xUnit.net 00:00:00.27] Received 2 non-matching calls (non-matching arguments indicated with '*' characters): +[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", *10*) +[xUnit.net 00:00:00.27] SetData(*"gen_ai.response.text"*, *"Hello"*) +[xUnit.net 00:00:00.27] +[xUnit.net 00:00:00.27] Stack Trace: +[xUnit.net 00:00:00.27] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) +[xUnit.net 00:00:00.27] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) +[xUnit.net 00:00:00.27] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.27] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.27] at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) +[xUnit.net 00:00:00.27] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(203,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() +[xUnit.net 00:00:00.27] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) +[xUnit.net 00:00:00.27] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) +[xUnit.net 00:00:00.30] Finished: Sentry.Extensions.AI.Tests + + + + + [xUnit.net 00:00:00.19] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException [FAIL] + + + [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName [FAIL] + + + [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan [FAIL] + + + [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction [FAIL] + + + [xUnit.net 00:00:00.21] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString [FAIL] + + + [xUnit.net 00:00:00.21] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput [FAIL] + + + [xUnit.net 00:00:00.21] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation [FAIL] + + + [xUnit.net 00:00:00.27] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided [FAIL] + + + [xUnit.net 00:00:00.27] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided [FAIL] + + + + \ No newline at end of file diff --git a/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx b/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx new file mode 100644 index 0000000000..b92273a839 --- /dev/null +++ b/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx @@ -0,0 +1,863 @@ + + + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 135 +--- End of stack trace from previous location --- + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 45 +--- End of stack trace from previous location --- + + + + + + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 90 +--- End of stack trace from previous location --- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 68 +--- End of stack trace from previous location --- + + + + + + + + + + + + NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: + SetData("gen_ai.usage.input_tokens", 10) +Actually received no matching calls. +Received 4 non-matching calls (non-matching arguments indicated with '*' characters): + SetData("gen_ai.usage.input_tokens", *10*) + SetData(*"gen_ai.usage.output_tokens"*, *20*) + SetData(*"gen_ai.usage.total_tokens"*, *30*) + SetData(*"gen_ai.response.text"*, *"Hello"*) + + at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) + at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.ObjectProxy.SetData(String key, Object value) + at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 175 + at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) + at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 19 +--- End of stack trace from previous location --- + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 113 +--- End of stack trace from previous location --- + + + + + + + + NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +Method signature: + InvokeCoreAsync(AIFunctionArguments, CancellationToken) +Method arguments (possible arg matchers are indicated with '*'): + InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +All queued specifications: + any AIFunctionArguments + any CancellationToken +Matched argument specifications: + InvokeCoreAsync(AIFunctionArguments, ???) + + at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) + at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) + at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 160 +--- End of stack trace from previous location --- + + + + + + + + + + NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: + SetData("gen_ai.usage.input_tokens", 10) +Actually received no matching calls. +Received 2 non-matching calls (non-matching arguments indicated with '*' characters): + SetData("gen_ai.usage.input_tokens", *10*) + SetData(*"gen_ai.response.text"*, *"Hello"*) + + at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) + at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) + at NSubstitute.Routing.Route.Handle(ICall call) + at NSubstitute.Core.CallRouter.Route(ICall call) + at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) + at Castle.DynamicProxy.AbstractInvocation.Proceed() + at Castle.Proxies.ObjectProxy.SetData(String key, Object value) + at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 203 + at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) + at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.1+bf6400fd51 (64-bit .NET 8.0.19) +[xUnit.net 00:00:00.06] Discovering: Sentry.Extensions.AI.Tests +[xUnit.net 00:00:00.08] Discovered: Sentry.Extensions.AI.Tests +[xUnit.net 00:00:00.09] Starting: Sentry.Extensions.AI.Tests +[xUnit.net 00:00:00.21] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: +[xUnit.net 00:00:00.21] SetData("gen_ai.usage.input_tokens", 10) +[xUnit.net 00:00:00.21] Actually received no matching calls. +[xUnit.net 00:00:00.21] Received 4 non-matching calls (non-matching arguments indicated with '*' characters): +[xUnit.net 00:00:00.21] SetData("gen_ai.usage.input_tokens", *10*) +[xUnit.net 00:00:00.21] SetData(*"gen_ai.usage.output_tokens"*, *20*) +[xUnit.net 00:00:00.21] SetData(*"gen_ai.usage.total_tokens"*, *30*) +[xUnit.net 00:00:00.21] SetData(*"gen_ai.response.text"*, *"Hello"*) +[xUnit.net 00:00:00.21] +[xUnit.net 00:00:00.21] Stack Trace: +[xUnit.net 00:00:00.21] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.21] at Castle.Proxies.ObjectProxy.SetData(String key, Object value) +[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(175,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() +[xUnit.net 00:00:00.21] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) +[xUnit.net 00:00:00.21] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) +[xUnit.net 00:00:00.22] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: +[xUnit.net 00:00:00.22] SetData("gen_ai.usage.input_tokens", 10) +[xUnit.net 00:00:00.22] Actually received no matching calls. +[xUnit.net 00:00:00.22] Received 2 non-matching calls (non-matching arguments indicated with '*' characters): +[xUnit.net 00:00:00.22] SetData("gen_ai.usage.input_tokens", *10*) +[xUnit.net 00:00:00.22] SetData(*"gen_ai.response.text"*, *"Hello"*) +[xUnit.net 00:00:00.22] +[xUnit.net 00:00:00.22] Stack Trace: +[xUnit.net 00:00:00.22] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) +[xUnit.net 00:00:00.22] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) +[xUnit.net 00:00:00.22] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.22] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.22] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.22] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.22] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.22] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.22] at Castle.Proxies.ObjectProxy.SetData(String key, Object value) +[xUnit.net 00:00:00.22] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(203,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() +[xUnit.net 00:00:00.22] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) +[xUnit.net 00:00:00.22] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) +[xUnit.net 00:00:00.29] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.29] Method signature: +[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.29] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.29] All queued specifications: +[xUnit.net 00:00:00.29] any AIFunctionArguments +[xUnit.net 00:00:00.29] any CancellationToken +[xUnit.net 00:00:00.29] Matched argument specifications: +[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.29] +[xUnit.net 00:00:00.29] Stack Trace: +[xUnit.net 00:00:00.29] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.29] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.29] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.29] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.29] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.29] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.29] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.29] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(68,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() +[xUnit.net 00:00:00.29] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.29] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.29] Method signature: +[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.29] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.29] All queued specifications: +[xUnit.net 00:00:00.29] any AIFunctionArguments +[xUnit.net 00:00:00.29] any CancellationToken +[xUnit.net 00:00:00.29] Matched argument specifications: +[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.29] +[xUnit.net 00:00:00.29] Stack Trace: +[xUnit.net 00:00:00.29] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.29] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.29] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.29] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.29] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.29] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.29] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.29] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(135,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() +[xUnit.net 00:00:00.29] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.30] Method signature: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.30] All queued specifications: +[xUnit.net 00:00:00.30] any AIFunctionArguments +[xUnit.net 00:00:00.30] any CancellationToken +[xUnit.net 00:00:00.30] Matched argument specifications: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.30] +[xUnit.net 00:00:00.30] Stack Trace: +[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(19,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() +[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.30] Method signature: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.30] All queued specifications: +[xUnit.net 00:00:00.30] any AIFunctionArguments +[xUnit.net 00:00:00.30] any CancellationToken +[xUnit.net 00:00:00.30] Matched argument specifications: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.30] +[xUnit.net 00:00:00.30] Stack Trace: +[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(45,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() +[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.30] Method signature: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.30] All queued specifications: +[xUnit.net 00:00:00.30] any AIFunctionArguments +[xUnit.net 00:00:00.30] any CancellationToken +[xUnit.net 00:00:00.30] Matched argument specifications: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.30] +[xUnit.net 00:00:00.30] Stack Trace: +[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(113,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() +[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.30] Method signature: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.30] All queued specifications: +[xUnit.net 00:00:00.30] any AIFunctionArguments +[xUnit.net 00:00:00.30] any CancellationToken +[xUnit.net 00:00:00.30] Matched argument specifications: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.30] +[xUnit.net 00:00:00.30] Stack Trace: +[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(90,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() +[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. +[xUnit.net 00:00:00.30] Method signature: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) +[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) +[xUnit.net 00:00:00.30] All queued specifications: +[xUnit.net 00:00:00.30] any AIFunctionArguments +[xUnit.net 00:00:00.30] any CancellationToken +[xUnit.net 00:00:00.30] Matched argument specifications: +[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) +[xUnit.net 00:00:00.30] +[xUnit.net 00:00:00.30] Stack Trace: +[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) +[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() +[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) +[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(160,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() +[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- +[xUnit.net 00:00:00.33] Finished: Sentry.Extensions.AI.Tests + + + + + [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided [FAIL] + + + [xUnit.net 00:00:00.22] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided [FAIL] + + + [xUnit.net 00:00:00.29] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException [FAIL] + + + [xUnit.net 00:00:00.29] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName [FAIL] + + + [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan [FAIL] + + + [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction [FAIL] + + + [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString [FAIL] + + + [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput [FAIL] + + + [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation [FAIL] + + + + \ No newline at end of file From 3185b9d1d3607203c18c4ee76078dc116dcd3fb5 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Mon, 20 Oct 2025 01:28:48 -0400 Subject: [PATCH 04/22] remove nullable --- samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs index 0a0b0ca997..75a117da81 100644 --- a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs @@ -1,4 +1,3 @@ -#nullable enable using Anthropic.SDK; using Anthropic.SDK.Constants; using Microsoft.Extensions.AI; From 27251acf5920b3ec7e69276cdfb84fd57655ddf2 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Mon, 20 Oct 2025 01:29:59 -0400 Subject: [PATCH 05/22] remove test results --- .../_CVDTQ2MGH4_2025-10-20_00_35_51.trx | 863 ------------------ .../_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx | 863 ------------------ 2 files changed, 1726 deletions(-) delete mode 100644 test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx delete mode 100644 test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx diff --git a/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx b/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx deleted file mode 100644 index eaf4012297..0000000000 --- a/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51.trx +++ /dev/null @@ -1,863 +0,0 @@ - - - - - - - - - - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 113 ---- End of stack trace from previous location --- - - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 45 ---- End of stack trace from previous location --- - - - - - - - - - - NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: - SetData("gen_ai.usage.input_tokens", 10) -Actually received no matching calls. -Received 4 non-matching calls (non-matching arguments indicated with '*' characters): - SetData("gen_ai.usage.input_tokens", *10*) - SetData(*"gen_ai.usage.output_tokens"*, *20*) - SetData(*"gen_ai.usage.total_tokens"*, *30*) - SetData(*"gen_ai.response.text"*, *"Hello"*) - - at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) - at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) - at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 175 - at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) - at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 135 ---- End of stack trace from previous location --- - - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 19 ---- End of stack trace from previous location --- - - - - - - - - - - - - - - - - - - NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: - SetData("gen_ai.usage.input_tokens", 10) -Actually received no matching calls. -Received 2 non-matching calls (non-matching arguments indicated with '*' characters): - SetData("gen_ai.usage.input_tokens", *10*) - SetData(*"gen_ai.response.text"*, *"Hello"*) - - at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) - at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) - at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 203 - at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) - at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 160 ---- End of stack trace from previous location --- - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 68 ---- End of stack trace from previous location --- - - - - - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 90 ---- End of stack trace from previous location --- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.1+bf6400fd51 (64-bit .NET 9.0.8) -[xUnit.net 00:00:00.05] Discovering: Sentry.Extensions.AI.Tests -[xUnit.net 00:00:00.07] Discovered: Sentry.Extensions.AI.Tests -[xUnit.net 00:00:00.08] Starting: Sentry.Extensions.AI.Tests -[xUnit.net 00:00:00.19] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.19] Method signature: -[xUnit.net 00:00:00.19] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.19] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.19] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.19] All queued specifications: -[xUnit.net 00:00:00.19] any AIFunctionArguments -[xUnit.net 00:00:00.19] any CancellationToken -[xUnit.net 00:00:00.19] Matched argument specifications: -[xUnit.net 00:00:00.19] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.19] -[xUnit.net 00:00:00.19] Stack Trace: -[xUnit.net 00:00:00.19] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.19] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.19] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.19] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.19] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.19] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.19] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.19] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.19] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.19] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.19] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.19] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(68,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() -[xUnit.net 00:00:00.19] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.20] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.20] Method signature: -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.20] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.20] All queued specifications: -[xUnit.net 00:00:00.20] any AIFunctionArguments -[xUnit.net 00:00:00.20] any CancellationToken -[xUnit.net 00:00:00.20] Matched argument specifications: -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.20] -[xUnit.net 00:00:00.20] Stack Trace: -[xUnit.net 00:00:00.20] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.20] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.20] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.20] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.20] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.20] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(135,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() -[xUnit.net 00:00:00.20] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.20] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.20] Method signature: -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.20] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.20] All queued specifications: -[xUnit.net 00:00:00.20] any AIFunctionArguments -[xUnit.net 00:00:00.20] any CancellationToken -[xUnit.net 00:00:00.20] Matched argument specifications: -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.20] -[xUnit.net 00:00:00.20] Stack Trace: -[xUnit.net 00:00:00.20] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.20] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.20] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.20] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.20] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.20] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(19,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() -[xUnit.net 00:00:00.20] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.20] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.20] Method signature: -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.20] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.20] All queued specifications: -[xUnit.net 00:00:00.20] any AIFunctionArguments -[xUnit.net 00:00:00.20] any CancellationToken -[xUnit.net 00:00:00.20] Matched argument specifications: -[xUnit.net 00:00:00.20] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.20] -[xUnit.net 00:00:00.20] Stack Trace: -[xUnit.net 00:00:00.20] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.20] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.20] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.20] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.20] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.20] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.20] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.20] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(45,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() -[xUnit.net 00:00:00.20] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.21] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.21] Method signature: -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.21] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.21] All queued specifications: -[xUnit.net 00:00:00.21] any AIFunctionArguments -[xUnit.net 00:00:00.21] any CancellationToken -[xUnit.net 00:00:00.21] Matched argument specifications: -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.21] -[xUnit.net 00:00:00.21] Stack Trace: -[xUnit.net 00:00:00.21] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.21] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.21] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(113,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() -[xUnit.net 00:00:00.21] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.21] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.21] Method signature: -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.21] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.21] All queued specifications: -[xUnit.net 00:00:00.21] any AIFunctionArguments -[xUnit.net 00:00:00.21] any CancellationToken -[xUnit.net 00:00:00.21] Matched argument specifications: -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.21] -[xUnit.net 00:00:00.21] Stack Trace: -[xUnit.net 00:00:00.21] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.21] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.21] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(90,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() -[xUnit.net 00:00:00.21] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.21] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.21] Method signature: -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.21] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.21] All queued specifications: -[xUnit.net 00:00:00.21] any AIFunctionArguments -[xUnit.net 00:00:00.21] any CancellationToken -[xUnit.net 00:00:00.21] Matched argument specifications: -[xUnit.net 00:00:00.21] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.21] -[xUnit.net 00:00:00.21] Stack Trace: -[xUnit.net 00:00:00.21] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.21] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.21] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(160,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() -[xUnit.net 00:00:00.21] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.27] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: -[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", 10) -[xUnit.net 00:00:00.27] Actually received no matching calls. -[xUnit.net 00:00:00.27] Received 4 non-matching calls (non-matching arguments indicated with '*' characters): -[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", *10*) -[xUnit.net 00:00:00.27] SetData(*"gen_ai.usage.output_tokens"*, *20*) -[xUnit.net 00:00:00.27] SetData(*"gen_ai.usage.total_tokens"*, *30*) -[xUnit.net 00:00:00.27] SetData(*"gen_ai.response.text"*, *"Hello"*) -[xUnit.net 00:00:00.27] -[xUnit.net 00:00:00.27] Stack Trace: -[xUnit.net 00:00:00.27] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) -[xUnit.net 00:00:00.27] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) -[xUnit.net 00:00:00.27] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.27] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.27] at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) -[xUnit.net 00:00:00.27] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(175,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() -[xUnit.net 00:00:00.27] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) -[xUnit.net 00:00:00.27] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) -[xUnit.net 00:00:00.27] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: -[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", 10) -[xUnit.net 00:00:00.27] Actually received no matching calls. -[xUnit.net 00:00:00.27] Received 2 non-matching calls (non-matching arguments indicated with '*' characters): -[xUnit.net 00:00:00.27] SetData("gen_ai.usage.input_tokens", *10*) -[xUnit.net 00:00:00.27] SetData(*"gen_ai.response.text"*, *"Hello"*) -[xUnit.net 00:00:00.27] -[xUnit.net 00:00:00.27] Stack Trace: -[xUnit.net 00:00:00.27] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) -[xUnit.net 00:00:00.27] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) -[xUnit.net 00:00:00.27] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.27] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.27] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.27] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.27] at Castle.Proxies.ObjectProxy_2.SetData(String key, Object value) -[xUnit.net 00:00:00.27] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(203,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() -[xUnit.net 00:00:00.27] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) -[xUnit.net 00:00:00.27] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) -[xUnit.net 00:00:00.30] Finished: Sentry.Extensions.AI.Tests - - - - - [xUnit.net 00:00:00.19] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException [FAIL] - - - [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName [FAIL] - - - [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan [FAIL] - - - [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction [FAIL] - - - [xUnit.net 00:00:00.21] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString [FAIL] - - - [xUnit.net 00:00:00.21] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput [FAIL] - - - [xUnit.net 00:00:00.21] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation [FAIL] - - - [xUnit.net 00:00:00.27] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided [FAIL] - - - [xUnit.net 00:00:00.27] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided [FAIL] - - - - \ No newline at end of file diff --git a/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx b/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx deleted file mode 100644 index b92273a839..0000000000 --- a/test/Sentry.Extensions.AI.Tests/TestResults/_CVDTQ2MGH4_2025-10-20_00_35_51[1].trx +++ /dev/null @@ -1,863 +0,0 @@ - - - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 135 ---- End of stack trace from previous location --- - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 45 ---- End of stack trace from previous location --- - - - - - - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 90 ---- End of stack trace from previous location --- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 68 ---- End of stack trace from previous location --- - - - - - - - - - - - - NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: - SetData("gen_ai.usage.input_tokens", 10) -Actually received no matching calls. -Received 4 non-matching calls (non-matching arguments indicated with '*' characters): - SetData("gen_ai.usage.input_tokens", *10*) - SetData(*"gen_ai.usage.output_tokens"*, *20*) - SetData(*"gen_ai.usage.total_tokens"*, *30*) - SetData(*"gen_ai.response.text"*, *"Hello"*) - - at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) - at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.ObjectProxy.SetData(String key, Object value) - at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 175 - at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) - at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 19 ---- End of stack trace from previous location --- - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 113 ---- End of stack trace from previous location --- - - - - - - - - NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -Method signature: - InvokeCoreAsync(AIFunctionArguments, CancellationToken) -Method arguments (possible arg matchers are indicated with '*'): - InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -All queued specifications: - any AIFunctionArguments - any CancellationToken -Matched argument specifications: - InvokeCoreAsync(AIFunctionArguments, ???) - - at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) - at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) - at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) - at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs:line 160 ---- End of stack trace from previous location --- - - - - - - - - - - NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: - SetData("gen_ai.usage.input_tokens", 10) -Actually received no matching calls. -Received 2 non-matching calls (non-matching arguments indicated with '*' characters): - SetData("gen_ai.usage.input_tokens", *10*) - SetData(*"gen_ai.response.text"*, *"Hello"*) - - at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) - at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) - at NSubstitute.Routing.Route.Handle(ICall call) - at NSubstitute.Core.CallRouter.Route(ICall call) - at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) - at Castle.DynamicProxy.AbstractInvocation.Proceed() - at Castle.Proxies.ObjectProxy.SetData(String key, Object value) - at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() in /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs:line 203 - at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) - at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.1+bf6400fd51 (64-bit .NET 8.0.19) -[xUnit.net 00:00:00.06] Discovering: Sentry.Extensions.AI.Tests -[xUnit.net 00:00:00.08] Discovered: Sentry.Extensions.AI.Tests -[xUnit.net 00:00:00.09] Starting: Sentry.Extensions.AI.Tests -[xUnit.net 00:00:00.21] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: -[xUnit.net 00:00:00.21] SetData("gen_ai.usage.input_tokens", 10) -[xUnit.net 00:00:00.21] Actually received no matching calls. -[xUnit.net 00:00:00.21] Received 4 non-matching calls (non-matching arguments indicated with '*' characters): -[xUnit.net 00:00:00.21] SetData("gen_ai.usage.input_tokens", *10*) -[xUnit.net 00:00:00.21] SetData(*"gen_ai.usage.output_tokens"*, *20*) -[xUnit.net 00:00:00.21] SetData(*"gen_ai.usage.total_tokens"*, *30*) -[xUnit.net 00:00:00.21] SetData(*"gen_ai.response.text"*, *"Hello"*) -[xUnit.net 00:00:00.21] -[xUnit.net 00:00:00.21] Stack Trace: -[xUnit.net 00:00:00.21] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.21] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.21] at Castle.Proxies.ObjectProxy.SetData(String key, Object value) -[xUnit.net 00:00:00.21] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(175,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided() -[xUnit.net 00:00:00.21] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) -[xUnit.net 00:00:00.21] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) -[xUnit.net 00:00:00.22] NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 1 call matching: -[xUnit.net 00:00:00.22] SetData("gen_ai.usage.input_tokens", 10) -[xUnit.net 00:00:00.22] Actually received no matching calls. -[xUnit.net 00:00:00.22] Received 2 non-matching calls (non-matching arguments indicated with '*' characters): -[xUnit.net 00:00:00.22] SetData("gen_ai.usage.input_tokens", *10*) -[xUnit.net 00:00:00.22] SetData(*"gen_ai.response.text"*, *"Hello"*) -[xUnit.net 00:00:00.22] -[xUnit.net 00:00:00.22] Stack Trace: -[xUnit.net 00:00:00.22] at NSubstitute.Core.ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) -[xUnit.net 00:00:00.22] at NSubstitute.Routing.Handlers.CheckReceivedCallsHandler.Handle(ICall call) -[xUnit.net 00:00:00.22] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.22] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.22] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.22] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.22] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.22] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.22] at Castle.Proxies.ObjectProxy.SetData(String key, Object value) -[xUnit.net 00:00:00.22] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs(203,0): at Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided() -[xUnit.net 00:00:00.22] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) -[xUnit.net 00:00:00.22] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) -[xUnit.net 00:00:00.29] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.29] Method signature: -[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.29] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.29] All queued specifications: -[xUnit.net 00:00:00.29] any AIFunctionArguments -[xUnit.net 00:00:00.29] any CancellationToken -[xUnit.net 00:00:00.29] Matched argument specifications: -[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.29] -[xUnit.net 00:00:00.29] Stack Trace: -[xUnit.net 00:00:00.29] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.29] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.29] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.29] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.29] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.29] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.29] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.29] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(68,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException() -[xUnit.net 00:00:00.29] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.29] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.29] Method signature: -[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.29] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.29] All queued specifications: -[xUnit.net 00:00:00.29] any AIFunctionArguments -[xUnit.net 00:00:00.29] any CancellationToken -[xUnit.net 00:00:00.29] Matched argument specifications: -[xUnit.net 00:00:00.29] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.29] -[xUnit.net 00:00:00.29] Stack Trace: -[xUnit.net 00:00:00.29] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.29] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.29] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.29] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.29] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.29] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.29] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.29] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.29] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.29] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(135,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName() -[xUnit.net 00:00:00.29] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.30] Method signature: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.30] All queued specifications: -[xUnit.net 00:00:00.30] any AIFunctionArguments -[xUnit.net 00:00:00.30] any CancellationToken -[xUnit.net 00:00:00.30] Matched argument specifications: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.30] -[xUnit.net 00:00:00.30] Stack Trace: -[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(19,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan() -[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.30] Method signature: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.30] All queued specifications: -[xUnit.net 00:00:00.30] any AIFunctionArguments -[xUnit.net 00:00:00.30] any CancellationToken -[xUnit.net 00:00:00.30] Matched argument specifications: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.30] -[xUnit.net 00:00:00.30] Stack Trace: -[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(45,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction() -[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.30] Method signature: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.30] All queued specifications: -[xUnit.net 00:00:00.30] any AIFunctionArguments -[xUnit.net 00:00:00.30] any CancellationToken -[xUnit.net 00:00:00.30] Matched argument specifications: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.30] -[xUnit.net 00:00:00.30] Stack Trace: -[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(113,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString() -[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.30] Method signature: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.30] All queued specifications: -[xUnit.net 00:00:00.30] any AIFunctionArguments -[xUnit.net 00:00:00.30] any CancellationToken -[xUnit.net 00:00:00.30] Matched argument specifications: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.30] -[xUnit.net 00:00:00.30] Stack Trace: -[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(90,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput() -[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.30] NSubstitute.Exceptions.AmbiguousArgumentsException : Cannot determine argument specifications to use. Please use specifications for all arguments of the same type. -[xUnit.net 00:00:00.30] Method signature: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, CancellationToken) -[xUnit.net 00:00:00.30] Method arguments (possible arg matchers are indicated with '*'): -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, *System.Threading.CancellationToken*) -[xUnit.net 00:00:00.30] All queued specifications: -[xUnit.net 00:00:00.30] any AIFunctionArguments -[xUnit.net 00:00:00.30] any CancellationToken -[xUnit.net 00:00:00.30] Matched argument specifications: -[xUnit.net 00:00:00.30] InvokeCoreAsync(AIFunctionArguments, ???) -[xUnit.net 00:00:00.30] -[xUnit.net 00:00:00.30] Stack Trace: -[xUnit.net 00:00:00.30] at NSubstitute.Core.Arguments.ArgumentSpecificationsFactory.Create(IList`1 argumentSpecs, Object[] arguments, IParameterInfo[] parameterInfos, MethodInfo methodInfo, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallSpecificationFactory.CreateFrom(ICall call, MatchArgs matchArgs) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Handlers.RecordCallSpecificationHandler.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Routing.Route.Handle(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Core.CallRouter.Route(ICall call) -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.CastleForwardingInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at NSubstitute.Proxies.CastleDynamicProxy.ProxyIdInterceptor.Intercept(IInvocation invocation) -[xUnit.net 00:00:00.30] at Castle.DynamicProxy.AbstractInvocation.Proceed() -[xUnit.net 00:00:00.30] at Castle.Proxies.AIFunctionProxy.InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] at Microsoft.Extensions.AI.AIFunction.InvokeAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) -[xUnit.net 00:00:00.30] /Users/alexsohn/code/sentry-dotnet/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs(160,0): at Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation() -[xUnit.net 00:00:00.30] --- End of stack trace from previous location --- -[xUnit.net 00:00:00.33] Finished: Sentry.Extensions.AI.Tests - - - - - [xUnit.net 00:00:00.20] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsUsageTokens_WhenUsageProvided [FAIL] - - - [xUnit.net 00:00:00.22] Sentry.Extensions.AI.Tests.SentryAISpanEnricherTests.EnrichWithResponse_SetsPartialTokens_WhenOnlyInputTokensProvided [FAIL] - - - [xUnit.net 00:00:00.29] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WhenInnerFunctionThrows_FinishesSpanWithException [FAIL] - - - [xUnit.net 00:00:00.29] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_PreservesInnerFunctionName [FAIL] - - - [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithParentSpan_CreatesChildSpan [FAIL] - - - [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithoutParentSpan_CreatesTransaction [FAIL] - - - [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithComplexResult_SetsOutputAsString [FAIL] - - - [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithNullResult_DoesNotSetOutput [FAIL] - - - [xUnit.net 00:00:00.30] Sentry.Extensions.AI.Tests.SentryInstrumentedFunctionTests.InvokeCoreAsync_WithCancellation_PropagatesCancellation [FAIL] - - - - \ No newline at end of file From 5f26ee605157489b91bd20aeebd0b0bb97c6ba53 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 20 Oct 2025 05:54:08 +0000 Subject: [PATCH 06/22] Format code --- modules/sentry-native | 2 +- samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs | 9 +++++---- samples/Sentry.Samples.ME.AI.Console/Program.cs | 2 +- src/Sentry.Extensions.AI/SentryAISpanEnricher.cs | 6 +++--- .../SentryAISpanEnricherTests.cs | 2 +- test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs | 2 +- .../SentryInstrumentedFunctionTests.cs | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/modules/sentry-native b/modules/sentry-native index 027459265a..22ac4cebcf 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit 027459265ab94de340a5f59b767248652640d1e6 +Subproject commit 22ac4cebcf749e7e3c47fa40bd1cf237d960258e diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs index 75a117da81..57aaf21f7d 100644 --- a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs @@ -8,9 +8,9 @@ builder.WebHost.UseSentry(options => { #if !SENTRY_DSN_DEFINED_IN_ENV - // A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable. - // See https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - options.Dsn = SamplesShared.Dsn; + // A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable. + // See https://docs.sentry.io/product/sentry-basics/dsn-explainer/ + options.Dsn = SamplesShared.Dsn; #endif options.Debug = true; @@ -121,7 +121,8 @@ "Please help me with the following tasks: 1) Find Alice's age, 2) Get weather in New York, 3) Calculate a complex result for number 15, 4) Get comprehensive info for Bob in London, and 5) Calculate average age for Alice, Bob, and Charlie (first get each person's age individually using GetPersonAge, then use CalculateAverageAge with those results). Please use the appropriate tools for each task and demonstrate tool chaining where needed.", options); - return Results.Ok(new { + return Results.Ok(new + { message = "AI test with multiple tools completed successfully", response = response.Messages?.FirstOrDefault()?.Text ?? "No response", timestamp = DateTime.UtcNow diff --git a/samples/Sentry.Samples.ME.AI.Console/Program.cs b/samples/Sentry.Samples.ME.AI.Console/Program.cs index ff65168df2..b5ba116f44 100644 --- a/samples/Sentry.Samples.ME.AI.Console/Program.cs +++ b/samples/Sentry.Samples.ME.AI.Console/Program.cs @@ -1,4 +1,4 @@ -using Anthropic.SDK; +using Anthropic.SDK; using Anthropic.SDK.Constants; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; diff --git a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs index 6d8697e90d..bc97700432 100644 --- a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs +++ b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs @@ -1,5 +1,5 @@ -using Microsoft.Extensions.AI; using System.Text.Json; +using Microsoft.Extensions.AI; namespace Sentry.Extensions.AI; @@ -122,13 +122,13 @@ public static void EnrichWithStreamingResponse(ISpan span, List(result); var jsonResult = (JsonElement)result; Assert.Equal("test output", jsonResult.GetString()); - + // The span should have recorded the ToString() output of the JsonElement // (This is testing the internal behavior that ToString() gets called for span data) } From 0950cec029f2ac39cc545f42291263ad8230abb6 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Mon, 20 Oct 2025 10:15:12 -0400 Subject: [PATCH 07/22] revert sentry-native update --- modules/sentry-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sentry-native b/modules/sentry-native index 22ac4cebcf..9bbbcaed72 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit 22ac4cebcf749e7e3c47fa40bd1cf237d960258e +Subproject commit 9bbbcaed729c2a7f6f258e3a2079ca736303bdec From 4d7354d97525040fa220f799b9328a81265f10d8 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Mon, 20 Oct 2025 10:16:23 -0400 Subject: [PATCH 08/22] revert sentry-native update (for real) --- modules/sentry-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sentry-native b/modules/sentry-native index 9bbbcaed72..027459265a 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit 9bbbcaed729c2a7f6f258e3a2079ca736303bdec +Subproject commit 027459265ab94de340a5f59b767248652640d1e6 From 125a81b88662e96952fa744ba99afaaba1e068d6 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Thu, 23 Oct 2025 16:41:06 -0400 Subject: [PATCH 09/22] chat spans between tool calls working --- .../Program.cs | 40 +++-- .../Sentry.Samples.ME.AI.AspNetCore.csproj | 5 +- .../Sentry.Samples.ME.AI.Console/Program.cs | 23 +-- .../Sentry.Samples.ME.AI.Console.csproj | 4 +- .../Extensions/SentryAIExtensions.cs | 2 +- src/Sentry.Extensions.AI/SentryAIOptions.cs | 5 + .../SentryAISpanEnricher.cs | 7 +- src/Sentry.Extensions.AI/SentryChatClient.cs | 144 +++++++++++------- .../SentryInstrumentedFunction.cs | 7 +- 9 files changed, 142 insertions(+), 95 deletions(-) diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs index 57aaf21f7d..6730823374 100644 --- a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs @@ -1,5 +1,3 @@ -using Anthropic.SDK; -using Anthropic.SDK.Constants; using Microsoft.Extensions.AI; using Sentry.Extensions.AI; @@ -20,17 +18,20 @@ options.Experimental.EnableLogs = true; }); -var client = new AnthropicClient().Messages - .AsBuilder() - .UseFunctionInvocation() - .Build() +var openAIClient = new OpenAI.Chat.ChatClient("gpt-4o-mini", Environment.GetEnvironmentVariable("OPENAI_API_KEY")) + .AsIChatClient() .WithSentry(options => { - options.IncludeAIRequestMessages = false; - options.IncludeAIResponseContent = false; + // In this case, we already initialized Sentry from ASP.NET WebHost creation, we don't need to initialize + options.IncludeAIRequestMessages = true; + options.IncludeAIResponseContent = true; }); -// Register the Claude API client and Sentry-instrumented chat client +var client = new ChatClientBuilder(openAIClient) + .UseFunctionInvocation() + .Build(); + +// Register the OpenAI API client and Sentry-instrumented chat client builder.Services.AddSingleton(client); var app = builder.Build(); @@ -41,7 +42,7 @@ logger.LogInformation("Running AI test endpoint with multiple tools"); var options = new ChatOptions { - ModelId = AnthropicModels.Claude3Haiku, + ModelId = "gpt-4o-mini", MaxOutputTokens = 1024, Tools = [ // Tool 1: Quick response with minimal delay @@ -117,14 +118,23 @@ try { - var response = await chatClient.GetResponseAsync( - "Please help me with the following tasks: 1) Find Alice's age, 2) Get weather in New York, 3) Calculate a complex result for number 15, 4) Get comprehensive info for Bob in London, and 5) Calculate average age for Alice, Bob, and Charlie (first get each person's age individually using GetPersonAge, then use CalculateAverageAge with those results). Please use the appropriate tools for each task and demonstrate tool chaining where needed.", - options); + var streamingResponse = new List(); + await foreach (var update in chatClient.GetStreamingResponseAsync([ + new ChatMessage(ChatRole.User, "Please help me with the following tasks: 1) Find Alice's age, 2) Get weather in New York, 3) Calculate a complex result for number 15, 4) Get comprehensive info for Bob in London, and 5) Calculate average age for Alice, Bob, and Charlie (first get each person's age individually using GetPersonAge, then use CalculateAverageAge with those results). Please use the appropriate tools for each task and demonstrate tool chaining where needed.") + ], options)) + { + if (!string.IsNullOrEmpty(update.Text)) + { + streamingResponse.Add(update.Text); + } + } + + var fullResponse = string.Concat(streamingResponse); return Results.Ok(new { - message = "AI test with multiple tools completed successfully", - response = response.Messages?.FirstOrDefault()?.Text ?? "No response", + message = "AI test with multiple tools completed successfully (streaming)", + response = fullResponse, timestamp = DateTime.UtcNow }); } diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj b/samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj index ca28278952..e8cfeab2e1 100644 --- a/samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Sentry.Samples.ME.AI.AspNetCore.csproj @@ -5,9 +5,8 @@ - - + @@ -22,4 +21,4 @@ - \ No newline at end of file + diff --git a/samples/Sentry.Samples.ME.AI.Console/Program.cs b/samples/Sentry.Samples.ME.AI.Console/Program.cs index b5ba116f44..1aa3df4552 100644 --- a/samples/Sentry.Samples.ME.AI.Console/Program.cs +++ b/samples/Sentry.Samples.ME.AI.Console/Program.cs @@ -1,5 +1,3 @@ -using Anthropic.SDK; -using Anthropic.SDK.Constants; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Sentry.Extensions.AI; @@ -9,11 +7,9 @@ logger.LogInformation("Starting Microsoft.Extensions.AI sample with Sentry instrumentation"); -// Create Claude API client and wrap it with Sentry instrumentation -var client = new AnthropicClient().Messages - .AsBuilder() - .UseFunctionInvocation() - .Build() +// Create OpenAI API client and wrap it with Sentry instrumentation +var openAIClient = new OpenAI.Chat.ChatClient("gpt-4o-mini", Environment.GetEnvironmentVariable("OPENAI_API_KEY")) + .AsIChatClient() .WithSentry(options => { #if !SENTRY_DSN_DEFINED_IN_ENV @@ -27,16 +23,21 @@ options.TracesSampleRate = 1; // AI-specific settings - options.IncludeAIRequestMessages = false; - options.IncludeAIResponseContent = false; + options.IncludeAIRequestMessages = true; + options.IncludeAIResponseContent = true; + // Since this is a simple console app without Sentry already set up, we need to initialize our SDK options.InitializeSdk = true; }); +var client = new ChatClientBuilder(openAIClient) + .UseFunctionInvocation() + .Build(); + logger.LogInformation("Making AI call with Sentry instrumentation and tools..."); var options = new ChatOptions { - ModelId = AnthropicModels.Claude3Haiku, + ModelId = "gpt-4o-mini", MaxOutputTokens = 1024, Tools = [ // Tool 1: Quick response with minimal delay @@ -87,7 +88,7 @@ var streamingOptions = new ChatOptions { - ModelId = AnthropicModels.Claude3Haiku, + ModelId = "gpt-4o-mini", MaxOutputTokens = 1024 }.WithSentry(); diff --git a/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj b/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj index 83db5db8a8..5b97f854d5 100644 --- a/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj +++ b/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj @@ -6,9 +6,9 @@ - - + + diff --git a/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs index f05fac1822..91e4ddd1b8 100644 --- a/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs +++ b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs @@ -27,7 +27,7 @@ public static ChatOptions WithSentry( var tool = options.Tools[i]; if (tool is AIFunction fn and not SentryInstrumentedFunction) { - options.Tools[i] = new SentryInstrumentedFunction(fn); + options.Tools[i] = new SentryInstrumentedFunction(fn, options); } } diff --git a/src/Sentry.Extensions.AI/SentryAIOptions.cs b/src/Sentry.Extensions.AI/SentryAIOptions.cs index 4d839e2c57..7618c1f9f6 100644 --- a/src/Sentry.Extensions.AI/SentryAIOptions.cs +++ b/src/Sentry.Extensions.AI/SentryAIOptions.cs @@ -16,6 +16,11 @@ public class SentryAIOptions : SentryOptions /// public bool IncludeAIResponseContent { get; set; } = true; + /// + /// Name of the AI Agent + /// + public string AgentName { get; set; } = "Agent"; + /// /// Whether to initialize the Sentry SDK through this integration. /// diff --git a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs index bc97700432..676370ea08 100644 --- a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs +++ b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs @@ -25,9 +25,10 @@ internal static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatO { span.SetData("gen_ai.request.model", modelId); } - else + + if (aiOptions?.AgentName is { } agentName) { - span.SetData("gen_ai.request.model", "Unknown model"); + span.SetData("gen_ai.agent.name", agentName); } if (messages is { Length: > 0 } && (aiOptions?.IncludeAIRequestMessages ?? true)) @@ -102,7 +103,7 @@ internal static void EnrichWithResponse(ISpan span, ChatResponse response, Sentr if (response.ModelId is { } modelId) { - span.SetData("gen_ai.response.model_id", modelId); + span.SetData("gen_ai.response.model", modelId); } } diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index a59870c500..630db14a4e 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -7,20 +7,17 @@ internal sealed class SentryChatClient : DelegatingChatClient { private readonly HubAdapter _hub; private readonly SentryAIOptions _sentryAIOptions; + private static ISpan? RootSpan; public SentryChatClient(IChatClient client, Action? configure = null) : base(client) { _sentryAIOptions = new SentryAIOptions(); configure?.Invoke(_sentryAIOptions); - if (_sentryAIOptions.InitializeSdk) + if (_sentryAIOptions.InitializeSdk && !SentrySdk.IsEnabled) { - if (!SentrySdk.IsEnabled || _sentryAIOptions.Dsn is not null) - { - // Initialize Sentry with our options/DSN - var hub = SentrySdk.InitHub(_sentryAIOptions); - SentrySdk.UseHub(hub); - } + var hub = SentrySdk.InitHub(_sentryAIOptions); + SentrySdk.UseHub(hub); } _hub = HubAdapter.Instance; @@ -31,83 +28,87 @@ public override async Task GetResponseAsync(IEnumerable - public override IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, + public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, - CancellationToken cancellationToken = new()) + [EnumeratorCancellation] CancellationToken cancellationToken = new()) { - var invokeSpanName = $"invoke_agent {InnerClient.GetType().Name}"; - const string invokeOperation = "gen_ai.invoke_agent"; - var outerSpan = StartSpanOrTransaction(invokeOperation, invokeSpanName); - - const string chatOperation = "gen_ai.chat"; - var chatSpanName = options is null || string.IsNullOrEmpty(options.ModelId) ? "chat unknown model" : $"chat {options.ModelId}"; - var initialSpan = outerSpan.StartChild(chatOperation, chatSpanName); - - try - { - return InstrumentStreamingResponseAsync(messages, options, outerSpan, initialSpan, cancellationToken); - } - catch (Exception ex) - { - initialSpan.Finish(ex); - outerSpan.Finish(ex); - _hub.CaptureException(ex); - throw; - } - } - - private async IAsyncEnumerable InstrumentStreamingResponseAsync(IEnumerable messages, - ChatOptions? options, - ISpan outerSpan, - ISpan span, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); - SentryAISpanEnricher.EnrichWithRequest(span, chatMessages, options, _sentryAIOptions); + var outerSpan = EnsureRootSpanExists(); + var innerSpan = CreateChatSpan(options, outerSpan); var responses = new List(); - var originalStream = base.GetStreamingResponseAsync(chatMessages, options, cancellationToken); + var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); + var enumerator = base + .GetStreamingResponseAsync(chatMessages, options, cancellationToken) + .GetAsyncEnumerator(cancellationToken); - await foreach (var chunk in originalStream.ConfigureAwait(false)) + while (true) { - responses.Add(chunk); + ChatResponseUpdate? current; - yield return chunk; - } + try + { + SentryAISpanEnricher.EnrichWithRequest(innerSpan, chatMessages, options, _sentryAIOptions); + var hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); + if (!hasNext) + { + SentryAISpanEnricher.EnrichWithStreamingResponse(innerSpan, responses, _sentryAIOptions); + innerSpan.Finish(SpanStatus.Ok); + outerSpan.Finish(SpanStatus.Ok); + + if (!ContainsFunctionCalls(responses)) + { + RootSpan = null; + } + + yield break; + } + + current = enumerator.Current; + responses.Add(enumerator.Current); + } + catch (Exception ex) + { + innerSpan.Finish(ex); + outerSpan.Finish(ex); + _hub.CaptureException(ex); + RootSpan = null; + throw; + } - SentryAISpanEnricher.EnrichWithStreamingResponse(span, responses, _sentryAIOptions); - span.Finish(SpanStatus.Ok); - outerSpan.Finish(SpanStatus.Ok); + yield return current; + } } /// @@ -129,4 +130,31 @@ private ISpan StartSpanOrTransaction(string operation, string description) _hub.ConfigureScope(scope => scope.Transaction = newTransaction); return newTransaction; } + + private static bool ContainsFunctionCalls(ChatResponse response) => + response.Messages.Any(m => m.Contents?.OfType().Any() ?? false) + || response.FinishReason == ChatFinishReason.ToolCalls; + + private static bool ContainsFunctionCalls(List responses) => + responses.Any(m => m.Contents?.OfType().Any() ?? false) + || responses.Any(m => m.FinishReason == ChatFinishReason.ToolCalls); + + private ISpan EnsureRootSpanExists() + { + var invokeSpanName = $"invoke_agent {InnerClient.GetType().Name}"; + const string invokeOperation = "gen_ai.invoke_agent"; + RootSpan ??= StartSpanOrTransaction(invokeOperation, invokeSpanName); + // In ME.AI, there's not really an agent name. In other SDKs we set this, so we should do so here + RootSpan.SetData("gen_ai.agent.name", $"{InnerClient.GetType().Name}"); + return RootSpan; + } + + private static ISpan CreateChatSpan(ChatOptions? options, ISpan outerSpan) + { + const string chatOperation = "gen_ai.chat"; + var chatSpanName = options is null || string.IsNullOrEmpty(options.ModelId) + ? "chat unknown model" + : $"chat {options.ModelId}"; + return outerSpan.StartChild(chatOperation, chatSpanName); + } } diff --git a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs index 4ab001e7c7..54caee614f 100644 --- a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs +++ b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs @@ -3,7 +3,8 @@ namespace Sentry.Extensions.AI; -internal sealed class SentryInstrumentedFunction(AIFunction innerFunction) : DelegatingAIFunction(innerFunction) +internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatOptions? aiOptions = null) + : DelegatingAIFunction(innerFunction) { private readonly HubAdapter _hub = HubAdapter.Instance; @@ -11,11 +12,13 @@ internal sealed class SentryInstrumentedFunction(AIFunction innerFunction) : Del AIFunctionArguments arguments, CancellationToken cancellationToken) { - var parentSpan = _hub.GetSpan(); const string operation = "gen_ai.execute_tool"; var spanName = $"execute_tool {Name}"; + var parentSpan = _hub.GetSpan(); var currSpan = parentSpan?.StartChild(operation, spanName) ?? _hub.StartTransaction(spanName, operation); + currSpan.SetData("gen_ai.request.model", aiOptions?.ModelId); + currSpan.SetData("gen_ai.operation.name", "execute_tool"); currSpan.SetData("gen_ai.tool.name", Name); currSpan.SetData("gen_ai.tool.description", Description); From b08ec8ea8cb665995fe266dcd13177f5fac7d577 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sat, 25 Oct 2025 23:15:01 -0400 Subject: [PATCH 10/22] fixed tests and span generation --- .../Sentry.Samples.ME.AI.Console/Program.cs | 6 +++ src/Sentry.Extensions.AI/SentryChatClient.cs | 50 ++++++------------- .../SentryInstrumentedFunction.cs | 5 +- .../SentryAISpanEnricherTests.cs | 8 +-- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/samples/Sentry.Samples.ME.AI.Console/Program.cs b/samples/Sentry.Samples.ME.AI.Console/Program.cs index 1aa3df4552..f32cfd0ae9 100644 --- a/samples/Sentry.Samples.ME.AI.Console/Program.cs +++ b/samples/Sentry.Samples.ME.AI.Console/Program.cs @@ -35,6 +35,10 @@ logger.LogInformation("Making AI call with Sentry instrumentation and tools..."); +// This starts a new transaction and attaches it to the scope. +var transaction = SentrySdk.StartTransaction("Program Main", "function"); +SentrySdk.ConfigureScope(scope => scope.Transaction = transaction); + var options = new ChatOptions { ModelId = "gpt-4o-mini", @@ -109,5 +113,7 @@ logger.LogInformation("Microsoft.Extensions.AI sample completed! Check your Sentry dashboard for the trace data."); +transaction.Finish(); + // Flush Sentry to ensure all transactions are sent before the app exits await SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)); diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index 630db14a4e..d7d2e836fb 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -7,7 +7,7 @@ internal sealed class SentryChatClient : DelegatingChatClient { private readonly HubAdapter _hub; private readonly SentryAIOptions _sentryAIOptions; - private static ISpan? RootSpan; + internal static ISpan? RootSpan; public SentryChatClient(IChatClient client, Action? configure = null) : base(client) { @@ -16,8 +16,7 @@ public SentryChatClient(IChatClient client, Action? configure = if (_sentryAIOptions.InitializeSdk && !SentrySdk.IsEnabled) { - var hub = SentrySdk.InitHub(_sentryAIOptions); - SentrySdk.UseHub(hub); + SentrySdk.Init(_sentryAIOptions); } _hub = HubAdapter.Instance; @@ -111,41 +110,16 @@ public override async IAsyncEnumerable GetStreamingResponseA } } - /// - /// Starts a span or transaction based on whether there's an active transaction context. - /// - /// The operation name - /// The span/transaction description - /// A child span of an existing transaction if available, else a new transaction - private ISpan StartSpanOrTransaction(string operation, string description) + private ISpan EnsureRootSpanExists() { - var currentSpan = _hub.GetSpan(); - - if (currentSpan?.GetTransaction() != null) + if (RootSpan == null) { - return currentSpan.StartChild(operation, description); + var invokeSpanName = $"invoke_agent {InnerClient.GetType().Name}"; + const string invokeOperation = "gen_ai.invoke_agent"; + RootSpan = _hub.StartSpan(invokeOperation, invokeSpanName); + RootSpan.SetData("gen_ai.agent.name", $"{InnerClient.GetType().Name}"); } - var newTransaction = _hub.StartTransaction(description, operation); - _hub.ConfigureScope(scope => scope.Transaction = newTransaction); - return newTransaction; - } - - private static bool ContainsFunctionCalls(ChatResponse response) => - response.Messages.Any(m => m.Contents?.OfType().Any() ?? false) - || response.FinishReason == ChatFinishReason.ToolCalls; - - private static bool ContainsFunctionCalls(List responses) => - responses.Any(m => m.Contents?.OfType().Any() ?? false) - || responses.Any(m => m.FinishReason == ChatFinishReason.ToolCalls); - - private ISpan EnsureRootSpanExists() - { - var invokeSpanName = $"invoke_agent {InnerClient.GetType().Name}"; - const string invokeOperation = "gen_ai.invoke_agent"; - RootSpan ??= StartSpanOrTransaction(invokeOperation, invokeSpanName); - // In ME.AI, there's not really an agent name. In other SDKs we set this, so we should do so here - RootSpan.SetData("gen_ai.agent.name", $"{InnerClient.GetType().Name}"); return RootSpan; } @@ -157,4 +131,12 @@ private static ISpan CreateChatSpan(ChatOptions? options, ISpan outerSpan) : $"chat {options.ModelId}"; return outerSpan.StartChild(chatOperation, chatSpanName); } + + private static bool ContainsFunctionCalls(ChatResponse response) => + response.Messages.Any(m => m.Contents?.OfType().Any() ?? false) + || response.FinishReason == ChatFinishReason.ToolCalls; + + private static bool ContainsFunctionCalls(List responses) => + responses.Any(m => m.Contents?.OfType().Any() ?? false) + || responses.Any(m => m.FinishReason == ChatFinishReason.ToolCalls); } diff --git a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs index 54caee614f..e50589baf4 100644 --- a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs +++ b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs @@ -14,8 +14,9 @@ internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatO { const string operation = "gen_ai.execute_tool"; var spanName = $"execute_tool {Name}"; - var parentSpan = _hub.GetSpan(); - var currSpan = parentSpan?.StartChild(operation, spanName) ?? _hub.StartTransaction(spanName, operation); + var currSpan = SentryChatClient.RootSpan == null ? + _hub.StartSpan(operation, spanName) : + SentryChatClient.RootSpan.StartChild(operation, spanName); currSpan.SetData("gen_ai.request.model", aiOptions?.ModelId); diff --git a/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs b/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs index d95320b50f..ed3f64bfe0 100644 --- a/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs +++ b/test/Sentry.Extensions.AI.Tests/SentryAISpanEnricherTests.cs @@ -26,7 +26,7 @@ public void EnrichWithRequest_SetsBasicOperationName() } [Fact] - public void EnrichWithRequest_SetsModelId_WhenProvided() + public void EnrichWithRequest_SetsModel_WhenProvided() { var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; var options = new ChatOptions { ModelId = "gpt-4" }; @@ -37,13 +37,13 @@ public void EnrichWithRequest_SetsModelId_WhenProvided() } [Fact] - public void EnrichWithRequest_SetsUnknownModel_WhenModelIdNotProvided() + public void EnrichWithRequest_DoesntSetModel_WhenModelIdNotProvided() { var messages = new[] { new ChatMessage(ChatRole.User, "Hello") }; SentryAISpanEnricher.EnrichWithRequest(_mockSpan, messages, null); - _mockSpan.Received(1).SetData("gen_ai.request.model", "Unknown model"); + _mockSpan.DidNotReceive().SetData("gen_ai.request.model", Arg.Any()); } [Fact] @@ -247,7 +247,7 @@ public void EnrichWithResponse_SetsModelId_WhenProvided() SentryAISpanEnricher.EnrichWithResponse(_mockSpan, response); - _mockSpan.Received(1).SetData("gen_ai.response.model_id", "gpt-4-turbo"); + _mockSpan.Received().SetData("gen_ai.response.model", "gpt-4-turbo"); } [Fact] From c510ccfebfc364b6edd8271fdf916e4492ace7f8 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Sun, 26 Oct 2025 03:34:05 +0000 Subject: [PATCH 11/22] Format code --- modules/sentry-native | 2 +- src/Sentry.Extensions.AI/SentryAISpanEnricher.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/sentry-native b/modules/sentry-native index 027459265a..22ac4cebcf 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit 027459265ab94de340a5f59b767248652640d1e6 +Subproject commit 22ac4cebcf749e7e3c47fa40bd1cf237d960258e diff --git a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs index 676370ea08..971698e58c 100644 --- a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs +++ b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs @@ -28,7 +28,7 @@ internal static void EnrichWithRequest(ISpan span, ChatMessage[] messages, ChatO if (aiOptions?.AgentName is { } agentName) { - span.SetData("gen_ai.agent.name", agentName); + span.SetData("gen_ai.agent.name", agentName); } if (messages is { Length: > 0 } && (aiOptions?.IncludeAIRequestMessages ?? true)) From 2e0c15cd2f638bc9a26f8f086da39cb950e4be65 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 16:27:12 -0400 Subject: [PATCH 12/22] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd5653686..3f287c8ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add support for `Microsoft.Extensions.AI` instrumentation ([#4657](https://github.com/getsentry/sentry-dotnet/pull/4657)) + ### Dependencies - Bump Java SDK from v8.22.0 to v8.23.0 ([#4586](https://github.com/getsentry/sentry-dotnet/pull/4586)) From 8502967ac091ce883ac951577003c3944432f0a4 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 16:32:21 -0400 Subject: [PATCH 13/22] revert sentry-native update --- modules/sentry-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sentry-native b/modules/sentry-native index 22ac4cebcf..027459265a 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit 22ac4cebcf749e7e3c47fa40bd1cf237d960258e +Subproject commit 027459265ab94de340a5f59b767248652640d1e6 From a5158b0c8715a459fe2b57b66ed79ca0be09b62f Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 17:00:59 -0400 Subject: [PATCH 14/22] Remove unused using in AISpanEnricher --- src/Sentry.Extensions.AI/SentryAISpanEnricher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs index 971698e58c..78e89cae0a 100644 --- a/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs +++ b/src/Sentry.Extensions.AI/SentryAISpanEnricher.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Microsoft.Extensions.AI; namespace Sentry.Extensions.AI; From 5bc81f12f90c7185bb90a4c1cbadbf0d29b2b853 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 17:19:06 -0400 Subject: [PATCH 15/22] Remove redundant span enrichment in GetStreamingResponseAsync --- src/Sentry.Extensions.AI/SentryChatClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index d7d2e836fb..febb3fffd5 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -78,7 +78,6 @@ public override async IAsyncEnumerable GetStreamingResponseA try { - SentryAISpanEnricher.EnrichWithRequest(innerSpan, chatMessages, options, _sentryAIOptions); var hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); if (!hasNext) { From a7bce63335fb9d52a9716a2123de366bf385c58e Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 17:20:57 -0400 Subject: [PATCH 16/22] forgot to add span enrichment --- src/Sentry.Extensions.AI/SentryChatClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index febb3fffd5..16304e259d 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -71,6 +71,7 @@ public override async IAsyncEnumerable GetStreamingResponseA var enumerator = base .GetStreamingResponseAsync(chatMessages, options, cancellationToken) .GetAsyncEnumerator(cancellationToken); + SentryAISpanEnricher.EnrichWithRequest(innerSpan, chatMessages, options, _sentryAIOptions); while (true) { From 7f49d5471e36243a5bdadac5b3dca9954a145d1a Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 17:39:34 -0400 Subject: [PATCH 17/22] Change options argument name in SentryInstrumentedFunction --- src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs index e50589baf4..724ccefba1 100644 --- a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs +++ b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs @@ -3,7 +3,7 @@ namespace Sentry.Extensions.AI; -internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatOptions? aiOptions = null) +internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatOptions? options = null) : DelegatingAIFunction(innerFunction) { private readonly HubAdapter _hub = HubAdapter.Instance; @@ -18,7 +18,7 @@ internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatO _hub.StartSpan(operation, spanName) : SentryChatClient.RootSpan.StartChild(operation, spanName); - currSpan.SetData("gen_ai.request.model", aiOptions?.ModelId); + currSpan.SetData("gen_ai.request.model", options?.ModelId); currSpan.SetData("gen_ai.operation.name", "execute_tool"); currSpan.SetData("gen_ai.tool.name", Name); From 136baf8102acba0eb550b9db46c3a833f5923640 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 22:33:56 -0400 Subject: [PATCH 18/22] Fix wrong checks for finishing root span --- src/Sentry.Extensions.AI/SentryChatClient.cs | 25 ++++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index 16304e259d..c1be3673bf 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -40,7 +40,11 @@ public override async Task GetResponseAsync(IEnumerable GetStreamingResponseA .GetStreamingResponseAsync(chatMessages, options, cancellationToken) .GetAsyncEnumerator(cancellationToken); SentryAISpanEnricher.EnrichWithRequest(innerSpan, chatMessages, options, _sentryAIOptions); + ChatResponseUpdate? current = null; while (true) { - ChatResponseUpdate? current; - try { var hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); @@ -84,10 +87,14 @@ public override async IAsyncEnumerable GetStreamingResponseA { SentryAISpanEnricher.EnrichWithStreamingResponse(innerSpan, responses, _sentryAIOptions); innerSpan.Finish(SpanStatus.Ok); - outerSpan.Finish(SpanStatus.Ok); - if (!ContainsFunctionCalls(responses)) + // Only if currentFinishReason is to stop, then we finish the RootSpan and set it to null. + // This allows the RootSpan to persist throughout multiple `GetStreamingResponseAsync` calls + // happening before and after tool calls + var shouldFinishRootSpan = current?.FinishReason == ChatFinishReason.Stop; + if (shouldFinishRootSpan) { + outerSpan.Finish(SpanStatus.Ok); RootSpan = null; } @@ -131,12 +138,4 @@ private static ISpan CreateChatSpan(ChatOptions? options, ISpan outerSpan) : $"chat {options.ModelId}"; return outerSpan.StartChild(chatOperation, chatSpanName); } - - private static bool ContainsFunctionCalls(ChatResponse response) => - response.Messages.Any(m => m.Contents?.OfType().Any() ?? false) - || response.FinishReason == ChatFinishReason.ToolCalls; - - private static bool ContainsFunctionCalls(List responses) => - responses.Any(m => m.Contents?.OfType().Any() ?? false) - || responses.Any(m => m.FinishReason == ChatFinishReason.ToolCalls); } From a61f9c7dc44fe9ed32aed8701d478211e2c7e936 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Sun, 26 Oct 2025 23:19:19 -0400 Subject: [PATCH 19/22] Add Env var checks for OpenAI API key in samples --- samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs | 14 ++++++++++++-- samples/Sentry.Samples.ME.AI.Console/Program.cs | 11 +++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs index 6730823374..be78c32305 100644 --- a/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.ME.AI.AspNetCore/Program.cs @@ -1,3 +1,4 @@ +#nullable enable using Microsoft.Extensions.AI; using Sentry.Extensions.AI; @@ -18,7 +19,16 @@ options.Experimental.EnableLogs = true; }); -var openAIClient = new OpenAI.Chat.ChatClient("gpt-4o-mini", Environment.GetEnvironmentVariable("OPENAI_API_KEY")) +// This sample uses Microsoft.Extensions.AI.OpenAI +// Check whether OPENAI_API_KEY env var exists +const string varName = "OPENAI_API_KEY"; +var openAiApiKey = Environment.GetEnvironmentVariable(varName); +if (openAiApiKey == null) +{ + throw new InvalidOperationException($"Environment variable for OpenAI API key '{varName}' is not set."); +} + +var openAiClient = new OpenAI.Chat.ChatClient("gpt-4o-mini", openAiApiKey) .AsIChatClient() .WithSentry(options => { @@ -27,7 +37,7 @@ options.IncludeAIResponseContent = true; }); -var client = new ChatClientBuilder(openAIClient) +var client = new ChatClientBuilder(openAiClient) .UseFunctionInvocation() .Build(); diff --git a/samples/Sentry.Samples.ME.AI.Console/Program.cs b/samples/Sentry.Samples.ME.AI.Console/Program.cs index f32cfd0ae9..d57a8076d1 100644 --- a/samples/Sentry.Samples.ME.AI.Console/Program.cs +++ b/samples/Sentry.Samples.ME.AI.Console/Program.cs @@ -7,8 +7,15 @@ logger.LogInformation("Starting Microsoft.Extensions.AI sample with Sentry instrumentation"); +const string varName = "OPENAI_API_KEY"; +var openAiApiKey = Environment.GetEnvironmentVariable(varName); +if (openAiApiKey == null) +{ + throw new InvalidOperationException($"Environment variable for OpenAI API key '{varName}' is not set."); +} + // Create OpenAI API client and wrap it with Sentry instrumentation -var openAIClient = new OpenAI.Chat.ChatClient("gpt-4o-mini", Environment.GetEnvironmentVariable("OPENAI_API_KEY")) +var openAiClient = new OpenAI.Chat.ChatClient("gpt-4o-mini", openAiApiKey) .AsIChatClient() .WithSentry(options => { @@ -29,7 +36,7 @@ options.InitializeSdk = true; }); -var client = new ChatClientBuilder(openAIClient) +var client = new ChatClientBuilder(openAiClient) .UseFunctionInvocation() .Build(); From a26501639fc28586d910d8aa44175e30825878a6 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Tue, 28 Oct 2025 14:00:19 -0400 Subject: [PATCH 20/22] Fix concurrency issue when multiple GetResponseAsync is called --- .../Extensions/SentryAIExtensions.cs | 6 + src/Sentry.Extensions.AI/SentryAIConstants.cs | 27 ++++ src/Sentry.Extensions.AI/SentryChatClient.cs | 128 ++++++++++++++---- .../SentryInstrumentedFunction.cs | 53 +++++--- 4 files changed, 175 insertions(+), 39 deletions(-) create mode 100644 src/Sentry.Extensions.AI/SentryAIConstants.cs diff --git a/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs index 91e4ddd1b8..be6c2e951e 100644 --- a/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs +++ b/src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs @@ -22,6 +22,7 @@ public static ChatOptions WithSentry( return options; } + // We wrap tools here so we don't have to wrap them each time we grab the response for (var i = 0; i < options.Tools.Count; i++) { var tool = options.Tools[i]; @@ -31,6 +32,11 @@ public static ChatOptions WithSentry( } } + // SentrySpanStore additional property will store the dictionary to keep track of which span is + // the "agent" span, which will persist through different chat/tool calls. + options.AdditionalProperties ??= new AdditionalPropertiesDictionary(); + options.AdditionalProperties.TryAdd("SentryChatMessageAgentSpan", new ConcurrentDictionary()); + return options; } diff --git a/src/Sentry.Extensions.AI/SentryAIConstants.cs b/src/Sentry.Extensions.AI/SentryAIConstants.cs new file mode 100644 index 0000000000..ac4ad771f1 --- /dev/null +++ b/src/Sentry.Extensions.AI/SentryAIConstants.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.AI; + +namespace Sentry.Extensions.AI; + +internal static class SentryAIConstants +{ + /// + /// + /// Sentry will add a to AdditionalAttribute in . + /// + /// + /// This constant represents the string key to get the span which represents the agent span. + /// + /// + internal const string OptionsAdditionalAttributeAgentSpanName = "SentryChatMessageAgentSpan"; + + /// + /// + /// When an LLM uses a tool, Sentry will add an argument to . + /// The additional argument will contain the request which initialized the tool call. + /// + /// + /// This constant represents the string key to get the message. + /// + /// + internal const string KeyMessageFunctionArgumentDictKey = "SentrySpanToMessageDictKey"; +} diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index c1be3673bf..dc405e3a71 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -7,7 +7,6 @@ internal sealed class SentryChatClient : DelegatingChatClient { private readonly HubAdapter _hub; private readonly SentryAIOptions _sentryAIOptions; - internal static ISpan? RootSpan; public SentryChatClient(IChatClient client, Action? configure = null) : base(client) { @@ -27,12 +26,15 @@ public override async Task GetResponseAsync(IEnumerable GetResponseAsync(IEnumerable GetResponseAsync(IEnumerable - public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, + public override async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = new()) { - var outerSpan = EnsureRootSpanExists(); - var innerSpan = CreateChatSpan(options, outerSpan); + var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); + var keyMessage = chatMessages[0]; + var outerSpan = CreateOrGetRootSpan(keyMessage, options); + var innerSpan = CreateChatSpan(outerSpan, options); + var spanDict = GetMessageToSpanDict(options); + SetMessageToSpanDict(keyMessage, outerSpan, options); + ChatResponseUpdate? current = null; var responses = new List(); - var chatMessages = messages as ChatMessage[] ?? messages.ToArray(); var enumerator = base .GetStreamingResponseAsync(chatMessages, options, cancellationToken) .GetAsyncEnumerator(cancellationToken); SentryAISpanEnricher.EnrichWithRequest(innerSpan, chatMessages, options, _sentryAIOptions); - ChatResponseUpdate? current = null; while (true) { try { var hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); + if (!hasNext) { SentryAISpanEnricher.EnrichWithStreamingResponse(innerSpan, responses, _sentryAIOptions); @@ -95,7 +105,11 @@ public override async IAsyncEnumerable GetStreamingResponseA if (shouldFinishRootSpan) { outerSpan.Finish(SpanStatus.Ok); - RootSpan = null; + spanDict.Remove(keyMessage, out _); + } + else if (current?.FinishReason == ChatFinishReason.ToolCalls) + { + WrapFunctionCallsInResponse(current, keyMessage); } yield break; @@ -109,7 +123,7 @@ public override async IAsyncEnumerable GetStreamingResponseA innerSpan.Finish(ex); outerSpan.Finish(ex); _hub.CaptureException(ex); - RootSpan = null; + spanDict.Remove(keyMessage, out _); throw; } @@ -117,20 +131,28 @@ public override async IAsyncEnumerable GetStreamingResponseA } } - private ISpan EnsureRootSpanExists() + /// + /// We create an entry in _spans concurrent dictionary to keep track of + /// what root span to use in consequent calls of or + /// + /// + /// + /// + private ISpan CreateOrGetRootSpan(ChatMessage message, ChatOptions? options) { - if (RootSpan == null) + var spanDict = GetMessageToSpanDict(options); + if (!spanDict.TryGetValue(message, out var rootSpan)) { var invokeSpanName = $"invoke_agent {InnerClient.GetType().Name}"; const string invokeOperation = "gen_ai.invoke_agent"; - RootSpan = _hub.StartSpan(invokeOperation, invokeSpanName); - RootSpan.SetData("gen_ai.agent.name", $"{InnerClient.GetType().Name}"); + rootSpan = _hub.StartSpan(invokeOperation, invokeSpanName); + rootSpan.SetData("gen_ai.agent.name", $"{InnerClient.GetType().Name}"); } - return RootSpan; + return rootSpan; } - private static ISpan CreateChatSpan(ChatOptions? options, ISpan outerSpan) + private static ISpan CreateChatSpan(ISpan outerSpan, ChatOptions? options) { const string chatOperation = "gen_ai.chat"; var chatSpanName = options is null || string.IsNullOrEmpty(options.ModelId) @@ -138,4 +160,64 @@ private static ISpan CreateChatSpan(ChatOptions? options, ISpan outerSpan) : $"chat {options.ModelId}"; return outerSpan.StartChild(chatOperation, chatSpanName); } + + internal static ConcurrentDictionary GetMessageToSpanDict(ChatOptions? options = null) + { + if (options?.AdditionalProperties?.TryGetValue>( + SentryAIConstants.OptionsAdditionalAttributeAgentSpanName, out var agentSpanDict) == true) + { + return agentSpanDict; + } + + // If we couldn't find the dictionary, we just initiate it now + agentSpanDict = new ConcurrentDictionary(); + if (options == null) + { + return agentSpanDict; + } + + options.AdditionalProperties = new AdditionalPropertiesDictionary(); + options.AdditionalProperties.TryAdd(SentryAIConstants.OptionsAdditionalAttributeAgentSpanName, agentSpanDict); + return agentSpanDict; + } + + private static void SetMessageToSpanDict(ChatMessage message, ISpan agentSpan, ChatOptions? options) + { + ConcurrentDictionary? agentSpanDict = null; + if (options == null || + options.AdditionalProperties?.TryGetValue(SentryAIConstants.OptionsAdditionalAttributeAgentSpanName, + out agentSpanDict) == false) + { + return; + } + + agentSpanDict?.TryAdd(message, agentSpan); + } + + private static void WrapFunctionCallsInResponse(ChatResponse response, ChatMessage keyMessage) + { + foreach (var message in response.Messages) + { + foreach (var content in message.Contents) + { + if (content is FunctionCallContent functionCall) + { + (functionCall.Arguments ??= new Dictionary()).Add( + SentryAIConstants.KeyMessageFunctionArgumentDictKey, keyMessage); + } + } + } + } + + private static void WrapFunctionCallsInResponse(ChatResponseUpdate response, ChatMessage keyMessage) + { + foreach (var content in response.Contents) + { + if (content is FunctionCallContent functionCall) + { + (functionCall.Arguments ??= new Dictionary()).Add( + SentryAIConstants.KeyMessageFunctionArgumentDictKey, keyMessage); + } + } + } } diff --git a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs index 724ccefba1..322774d033 100644 --- a/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs +++ b/src/Sentry.Extensions.AI/SentryInstrumentedFunction.cs @@ -3,29 +3,17 @@ namespace Sentry.Extensions.AI; -internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatOptions? options = null) +internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatOptions options) : DelegatingAIFunction(innerFunction) { - private readonly HubAdapter _hub = HubAdapter.Instance; + private static readonly HubAdapter Hub = HubAdapter.Instance; protected override async ValueTask InvokeCoreAsync( AIFunctionArguments arguments, CancellationToken cancellationToken) { - const string operation = "gen_ai.execute_tool"; - var spanName = $"execute_tool {Name}"; - var currSpan = SentryChatClient.RootSpan == null ? - _hub.StartSpan(operation, spanName) : - SentryChatClient.RootSpan.StartChild(operation, spanName); - - currSpan.SetData("gen_ai.request.model", options?.ModelId); - - currSpan.SetData("gen_ai.operation.name", "execute_tool"); - currSpan.SetData("gen_ai.tool.name", Name); - currSpan.SetData("gen_ai.tool.description", Description); - - currSpan.SetData("gen_ai.tool.input", arguments); - + var currSpan = InitToolSpan(arguments); + RemoveSentryArgs(ref arguments); try { var result = await base.InvokeCoreAsync(arguments, cancellationToken).ConfigureAwait(false); @@ -44,4 +32,37 @@ internal sealed class SentryInstrumentedFunction(AIFunction innerFunction, ChatO throw; } } + + private ISpan InitToolSpan(AIFunctionArguments arguments) + { + const string operation = "gen_ai.execute_tool"; + var spanName = $"execute_tool {Name}"; + ISpan currSpan; + + if (arguments.TryGetValue(SentryAIConstants.KeyMessageFunctionArgumentDictKey, + out var keyMessage) + && keyMessage is ChatMessage message + && SentryChatClient.GetMessageToSpanDict(options).TryGetValue(message, out var agentSpan)) + { + currSpan = agentSpan.StartChild(operation, spanName); + } + else + { + // If we couldn't find the agent span, just attach it to the hub's current scope + currSpan = Hub.StartSpan(operation, spanName); + } + + currSpan.SetData("gen_ai.request.model", options?.ModelId); + currSpan.SetData("gen_ai.operation.name", "execute_tool"); + currSpan.SetData("gen_ai.tool.name", Name); + currSpan.SetData("gen_ai.tool.description", Description); + currSpan.SetData("gen_ai.tool.input", arguments); + + return currSpan; + } + + private static void RemoveSentryArgs(ref AIFunctionArguments arguments) + { + arguments.Remove(SentryAIConstants.KeyMessageFunctionArgumentDictKey); + } } From b139b0f3faac9cbbaf3d015eafadf96e2d1a296b Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Tue, 28 Oct 2025 15:20:39 -0400 Subject: [PATCH 21/22] fix tests --- .../SentryAIExtensionsTests.cs | 6 +++-- .../SentryChatClientTests.cs | 12 ++++----- .../SentryInstrumentedFunctionTests.cs | 27 ++++++++++++------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs b/test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs index d82b168f7e..a9525fda7c 100644 --- a/test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs +++ b/test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs @@ -67,7 +67,8 @@ public void WithSentry_ChatOptions_DoesNotDoubleWrapSentryInstrumentedFunction() { // Arrange var mockFunction = Substitute.For(); - var alreadyInstrumentedFunction = new SentryInstrumentedFunction(mockFunction); + var mockOption = Substitute.For(); + var alreadyInstrumentedFunction = new SentryInstrumentedFunction(mockFunction, mockOption); var options = new ChatOptions { @@ -93,7 +94,8 @@ public void WithSentry_ChatOptions_HandlesMultipleFunctions() var mockFunction2 = Substitute.For(); mockFunction2.Name.Returns("Function2"); - var alreadyInstrumentedFunction = new SentryInstrumentedFunction(mockFunction1); + var mockOption1 = Substitute.For(); + var alreadyInstrumentedFunction = new SentryInstrumentedFunction(mockFunction1, mockOption1); var options = new ChatOptions { diff --git a/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs b/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs index 1bfa250aaa..3cd5d0e9d7 100644 --- a/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs +++ b/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs @@ -1,6 +1,5 @@ #nullable enable using Microsoft.Extensions.AI; -using Sentry.Extensions.AI; namespace Sentry.Extensions.AI.Tests; @@ -20,7 +19,8 @@ public async Task CompleteAsync_CallsInnerClient() var res = await sentryChatClient.GetResponseAsync([new ChatMessage(ChatRole.User, "hi")], null); Assert.Equal([message], res.Messages); - await inner.Received(1).GetResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()); + await inner.Received(1).GetResponseAsync(Arg.Any>(), Arg.Any(), + Arg.Any()); } [Fact] @@ -28,7 +28,8 @@ public async Task CompleteStreamingAsync_CallsInnerClient() { var inner = Substitute.For(); - inner.GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + inner.GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), + Arg.Any()) .Returns(CreateTestStreamingUpdatesAsync()); var client = new SentryChatClient(inner); @@ -43,7 +44,8 @@ public async Task CompleteStreamingAsync_CallsInnerClient() Assert.Equal("Hello", results[0].Text); Assert.Equal(" World!", results[1].Text); - inner.Received(1).GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()); + inner.Received(1).GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), + Arg.Any()); } private static async IAsyncEnumerable CreateTestStreamingUpdatesAsync() @@ -53,5 +55,3 @@ private static async IAsyncEnumerable CreateTestStreamingUpd yield return new ChatResponseUpdate(ChatRole.System, " World!"); } } - - diff --git a/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs b/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs index 82e368fbca..ccc6e8c781 100644 --- a/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs +++ b/test/Sentry.Extensions.AI.Tests/SentryInstrumentedFunctionTests.cs @@ -14,7 +14,8 @@ public async Task InvokeCoreAsync_WithValidFunction_ReturnsResult() // Arrange using var sentryDisposable = SentryHelpers.InitializeSdk(); var testFunction = AIFunctionFactory.Create(() => "test result", "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments(); // Act @@ -41,7 +42,8 @@ public async Task InvokeCoreAsync_WithNullResult_ReturnsNull() // Arrange using var sentryDisposable = SentryHelpers.InitializeSdk(); var testFunction = AIFunctionFactory.Create(object? () => null, "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments(); // Act @@ -66,7 +68,8 @@ public async Task InvokeCoreAsync_WithJsonNullResult_ReturnsJsonElement() using var sentryDisposable = SentryHelpers.InitializeSdk(); var jsonNullElement = JsonSerializer.Deserialize("null"); var testFunction = AIFunctionFactory.Create(() => jsonNullElement, "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments(); // Act @@ -86,7 +89,8 @@ public async Task InvokeCoreAsync_WithJsonElementResult_CallsToStringForSpanOutp using var sentryDisposable = SentryHelpers.InitializeSdk(); var jsonElement = JsonSerializer.Deserialize("\"test output\""); var testFunction = AIFunctionFactory.Create(() => jsonElement, "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments(); // Act @@ -109,7 +113,8 @@ public async Task InvokeCoreAsync_WithComplexResult_ReturnsObject() using var sentryDisposable = SentryHelpers.InitializeSdk(); var resultObject = new { message = "test", count = 42 }; var testFunction = AIFunctionFactory.Create(() => resultObject, "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments(); // Act @@ -138,7 +143,8 @@ public async Task InvokeCoreAsync_WhenFunctionThrows_PropagatesException() using var sentryDisposable = SentryHelpers.InitializeSdk(); var expectedException = new InvalidOperationException("Test exception"); var testFunction = AIFunctionFactory.Create(new Func(() => throw expectedException), "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments(); // Act & Assert @@ -159,7 +165,8 @@ public async Task InvokeCoreAsync_WithCancellation_PropagatesCancellation() return "result"; }, "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments(); var cts = new CancellationTokenSource(); cts.Cancel(); @@ -181,7 +188,8 @@ public async Task InvokeCoreAsync_WithParameters_PassesParametersCorrectly() return "result"; }, "TestFunction", "Test function description"); - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); var arguments = new AIFunctionArguments { ["param1"] = "value1" }; // Act @@ -199,7 +207,8 @@ public void Constructor_PreservesInnerFunctionProperties() var testFunction = AIFunctionFactory.Create(() => "test", "TestFunction", "Test function description"); // Act - var sentryFunction = new SentryInstrumentedFunction(testFunction); + var mockOption = Substitute.For(); + var sentryFunction = new SentryInstrumentedFunction(testFunction, mockOption); // Assert Assert.Equal("TestFunction", sentryFunction.Name); From 717d191668f92f6a8f9907bcc8c5b74db6f04375 Mon Sep 17 00:00:00 2001 From: Alex Sohn Date: Tue, 28 Oct 2025 15:47:17 -0400 Subject: [PATCH 22/22] cleanup SentryChatClient --- src/Sentry.Extensions.AI/SentryChatClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sentry.Extensions.AI/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs index dc405e3a71..7d66bc2a0d 100644 --- a/src/Sentry.Extensions.AI/SentryChatClient.cs +++ b/src/Sentry.Extensions.AI/SentryChatClient.cs @@ -109,7 +109,7 @@ public override async IAsyncEnumerable GetStreamingResponseA } else if (current?.FinishReason == ChatFinishReason.ToolCalls) { - WrapFunctionCallsInResponse(current, keyMessage); + InjectMessageToFunctionCallArguments(current, keyMessage); } yield break; @@ -176,7 +176,7 @@ internal static ConcurrentDictionary GetMessageToSpanDict(Ch return agentSpanDict; } - options.AdditionalProperties = new AdditionalPropertiesDictionary(); + options.AdditionalProperties ??= new AdditionalPropertiesDictionary(); options.AdditionalProperties.TryAdd(SentryAIConstants.OptionsAdditionalAttributeAgentSpanName, agentSpanDict); return agentSpanDict; } @@ -209,7 +209,7 @@ private static void WrapFunctionCallsInResponse(ChatResponse response, ChatMessa } } - private static void WrapFunctionCallsInResponse(ChatResponseUpdate response, ChatMessage keyMessage) + private static void InjectMessageToFunctionCallArguments(ChatResponseUpdate response, ChatMessage keyMessage) { foreach (var content in response.Contents) {