Skip to content

Commit fc64d02

Browse files
Removed static dependencies
1 parent 8fcebc9 commit fc64d02

File tree

9 files changed

+122
-137
lines changed

9 files changed

+122
-137
lines changed

src/Sentry.Extensions.AI/Extensions/SentryAIExtensions.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@ public static ChatOptions AddSentryToolInstrumentation(this ChatOptions options)
5151
/// <param name="configure">The <see cref="SentryAIOptions"/> configuration</param>
5252
/// <returns>The instrumented <see cref="IChatClient"/></returns>
5353
[Experimental(DiagnosticId.ExperimentalFeature)]
54-
public static IChatClient AddSentry(this IChatClient client, Action<SentryAIOptions>? configure = null)
54+
public static IChatClient AddSentry(this IChatClient client, Action<SentryAIOptions>? configure = null) =>
55+
AddSentry(client, SentryAiActivityListener.Instance, configure);
56+
57+
/// <summary>
58+
/// Internal overload for testing
59+
/// </summary>
60+
internal static IChatClient AddSentry(this IChatClient client, ActivityListener listener, Action<SentryAIOptions>? configure = null)
5561
{
56-
// The constructor automatically adds the listener, so we can discard the instance
57-
_ = new SentryAiActivityListener();
62+
ActivitySource.AddActivityListener(listener);
5863
return new SentryChatClient(client, configure);
5964
}
6065
}

src/Sentry.Extensions.AI/SentryAIActivityListener.cs

Lines changed: 0 additions & 62 deletions
This file was deleted.

src/Sentry.Extensions.AI/SentryAIActivitySource.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ namespace Sentry.Extensions.AI;
55
/// <summary>Sentry's <see cref="ActivitySource"/> to be used in <see cref="IChatClient"/></summary>
66
internal static class SentryAIActivitySource
77
{
8-
/// <summary>Sentry's <see cref="ActivitySource"/> to be used in <see cref="IChatClient"/></summary>
9-
internal static ActivitySource Instance { get; } = new(SentryAIConstants.SentryActivitySourceName);
8+
internal const string SentryActivitySourceName = "Sentry.AgentMonitoring";
9+
10+
private static readonly Lazy<ActivitySource> LazyInstance = new(CreateSource);
11+
internal static ActivitySource Instance => LazyInstance.Value;
12+
13+
internal static ActivitySource CreateSource() => new(SentryActivitySourceName);
1014
}

src/Sentry.Extensions.AI/SentryAIConstants.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ internal static class SentryAIConstants
99
internal static readonly string[] FICCActivityNames =
1010
["orchestrate_tools", "FunctionInvokingChatClient.GetResponseAsync", "FunctionInvokingChatClient"];
1111

12-
/// <summary>
13-
/// The string we use to identify our <see cref="ActivitySource"/>.
14-
/// </summary>
15-
internal const string SentryActivitySourceName = "Sentry.AgentMonitoring";
16-
1712
/// <summary>
1813
/// The string we use to retrieve a Sentry span from the <see cref="Activity"/> using a Fused property
1914
/// </summary>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Sentry.Internal;
2+
3+
namespace Sentry.Extensions.AI;
4+
5+
/// <summary>
6+
/// Listens to FunctionInvokingChatClient's Activity
7+
/// </summary>
8+
internal static class SentryAiActivityListener
9+
{
10+
/// <summary>
11+
/// Singleton used outside of testing
12+
/// </summary>
13+
private static readonly Lazy<ActivityListener> LazyInstance = new(() => CreateListener());
14+
internal static readonly ActivityListener Instance = LazyInstance.Value;
15+
16+
/// <summary>
17+
/// Initializes Sentry's <see cref="ActivityListener"/> to tap into FunctionInvokingChatClient's Activity
18+
/// </summary>
19+
public static ActivityListener CreateListener(IHub? hub = null)
20+
{
21+
hub ??= HubAdapter.Instance;
22+
23+
var listener = new ActivityListener
24+
{
25+
ShouldListenTo = source => source.Name.StartsWith(SentryAIActivitySource.SentryActivitySourceName),
26+
Sample = (ref ActivityCreationOptions<ActivityContext> options) =>
27+
SentryAIConstants.FICCActivityNames.Contains(options.Name)
28+
? ActivitySamplingResult.AllDataAndRecorded
29+
: ActivitySamplingResult.None,
30+
ActivityStarted = activity =>
31+
{
32+
var agentSpan = hub.StartSpan(SentryAIConstants.SpanAttributes.InvokeAgentOperation,
33+
SentryAIConstants.SpanAttributes.InvokeAgentDescription);
34+
activity.SetFused(SentryAIConstants.SentryFICCSpanAttributeName, agentSpan);
35+
},
36+
ActivityStopped = activity =>
37+
{
38+
var agentSpan = activity.GetFused<ISpan>(SentryAIConstants.SentryFICCSpanAttributeName);
39+
// Don't pass in OK status in case there was an exception
40+
agentSpan?.Finish();
41+
}
42+
};
43+
ActivitySource.AddActivityListener(listener);
44+
return listener;
45+
}
46+
}

src/Sentry.Extensions.AI/SentryChatClient.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@ namespace Sentry.Extensions.AI;
44

55
internal sealed class SentryChatClient : DelegatingChatClient
66
{
7+
private readonly ActivitySource _activitySource;
78
private readonly HubAdapter _hub = HubAdapter.Instance;
89
private readonly SentryAIOptions _sentryAIOptions;
910

10-
public SentryChatClient(IChatClient client, Action<SentryAIOptions>? configure = null) : base(client)
11+
public SentryChatClient(IChatClient client, Action<SentryAIOptions>? configure = null) : this(null, client, configure)
1112
{
13+
}
14+
15+
/// <summary>
16+
/// Internal ovverride for testing
17+
/// </summary>
18+
internal SentryChatClient(ActivitySource? activitySource, IChatClient client, Action<SentryAIOptions>? configure = null) : base(client)
19+
{
20+
_activitySource = activitySource ?? SentryAIActivitySource.Instance;
1221
_sentryAIOptions = new SentryAIOptions();
1322
configure?.Invoke(_sentryAIOptions);
1423
}
@@ -102,9 +111,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
102111

103112
/// <inheritdoc/>
104113
public override object? GetService(Type serviceType, object? serviceKey = null) =>
105-
serviceType == typeof(ActivitySource)
106-
? SentryAIActivitySource.Instance
107-
: base.GetService(serviceType, serviceKey);
114+
serviceType == typeof(ActivitySource) ? _activitySource : base.GetService(serviceType, serviceKey);
108115

109116
private void AfterResponseCleanup(ISpan chatSpan, ISpan agentSpan, Exception? exception = null)
110117
{

test/Sentry.Extensions.AI.Tests/SentryAIActivityListenerTests.cs

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,25 @@ public Fixture()
1616

1717
private readonly Fixture _fixture = new();
1818

19-
public SentryAIActivityListenerTests()
20-
{
21-
// Dispose ActivityListener before each test, otherwise the singleton instance will persist between tests
22-
new SentryAiActivityListener().Dispose();
23-
}
24-
2519
[Fact]
2620
public void Init_AddsActivityListenerToActivitySource()
2721
{
22+
// Arrange
23+
var source = SentryAIActivitySource.CreateSource();
24+
2825
// Act
29-
_ = new SentryAiActivityListener();
26+
using var listener = SentryAiActivityListener.CreateListener(_fixture.Hub);
3027

3128
// Assert
32-
Assert.True(SentryAIActivitySource.Instance.HasListeners());
29+
Assert.True(source.HasListeners());
3330
}
3431

35-
[Theory]
36-
[InlineData(SentryAIConstants.SentryActivitySourceName)]
37-
public void ShouldListenTo_ReturnsTrueForSentryActivitySource(string sourceName)
32+
[Fact]
33+
public void ShouldListenTo_ReturnsTrueForSentryActivitySource()
3834
{
3935
// Arrange
40-
_ = new SentryAiActivityListener(_fixture.Hub);
36+
var sourceName = SentryAIActivitySource.SentryActivitySourceName;
37+
using var listener = SentryAiActivityListener.CreateListener(_fixture.Hub);
4138
var activitySource = new ActivitySource(sourceName);
4239

4340
// Act
@@ -57,7 +54,7 @@ public void ShouldListenTo_ReturnsTrueForSentryActivitySource(string sourceName)
5754
public void ShouldListenTo_ReturnsFalseForNonSentryActivitySource()
5855
{
5956
// Arrange
60-
_ = new SentryAiActivityListener(_fixture.Hub);
57+
using var listener = SentryAiActivityListener.CreateListener(_fixture.Hub);
6158
var activitySource = new ActivitySource("Other.ActivitySource");
6259

6360
// Act & Assert
@@ -69,15 +66,16 @@ public void ShouldListenTo_ReturnsFalseForNonSentryActivitySource()
6966
Arg.Any<IReadOnlyDictionary<string, object?>>());
7067
}
7168

72-
[Theory]
73-
[InlineData("orchestrate_tools")]
74-
public void Sample_ReturnsAllDataAndRecordedForFICCActivityNames(string activityName)
69+
[Fact]
70+
public void Sample_ReturnsAllDataAndRecordedForFICCActivityNames()
7571
{
7672
// Arrange
77-
_ = new SentryAiActivityListener(_fixture.Hub);
73+
var activityName = "orchestrate_tools";
74+
using var listener = SentryAiActivityListener.CreateListener(_fixture.Hub);
75+
var source = SentryAIActivitySource.CreateSource();
7876

7977
// Act
80-
using var activity = SentryAIActivitySource.Instance.StartActivity(activityName);
78+
using var activity = source.StartActivity(activityName);
8179

8280
// Assert
8381
Assert.NotNull(activity);
@@ -89,25 +87,4 @@ public void Sample_ReturnsAllDataAndRecordedForFICCActivityNames(string activity
8987
Arg.Any<ITransactionContext>(),
9088
Arg.Any<IReadOnlyDictionary<string, object?>>());
9189
}
92-
93-
[Fact]
94-
public void Init_MultipleCalls_NoDuplicateListener_StartsOnlyOneTransaction()
95-
{
96-
// Arrange
97-
_ = new SentryAiActivityListener(_fixture.Hub);
98-
_ = new SentryAiActivityListener(_fixture.Hub);
99-
_ = new SentryAiActivityListener(_fixture.Hub);
100-
101-
// Act
102-
using var activity = SentryAIActivitySource.Instance.StartActivity(SentryAIConstants.FICCActivityNames[0]);
103-
104-
// Assert
105-
Assert.NotNull(activity);
106-
Assert.True(SentryAIActivitySource.Instance.HasListeners());
107-
activity.Stop();
108-
109-
_fixture.Hub.Received(1).StartTransaction(
110-
Arg.Any<ITransactionContext>(),
111-
Arg.Any<IReadOnlyDictionary<string, object?>>());
112-
}
11390
}

test/Sentry.Extensions.AI.Tests/SentryAIExtensionsTests.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,27 @@ namespace Sentry.Extensions.AI.Tests;
55

66
public class SentryAIExtensionsTests
77
{
8+
private class Fixture
9+
{
10+
public IHub Hub { get; } = Substitute.For<IHub>();
11+
12+
public Fixture()
13+
{
14+
Hub.IsEnabled.Returns(true);
15+
}
16+
}
17+
18+
private readonly Fixture _fixture = new();
19+
820
[Fact]
921
public void WithSentry_IChatClient_ReturnsWrappedClient()
1022
{
1123
// Arrange
1224
var mockClient = Substitute.For<IChatClient>();
25+
using var listener = SentryAiActivityListener.CreateListener(_fixture.Hub);
1326

1427
// Act
15-
var result = mockClient.AddSentry();
28+
var result = mockClient.AddSentry(listener);
1629

1730
// Assert
1831
Assert.IsType<SentryChatClient>(result);
@@ -23,10 +36,11 @@ public void WithSentry_IChatClient_WithConfiguration_PassesConfigurationToWrappe
2336
{
2437
// Arrange
2538
var mockClient = Substitute.For<IChatClient>();
39+
using var listener = SentryAiActivityListener.CreateListener(_fixture.Hub);
2640
var configureWasCalled = false;
2741

2842
// Act
29-
var result = mockClient.AddSentry(options =>
43+
var result = mockClient.AddSentry(listener, options =>
3044
{
3145
configureWasCalled = true;
3246
options.Experimental.RecordInputs = false;
@@ -44,9 +58,10 @@ public void WithSentry_IChatClient_WithNullConfiguration_UsesDefaultConfiguratio
4458
{
4559
// Arrange
4660
var mockClient = Substitute.For<IChatClient>();
61+
using var listener = SentryAiActivityListener.CreateListener(_fixture.Hub);
4762

4863
// Act
49-
var result = mockClient.AddSentry(null);
64+
var result = mockClient.AddSentry(listener, null);
5065

5166
// Assert
5267
Assert.IsType<SentryChatClient>(result);

0 commit comments

Comments
 (0)