-
Notifications
You must be signed in to change notification settings - Fork 23
feat: Add tracking to multi-provider #612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
askpt
wants to merge
8
commits into
main
Choose a base branch
from
askpt/issue550
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1084e14
feat: Add tracking event handling to MultiProvider and evaluation str…
askpt c770ec9
feat: Add validation for tracking event name in MultiProvider
askpt 8f63c8f
feat: Update ShouldTrackWithThisProvider to support generic provider …
askpt eeec55e
feat: Enhance TestProvider with tracking invocation methods and event…
askpt c62192a
feat: Add comprehensive tracking tests for MultiProvider functionality
askpt 3a748a5
refactor: Clarify tracking context handling and rename test for inval…
askpt 8a51ae8
refactor: Change TrackingInvocation from record to class for enhanced…
askpt 51e7adb
fix: Change log level to Error for tracking event errors in MultiProv…
askpt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
299 changes: 299 additions & 0 deletions
299
test/OpenFeature.Providers.MultiProvider.Tests/MultiProviderTrackingTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,299 @@ | ||
| using NSubstitute; | ||
| using OpenFeature.Constant; | ||
| using OpenFeature.Model; | ||
| using OpenFeature.Providers.MultiProvider.Models; | ||
| using OpenFeature.Providers.MultiProvider.Strategies; | ||
| using OpenFeature.Providers.MultiProvider.Strategies.Models; | ||
| using OpenFeature.Providers.MultiProvider.Tests.Utils; | ||
|
|
||
| namespace OpenFeature.Providers.MultiProvider.Tests; | ||
|
|
||
| public class MultiProviderTrackingTests | ||
| { | ||
| private const string TestTrackingEventName = "test-event"; | ||
| private const string Provider1Name = "provider1"; | ||
| private const string Provider2Name = "provider2"; | ||
| private const string Provider3Name = "provider3"; | ||
|
|
||
| private readonly TestProvider _testProvider1 = new(Provider1Name); | ||
| private readonly TestProvider _testProvider2 = new(Provider2Name); | ||
| private readonly TestProvider _testProvider3 = new(Provider3Name); | ||
| private readonly EvaluationContext _evaluationContext = EvaluationContext.Builder().Build(); | ||
|
|
||
| [Fact] | ||
| public async Task Track_WithMultipleReadyProviders_CallsTrackOnAllReadyProviders() | ||
| { | ||
| // Arrange | ||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name), | ||
| new(this._testProvider2, Provider2Name), | ||
| new(this._testProvider3, Provider3Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy()); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
|
|
||
| var trackingDetails = TrackingEventDetails.Builder().SetValue(99.99).Build(); | ||
|
|
||
| // Act | ||
| multiProvider.Track(TestTrackingEventName, this._evaluationContext, trackingDetails); | ||
|
|
||
| // Assert | ||
| var provider1Invocations = this._testProvider1.GetTrackingInvocations(); | ||
| var provider2Invocations = this._testProvider2.GetTrackingInvocations(); | ||
| var provider3Invocations = this._testProvider3.GetTrackingInvocations(); | ||
|
|
||
| Assert.Single(provider1Invocations); | ||
| Assert.Single(provider2Invocations); | ||
| Assert.Single(provider3Invocations); | ||
|
|
||
| Assert.Equal(TestTrackingEventName, provider1Invocations[0].EventName); | ||
| Assert.Equal(TestTrackingEventName, provider2Invocations[0].EventName); | ||
| Assert.Equal(TestTrackingEventName, provider3Invocations[0].EventName); | ||
|
|
||
| Assert.Equal(trackingDetails.Value, provider1Invocations[0].TrackingEventDetails?.Value); | ||
| Assert.Equal(trackingDetails.Value, provider2Invocations[0].TrackingEventDetails?.Value); | ||
| Assert.Equal(trackingDetails.Value, provider3Invocations[0].TrackingEventDetails?.Value); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Track_WithNullEvaluationContext_CallsTrackWithNullContext() | ||
| { | ||
| // Arrange | ||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name), | ||
| new(this._testProvider2, Provider2Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy()); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
|
|
||
| // Act | ||
| multiProvider.Track(TestTrackingEventName); | ||
|
|
||
| // Assert | ||
| var provider1Invocations = this._testProvider1.GetTrackingInvocations(); | ||
| var provider2Invocations = this._testProvider2.GetTrackingInvocations(); | ||
|
|
||
| Assert.Single(provider1Invocations); | ||
| Assert.Single(provider2Invocations); | ||
|
|
||
| Assert.Equal(TestTrackingEventName, provider1Invocations[0].EventName); | ||
| Assert.Equal(TestTrackingEventName, provider2Invocations[0].EventName); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Track_WithNullTrackingDetails_CallsTrackWithNullDetails() | ||
| { | ||
| // Arrange | ||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name), | ||
| new(this._testProvider2, Provider2Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy()); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
|
|
||
| // Act | ||
| multiProvider.Track(TestTrackingEventName, this._evaluationContext); | ||
|
|
||
| // Assert | ||
| var provider1Invocations = this._testProvider1.GetTrackingInvocations(); | ||
| var provider2Invocations = this._testProvider2.GetTrackingInvocations(); | ||
|
|
||
| Assert.Single(provider1Invocations); | ||
| Assert.Single(provider2Invocations); | ||
|
|
||
| Assert.Equal(TestTrackingEventName, provider1Invocations[0].EventName); | ||
| Assert.Null(provider1Invocations[0].TrackingEventDetails); | ||
|
|
||
| Assert.Equal(TestTrackingEventName, provider2Invocations[0].EventName); | ||
| Assert.Null(provider2Invocations[0].TrackingEventDetails); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Track_WhenProviderThrowsException_ContinuesWithOtherProviders() | ||
| { | ||
| // Arrange | ||
| var throwingProvider = Substitute.For<FeatureProvider>(); | ||
| throwingProvider.GetMetadata().Returns(new Metadata(Provider2Name)); | ||
| throwingProvider.When(x => x.Track(Arg.Any<string>(), Arg.Any<EvaluationContext>(), Arg.Any<TrackingEventDetails>())) | ||
| .Do(_ => throw new InvalidOperationException("Test exception")); | ||
|
|
||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name), | ||
| new(throwingProvider, Provider2Name), | ||
| new(this._testProvider3, Provider3Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy()); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
|
|
||
| // Manually set all providers to Ready status | ||
| throwingProvider.Status.Returns(ProviderStatus.Ready); | ||
|
|
||
| var trackingDetails = TrackingEventDetails.Builder().SetValue(99.99).Build(); | ||
|
|
||
| // Act | ||
| multiProvider.Track(TestTrackingEventName, this._evaluationContext, trackingDetails); | ||
|
|
||
| // Assert - should not throw and should continue with other providers | ||
| var provider1Invocations = this._testProvider1.GetTrackingInvocations(); | ||
| var provider3Invocations = this._testProvider3.GetTrackingInvocations(); | ||
|
|
||
| Assert.Single(provider1Invocations); | ||
| Assert.Single(provider3Invocations); | ||
|
|
||
| throwingProvider.Received(1).Track(TestTrackingEventName, Arg.Any<EvaluationContext>(), trackingDetails); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Track_WhenDisposed_ThrowsObjectDisposedException() | ||
| { | ||
| // Arrange | ||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy()); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
| await multiProvider.DisposeAsync(); | ||
|
|
||
| // Act & Assert | ||
| Assert.Throws<ObjectDisposedException>(() => multiProvider.Track(TestTrackingEventName, this._evaluationContext)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Track_WithCustomStrategy_RespectsStrategyDecision() | ||
| { | ||
| // Arrange | ||
| var customStrategy = Substitute.For<BaseEvaluationStrategy>(); | ||
| customStrategy.RunMode.Returns(RunMode.Sequential); | ||
|
|
||
| // Only allow tracking with the first provider | ||
| customStrategy.ShouldTrackWithThisProvider( | ||
| Arg.Is<StrategyPerProviderContext<object>>(ctx => ctx.ProviderName == Provider1Name), | ||
| Arg.Any<EvaluationContext>(), | ||
| Arg.Any<string>(), | ||
| Arg.Any<TrackingEventDetails>() | ||
| ).Returns(true); | ||
|
|
||
| customStrategy.ShouldTrackWithThisProvider( | ||
| Arg.Is<StrategyPerProviderContext<object>>(ctx => ctx.ProviderName != Provider1Name), | ||
| Arg.Any<EvaluationContext>(), | ||
| Arg.Any<string>(), | ||
| Arg.Any<TrackingEventDetails>() | ||
| ).Returns(false); | ||
|
|
||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name), | ||
| new(this._testProvider2, Provider2Name), | ||
| new(this._testProvider3, Provider3Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, customStrategy); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
|
|
||
| var trackingDetails = TrackingEventDetails.Builder().SetValue(99.99).Build(); | ||
|
|
||
| // Act | ||
| multiProvider.Track(TestTrackingEventName, this._evaluationContext, trackingDetails); | ||
|
|
||
| // Assert - only provider1 should receive the tracking call | ||
| var provider1Invocations = this._testProvider1.GetTrackingInvocations(); | ||
| var provider2Invocations = this._testProvider2.GetTrackingInvocations(); | ||
| var provider3Invocations = this._testProvider3.GetTrackingInvocations(); | ||
|
|
||
| Assert.Single(provider1Invocations); | ||
| Assert.Empty(provider2Invocations); | ||
| Assert.Empty(provider3Invocations); | ||
|
|
||
| customStrategy.Received(3).ShouldTrackWithThisProvider( | ||
| Arg.Any<StrategyPerProviderContext<object>>(), | ||
| Arg.Any<EvaluationContext>(), | ||
| TestTrackingEventName, | ||
| trackingDetails | ||
| ); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Track_WithComplexTrackingDetails_PropagatesAllDetails() | ||
| { | ||
| // Arrange | ||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name), | ||
| new(this._testProvider2, Provider2Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy()); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
|
|
||
| var trackingDetails = TrackingEventDetails.Builder() | ||
| .SetValue(199.99) | ||
| .Set("currency", new Value("USD")) | ||
| .Set("productId", new Value("prod-123")) | ||
| .Set("quantity", new Value(5)) | ||
| .Build(); | ||
|
|
||
| // Act | ||
| multiProvider.Track(TestTrackingEventName, this._evaluationContext, trackingDetails); | ||
|
|
||
| // Assert | ||
| var provider1Invocations = this._testProvider1.GetTrackingInvocations(); | ||
| var provider2Invocations = this._testProvider2.GetTrackingInvocations(); | ||
|
|
||
| Assert.Single(provider1Invocations); | ||
| Assert.Single(provider2Invocations); | ||
|
|
||
| var details1 = provider1Invocations[0].TrackingEventDetails; | ||
| var details2 = provider2Invocations[0].TrackingEventDetails; | ||
|
|
||
| Assert.NotNull(details1); | ||
| Assert.NotNull(details2); | ||
|
|
||
| Assert.Equal(199.99, details1.Value); | ||
| Assert.Equal(199.99, details2.Value); | ||
|
|
||
| Assert.Equal("USD", details1.GetValue("currency").AsString); | ||
| Assert.Equal("USD", details2.GetValue("currency").AsString); | ||
|
|
||
| Assert.Equal("prod-123", details1.GetValue("productId").AsString); | ||
| Assert.Equal("prod-123", details2.GetValue("productId").AsString); | ||
|
|
||
| Assert.Equal(5, details1.GetValue("quantity").AsInteger); | ||
| Assert.Equal(5, details2.GetValue("quantity").AsInteger); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(null)] | ||
| [InlineData("")] | ||
| [InlineData(" ")] | ||
| public async Task Track_WhenInvalidTrackingEventName_DoesNotCallProviders(string? trackingEventName) | ||
| { | ||
| // Arrange | ||
| var providerEntries = new List<ProviderEntry> | ||
| { | ||
| new(this._testProvider1, Provider1Name), | ||
| new(this._testProvider2, Provider2Name) | ||
| }; | ||
|
|
||
| var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy()); | ||
| await multiProvider.InitializeAsync(this._evaluationContext); | ||
|
|
||
| // Act & Assert | ||
| multiProvider.Track(trackingEventName!, this._evaluationContext, TrackingEventDetails.Empty); | ||
|
|
||
| var provider1Invocations = this._testProvider1.GetTrackingInvocations(); | ||
| var provider2Invocations = this._testProvider2.GetTrackingInvocations(); | ||
|
|
||
| Assert.Empty(provider1Invocations); | ||
| Assert.Empty(provider2Invocations); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.