diff --git a/.cursor/sdk_development.mdc b/.cursor/sdk_development.mdc
new file mode 100644
index 0000000000..38796d8c51
--- /dev/null
+++ b/.cursor/sdk_development.mdc
@@ -0,0 +1,218 @@
+# Sentry .NET SDK Development Guidelines
+
+This document contains important patterns, conventions, and guidelines learned during SDK development to ensure consistency and avoid common pitfalls.
+
+## Project Structure & Conventions
+
+### Project Organization
+- **Source packages**: `src/Sentry.Extensions.*`
+- **Tests**: `test/Sentry.Extensions.*.Tests`
+- **Samples**: `samples/Sentry.Samples.ME.*` (ME = Microsoft.Extensions)
+- **Solution file**: `Sentry.sln` in root
+
+### Naming Conventions
+- Extension packages for packages targeting Microsoft.Extensions.Technology: `Sentry.Extensions.{Technology}`
+- Sample projects for packages targeting Microsoft.Extensions.Technology: `Sentry.Samples.ME.{Technology}.{ProjectType}`
+- Test projects: `{SourceProject}.Tests`
+
+## Directory.Build.props Pattern
+
+### Central Package Management
+- Some packages are managed centrally in `Directory.Build.props` files
+- Check existing projects of the same type (src/test/samples) for reference
+
+### Hierarchical Structure
+```
+/Directory.Build.props # Root level
+/src/Directory.Build.props # Source projects
+/test/Directory.Build.props # Test projects
+/samples/Directory.Build.props # Sample projects
+```
+
+### Key Properties Set Centrally
+- `TargetFrameworks`
+- Common package references
+- Build configurations
+- Analyzer references
+
+## Sample Project Guidelines
+
+### DSN Configuration Pattern
+**Always use conditional compilation for DSN in samples:**
+
+```csharp
+builder.AddSentry(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
+
+ // Other configuration...
+});
+```
+
+### Required Sample Files
+- **Don't exclude** `SamplesShared.cs` or `EnvironmentVariables.g.cs`
+- These provide proper DSN handling for CI/CD environments
+- The build system automatically defines `SENTRY_DSN_DEFINED_IN_ENV` when environment variable is set
+
+### Building and Running Samples
+```bash
+# Build (requires SENTRY_DSN environment variable)
+env SENTRY_DSN=your_dsn dotnet build samples/Project/Project.csproj
+
+# Run (requires SENTRY_DSN environment variable)
+env SENTRY_DSN=your_dsn dotnet run --project samples/Project/Project.csproj
+```
+
+### Sample Project Structure
+Base sample projects on existing ones like `Sentry.Samples.ME.Logging`:
+- Target the same TFM (such as `net9.0`) as other projects, for consistency
+- Follow the same logging and configuration patterns
+
+## Extension Package Guidelines
+
+### Target Frameworks
+- Use `net9.0;net8.0` for new extension packages (or whatever is being used on other projects)
+- Follow existing patterns in similar extensions
+
+### Package References
+- Check `Directory.Build.props` for centrally managed versions
+- Avoid duplicating dependencies already brought in transitively
+- Use exact versions for preview packages (e.g., `Microsoft.Extensions.AI`)
+
+### Internal Visibility
+```xml
+
+
+
+```
+
+### Hub Access Pattern
+**Always use automatic hub detection:**
+```csharp
+// ✅ Good - Automatic hub detection
+public static IChatClient WithSentry(this IChatClient client, string? agentName = null)
+{
+ return new SentryChatClient(client, HubAdapter.Instance, agentName);
+}
+
+// ✅ Good - DI fallback pattern
+public static ChatClientBuilder UseSentry(this ChatClientBuilder builder, string? agentName = null)
+{
+ return builder.Use((serviceProvider, inner) =>
+ {
+ var hub = serviceProvider.GetService() ?? HubAdapter.Instance;
+ return new SentryChatClient(inner, hub, agentName);
+ });
+}
+
+// ❌ Avoid - Requiring manual hub passing, unless it's for testing so it can be injected. Then use a `internal` overload ctor.
+public static IChatClient WithSentry(this IChatClient client, IHub hub, string? agentName = null)
+```
+
+## Span and Transaction Guidelines
+
+### Exception Handling in Spans
+**Always pass exception reference to Finish():**
+```csharp
+try
+{
+ var result = await operation();
+ transaction.Finish(SpanStatus.Ok);
+ return result;
+}
+catch (Exception ex)
+{
+ transaction.Finish(ex); // ✅ Pass exception reference
+ _hub.CaptureException(ex);
+ throw;
+}
+```
+
+
+## Testing Guidelines
+
+### Test Project Setup
+- Target same frameworks as source project
+- Reference source project, core Sentry project, and `Sentry.Testing`
+- Stay consist with other test projects, for example use `NSubstitute` for mocking
+
+### Test Structure
+```csharp
+[Fact]
+public async Task Operation_CallsInnerClient()
+{
+ // Arrange
+ var inner = Substitute.For();
+ var hub = Substitute.For();
+ var client = new YourClient(inner, hub);
+
+ // Act & Assert
+ await client.Method();
+ inner.Received(1).Method();
+}
+```
+
+### Async Enumerable Testing
+```csharp
+private static async IAsyncEnumerable CreateTestEnumerable()
+{
+ yield return item1;
+ await Task.Yield(); // Make it actually async
+ yield return item2;
+}
+```
+
+## Build and Test Commands
+
+### Standard Build Commands
+```bash
+# Build entire solution
+dotnet build Sentry.sln
+
+# Build specific project
+dotnet build src/ProjectName/ProjectName.csproj
+
+# Run tests
+dotnet test test/ProjectName.Tests/ProjectName.Tests.csproj
+
+# Build samples (with environment variable)
+env SENTRY_DSN=placeholder dotnet build samples/SampleProject/SampleProject.csproj
+```
+
+### Target Framework Testing
+- Always test on both target frameworks (net8.0, net9.0)
+- CI builds will validate against all supported frameworks
+
+## Common Pitfalls to Avoid
+
+1. **Don't exclude shared sample files** - They provide proper DSN handling
+3. **Don't require manual hub passing** - Use automatic detection
+4. **Don't use `SpanStatus.InternalError`** - Pass exception reference
+5. **Don't forget `[EnumeratorCancellation]`** - For async enumerable parameters
+6. **Don't use `yield return` in try-catch** - Create wrapper classes instead
+
+## File Organization
+
+### Extension Structure
+```
+src/Sentry.Extensions.Technology/
+├── Sentry.Extensions.Technology.csproj
+├── Extensions/
+│ └── TechnologyExtensions.cs
+└── Internal/
+ ├── SentryWrapper.cs
+ └── SentryEnumerable.cs (if needed)
+```
+
+### Test Structure
+```
+test/Sentry.Extensions.Technology.Tests/
+├── Sentry.Extensions.Technology.Tests.csproj
+└── WrapperTests.cs
+```
+
+This document should be updated as new patterns emerge and conventions evolve.
\ No newline at end of file
diff --git a/.generated.NoMobile.sln b/.generated.NoMobile.sln
index 3730377a46..d06cfc4124 100644
--- a/.generated.NoMobile.sln
+++ b/.generated.NoMobile.sln
@@ -209,6 +209,12 @@ 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.Extensions.AI", "src\Sentry.Extensions.AI\Sentry.Extensions.AI.csproj", "{7FBB62AE-ED72-4746-A437-77F9718F7E2A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Extensions.AI.Tests", "test\Sentry.Extensions.AI.Tests\Sentry.Extensions.AI.Tests.csproj", "{0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.ME.AI.Console", "samples\Sentry.Samples.ME.AI.Console\Sentry.Samples.ME.AI.Console.csproj", "{421B0105-CA96-43AA-A7EC-B8C4A4366B9B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1252,6 +1258,42 @@ 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
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x64.Build.0 = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x86.Build.0 = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x64.ActiveCfg = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x64.Build.0 = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x86.ActiveCfg = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x86.Build.0 = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x64.Build.0 = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x86.Build.0 = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x64.ActiveCfg = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x64.Build.0 = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x86.ActiveCfg = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x86.Build.0 = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x64.Build.0 = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x86.Build.0 = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x64.ActiveCfg = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x64.Build.0 = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x86.ActiveCfg = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1343,5 +1385,8 @@ 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}
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A} = {230B9384-90FD-4551-A5DE-1A5C197F25B6}
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7} = {230B9384-90FD-4551-A5DE-1A5C197F25B6}
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B} = {21B42F60-5802-404E-90F0-AEBCC56760C0}
EndGlobalSection
EndGlobal
diff --git a/Sentry-CI-Build-Linux-NoMobile.slnf b/Sentry-CI-Build-Linux-NoMobile.slnf
index f29fd0a74e..97fc0e498e 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.Console\\Sentry.Samples.AI.Console.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..4752159c73 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.Console\\Sentry.Samples.AI.Console.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 c1f6bd735d..e7408b5427 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.Console\\Sentry.Samples.AI.Console.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",
@@ -61,6 +64,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 d5960f2212..f090004d30 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.Console\\Sentry.Samples.AI.Console.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",
@@ -64,6 +67,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..d9899b2780 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.Console\\Sentry.Samples.AI.Console.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..d06cfc4124 100644
--- a/Sentry.sln
+++ b/Sentry.sln
@@ -209,6 +209,12 @@ 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.Extensions.AI", "src\Sentry.Extensions.AI\Sentry.Extensions.AI.csproj", "{7FBB62AE-ED72-4746-A437-77F9718F7E2A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Extensions.AI.Tests", "test\Sentry.Extensions.AI.Tests\Sentry.Extensions.AI.Tests.csproj", "{0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.ME.AI.Console", "samples\Sentry.Samples.ME.AI.Console\Sentry.Samples.ME.AI.Console.csproj", "{421B0105-CA96-43AA-A7EC-B8C4A4366B9B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1252,6 +1258,42 @@ 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
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x64.Build.0 = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Debug|x86.Build.0 = Debug|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x64.ActiveCfg = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x64.Build.0 = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x86.ActiveCfg = Release|Any CPU
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A}.Release|x86.Build.0 = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x64.Build.0 = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Debug|x86.Build.0 = Debug|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x64.ActiveCfg = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x64.Build.0 = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x86.ActiveCfg = Release|Any CPU
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7}.Release|x86.Build.0 = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x64.Build.0 = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Debug|x86.Build.0 = Debug|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x64.ActiveCfg = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x64.Build.0 = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x86.ActiveCfg = Release|Any CPU
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1343,5 +1385,8 @@ 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}
+ {7FBB62AE-ED72-4746-A437-77F9718F7E2A} = {230B9384-90FD-4551-A5DE-1A5C197F25B6}
+ {0013A6AD-D37E-489D-AFBA-53BB3AB8A8C7} = {230B9384-90FD-4551-A5DE-1A5C197F25B6}
+ {421B0105-CA96-43AA-A7EC-B8C4A4366B9B} = {21B42F60-5802-404E-90F0-AEBCC56760C0}
EndGlobalSection
EndGlobal
diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf
index 1592f77697..fda0eb06bf 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.Console\\Sentry.Samples.AI.Console.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.Console/Program.cs b/samples/Sentry.Samples.ME.AI.Console/Program.cs
new file mode 100644
index 0000000000..3a6614ab61
--- /dev/null
+++ b/samples/Sentry.Samples.ME.AI.Console/Program.cs
@@ -0,0 +1,86 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Sentry.Extensions.AI;
+
+using var loggerFactory = LoggerFactory.Create(builder =>
+{
+ builder.AddConsole();
+ builder.AddSentry(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 = false;
+
+ // Enable performance monitoring for AI traces, 1.0 = 100%
+ options.TracesSampleRate = 1.0;
+ });
+});
+var logger = loggerFactory.CreateLogger();
+
+logger.LogInformation("Starting Microsoft.Extensions.AI sample with Sentry instrumentation");
+
+// Create a simple echo client and wrap it with Sentry instrumentation
+var echoClient = new EchoChatClient();
+var chat = echoClient.WithSentry(agentName: "ME.AI Sample Agent", model: "gpt-4o-mini", system: "openai");
+
+logger.LogInformation("Making AI call with Sentry instrumentation...");
+
+var response = await chat.CompleteAsync(new[]
+{
+ new ChatMessage(ChatRole.User, "Say hello from Sentry sample")
+});
+
+logger.LogInformation("Response: {ResponseText}", response.Message.Text);
+
+// Demonstrate streaming with Sentry instrumentation
+logger.LogInformation("Making streaming AI call with Sentry instrumentation...");
+
+var streamingResponse = new List();
+await foreach (var update in chat.CompleteStreamingAsync(new[]
+{
+ 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.");
+
+// Simple echo client for demonstration
+public class EchoChatClient : IChatClient
+{
+ public ChatClientMetadata Metadata => new("echo-client");
+
+ public Task CompleteAsync(IList messages, ChatOptions options = null, CancellationToken cancellationToken = default)
+ {
+ var lastMessage = messages.LastOrDefault()?.Text ?? "Hello from echo client!";
+ var responseMessage = new ChatMessage(ChatRole.Assistant, $"Echo: {lastMessage}");
+ return Task.FromResult(new ChatCompletion(responseMessage));
+ }
+
+ public async IAsyncEnumerable CompleteStreamingAsync(IList messages, ChatOptions options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var lastMessage = messages.LastOrDefault()?.Text ?? "Hello from echo client!";
+ var parts = new[] { "Echo: ", lastMessage.Substring(0, Math.Min(10, lastMessage.Length)), "...", " (streaming)" };
+
+ foreach (var part in parts)
+ {
+ await Task.Delay(100, cancellationToken); // Simulate streaming delay
+ yield return new StreamingChatCompletionUpdate { Text = part };
+ }
+ }
+
+ public TService GetService(object key = null) where TService : class => null;
+ public void Dispose() { }
+}
+
+
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..dba7d7fbff
--- /dev/null
+++ b/samples/Sentry.Samples.ME.AI.Console/Sentry.Samples.ME.AI.Console.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net8.0
+
+
+
+
+
+
+
+
+ sentry-sdks
+ sentry-dotnet
+ true
+ true
+
+
+
+
+
+
+
+
+
diff --git a/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs b/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs
index 77ad1d5797..3bbfc298dc 100644
--- a/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs
+++ b/src/Sentry.Bindings.Cocoa/ApiDefinitions.cs
@@ -27,6 +27,7 @@ namespace Sentry.CocoaSdk;
// typedef SentryEvent * _Nullable (^SentryBeforeSendEventCallback)(SentryEvent * _Nonnull);
[Internal]
+[return: NullAllowed]
delegate SentryEvent SentryBeforeSendEventCallback (SentryEvent @event);
// typedef id _Nullable (^SentryBeforeSendSpanCallback)(id _Nonnull);
diff --git a/src/Sentry.Extensions.AI/Extensions/SentryAIBuilderExtensions.cs b/src/Sentry.Extensions.AI/Extensions/SentryAIBuilderExtensions.cs
new file mode 100644
index 0000000000..dcde9ed07c
--- /dev/null
+++ b/src/Sentry.Extensions.AI/Extensions/SentryAIBuilderExtensions.cs
@@ -0,0 +1,35 @@
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Sentry.Extensibility;
+
+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((serviceProvider, inner) =>
+ {
+ // Try to get IHub from DI first, fallback to HubAdapter.Instance
+ var hub = serviceProvider.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? model = null, string? system = null)
+ {
+ return new SentryChatClient(client, HubAdapter.Instance, agentName, model, 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..dbd26b9d7d
--- /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/SentryChatClient.cs b/src/Sentry.Extensions.AI/SentryChatClient.cs
new file mode 100644
index 0000000000..57e5685478
--- /dev/null
+++ b/src/Sentry.Extensions.AI/SentryChatClient.cs
@@ -0,0 +1,195 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Sentry;
+
+namespace Sentry.Extensions.AI;
+
+internal sealed class SentryChatClient : IChatClient
+{
+ private readonly IChatClient _innerClient;
+ private readonly IHub _hub;
+ private readonly string? _agentName;
+ private readonly string? _model;
+ private readonly string? _system;
+
+ public SentryChatClient(IChatClient innerClient, IHub hub, string? agentName = null, string? model = null, string? system = null)
+ {
+ _innerClient = innerClient;
+ _hub = hub;
+ _agentName = agentName;
+ _model = model;
+ _system = system;
+ }
+
+ public ChatClientMetadata Metadata => _innerClient.Metadata;
+
+ public async Task CompleteAsync(IList messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var operation = "gen_ai.invoke_agent";
+ var spanName = _agentName is { Length: > 0 } ? $"invoke_agent {_agentName}" : "invoke_agent";
+ var transaction = _hub.GetSpan()?.StartChild(operation, spanName) ?? _hub.StartTransaction(spanName, operation);
+
+ if (_system is { Length: > 0 })
+ {
+ transaction.SetTag("gen_ai.system", _system);
+ }
+
+ if (_model is { Length: > 0 })
+ {
+ transaction.SetTag("gen_ai.request.model", _model);
+ }
+
+ transaction.SetTag("gen_ai.operation.name", "invoke_agent");
+ if (_agentName is { Length: > 0 })
+ {
+ transaction.SetTag("gen_ai.agent.name", _agentName);
+ }
+
+ try
+ {
+ var response = await _innerClient.CompleteAsync(messages, options, cancellationToken).ConfigureAwait(false);
+ transaction.Finish(SpanStatus.Ok);
+ return response;
+ }
+ catch (Exception ex)
+ {
+ transaction.Finish(ex);
+ _hub.CaptureException(ex);
+ throw;
+ }
+ }
+
+ public IAsyncEnumerable CompleteStreamingAsync(IList messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return new SentryStreamingChatEnumerable(_innerClient.CompleteStreamingAsync(messages, options, cancellationToken), _hub, _agentName, _model, _system);
+ }
+
+ public TService? GetService(object? key = null) where TService : class
+ {
+ return _innerClient.GetService(key);
+ }
+
+ public void Dispose()
+ {
+ _innerClient?.Dispose();
+ }
+}
+
+internal sealed class SentryStreamingChatEnumerable : IAsyncEnumerable
+{
+ private readonly IAsyncEnumerable _innerEnumerable;
+ private readonly IHub _hub;
+ private readonly string? _agentName;
+ private readonly string? _model;
+ private readonly string? _system;
+
+ public SentryStreamingChatEnumerable(
+ IAsyncEnumerable innerEnumerable,
+ IHub hub,
+ string? agentName,
+ string? model,
+ string? system)
+ {
+ _innerEnumerable = innerEnumerable;
+ _hub = hub;
+ _agentName = agentName;
+ _model = model;
+ _system = system;
+ }
+
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ {
+ return new SentryStreamingChatEnumerator(_innerEnumerable.GetAsyncEnumerator(cancellationToken), _hub, _agentName, _model, _system);
+ }
+}
+
+internal sealed class SentryStreamingChatEnumerator : IAsyncEnumerator
+{
+ private readonly IAsyncEnumerator _innerEnumerator;
+ private readonly ISpan _transaction;
+ private readonly IHub _hub;
+ private bool _finished;
+
+ public SentryStreamingChatEnumerator(
+ IAsyncEnumerator innerEnumerator,
+ IHub hub,
+ string? agentName,
+ string? model,
+ string? system)
+ {
+ _innerEnumerator = innerEnumerator;
+ _hub = hub;
+
+ // Create the span/transaction
+ var operation = "gen_ai.invoke_agent";
+ var spanName = agentName is { Length: > 0 } ? $"invoke_agent {agentName}" : "invoke_agent";
+ _transaction = hub.GetSpan()?.StartChild(operation, spanName) ?? hub.StartTransaction(spanName, operation);
+
+ // Set the same tags as CompleteAsync
+ if (system is { Length: > 0 })
+ {
+ _transaction.SetTag("gen_ai.system", system);
+ }
+
+ if (model is { Length: > 0 })
+ {
+ _transaction.SetTag("gen_ai.request.model", model);
+ }
+
+ _transaction.SetTag("gen_ai.operation.name", "invoke_agent");
+ if (agentName is { Length: > 0 })
+ {
+ _transaction.SetTag("gen_ai.agent.name", agentName);
+ }
+
+ // Add streaming-specific tag
+ _transaction.SetTag("gen_ai.streaming", "true");
+ }
+
+ public StreamingChatCompletionUpdate Current => _innerEnumerator.Current;
+
+ public async ValueTask MoveNextAsync()
+ {
+ try
+ {
+ var hasNext = await _innerEnumerator.MoveNextAsync().ConfigureAwait(false);
+
+ if (!hasNext && !_finished)
+ {
+ _transaction.Finish(SpanStatus.Ok);
+ _finished = true;
+ }
+
+ return hasNext;
+ }
+ catch (Exception ex)
+ {
+ if (!_finished)
+ {
+ _transaction.Finish(ex);
+ _hub.CaptureException(ex);
+ _finished = true;
+ }
+ throw;
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ await _innerEnumerator.DisposeAsync().ConfigureAwait(false);
+ }
+ finally
+ {
+ if (!_finished)
+ {
+ _transaction.Finish(SpanStatus.Ok);
+ _finished = true;
+ }
+ }
+ }
+}
+
+
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..d6bec87995
--- /dev/null
+++ b/test/Sentry.Extensions.AI.Tests/Sentry.Extensions.AI.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+
+ 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..a608c52bf1
--- /dev/null
+++ b/test/Sentry.Extensions.AI.Tests/SentryChatClientTests.cs
@@ -0,0 +1,69 @@
+using Sentry.Extensions.AI;
+
+namespace Sentry.Extensions.AI.Tests;
+
+public class SentryChatClientTests
+{
+ [Fact]
+ public async Task CompleteAsync_CallsInnerClient()
+ {
+ var inner = Substitute.For();
+ var chatCompletion = new ChatCompletion(new ChatMessage(ChatRole.Assistant, "ok"));
+ inner.CompleteAsync(Arg.Any>(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(chatCompletion));
+
+ var hub = Substitute.For();
+ var client = new SentryChatClient(inner, hub, agentName: "Agent", model: "gpt-4o-mini", system: "openai");
+
+ var res = await client.CompleteAsync(new[] { new ChatMessage(ChatRole.User, "hi") }, null);
+
+ Assert.Equal("ok", res.Message.Text);
+ await inner.Received(1).CompleteAsync(Arg.Any>(), Arg.Any(), Arg.Any());
+ }
+
+ [Fact]
+ public void Metadata_ReturnsInnerClientMetadata()
+ {
+ var inner = Substitute.For();
+ var metadata = new ChatClientMetadata("test-client");
+ inner.Metadata.Returns(metadata);
+
+ var hub = Substitute.For();
+ var client = new SentryChatClient(inner, hub);
+
+ Assert.Equal(metadata, client.Metadata);
+ }
+
+ [Fact]
+ public async Task CompleteStreamingAsync_CallsInnerClient()
+ {
+ var inner = Substitute.For();
+
+ inner.CompleteStreamingAsync(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.CompleteStreamingAsync(new[] { 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).CompleteStreamingAsync(Arg.Any>(), Arg.Any(), Arg.Any());
+ }
+
+ private static async IAsyncEnumerable CreateTestStreamingUpdates()
+ {
+ yield return new StreamingChatCompletionUpdate { Text = "Hello" };
+ await Task.Yield(); // Make it actually async
+ yield return new StreamingChatCompletionUpdate { Text = " World" };
+ }
+}
+
+