From 7287521fe846c4462d09b776ca85feb792b58ac4 Mon Sep 17 00:00:00 2001 From: Jens Henneberg Date: Fri, 8 Mar 2024 11:49:20 +1300 Subject: [PATCH 1/4] Minimal Statsig provider for OpenFeature Signed-off-by: Jens Henneberg --- .github/component_owners.yml | 4 + .release-please-manifest.json | 3 +- DotnetSdkContrib.sln | 14 +++ .../EvaluationContextExtensions.cs | 70 +++++++++++ ...enFeature.Contrib.Providers.Statsig.csproj | 29 +++++ .../README.md | 96 +++++++++++++++ .../StatsigProvider.cs | 107 +++++++++++++++++ .../version.txt | 1 + .../EvaluationContextExtensionsTests.cs | 113 ++++++++++++++++++ ...ture.Contrib.Providers.Statsig.Test.csproj | 10 ++ .../StatsigProviderTest.cs | 71 +++++++++++ 11 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs create mode 100644 src/OpenFeature.Contrib.Providers.Statsig/OpenFeature.Contrib.Providers.Statsig.csproj create mode 100644 src/OpenFeature.Contrib.Providers.Statsig/README.md create mode 100644 src/OpenFeature.Contrib.Providers.Statsig/StatsigProvider.cs create mode 100644 src/OpenFeature.Contrib.Providers.Statsig/version.txt create mode 100644 test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs create mode 100644 test/OpenFeature.Contrib.Providers.Statsig.Test/OpenFeature.Contrib.Providers.Statsig.Test.csproj create mode 100644 test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs diff --git a/.github/component_owners.yml b/.github/component_owners.yml index ec79e018..eaed7942 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -18,6 +18,8 @@ components: src/OpenFeature.Contrib.Providers.FeatureManagement: - ericpattison - toddbaert + src/OpenFeature.Contrib.Providers.Statsig: + - jenshenneberg # test/ test/OpenFeature.Contrib.Hooks.Otel.Test: @@ -37,6 +39,8 @@ components: test/OpenFeature.Contrib.Providers.FeatureManagement.Test: - ericpattison - toddbaert + test/src/OpenFeature.Contrib.Providers.Statsig.Test: + - jenshenneberg ignored-authors: - renovate-bot diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3c8fc3fe..ea82e80a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,5 +4,6 @@ "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.1.5", "src/OpenFeature.Contrib.Providers.Flagsmith": "0.1.5", "src/OpenFeature.Contrib.Providers.ConfigCat": "0.0.2", - "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.0.1" + "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.0.1", + "src/OpenFeature.Contrib.Providers.Statsig": "0.0.1" } \ No newline at end of file diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln index 21d89cdb..2c8566d1 100644 --- a/DotnetSdkContrib.sln +++ b/DotnetSdkContrib.sln @@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest", "test\OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest\OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj", "{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig", "src\OpenFeature.Contrib.Providers.Statsig\OpenFeature.Contrib.Providers.Statsig.csproj", "{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig.Test", "test\OpenFeature.Contrib.Providers.Statsig.Test\OpenFeature.Contrib.Providers.Statsig.Test.csproj", "{F3080350-B0AB-4D59-B416-50CC38C99087}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -103,6 +107,14 @@ Global {B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.Build.0 = Release|Any CPU + {4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Release|Any CPU.Build.0 = Release|Any CPU + {F3080350-B0AB-4D59-B416-50CC38C99087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3080350-B0AB-4D59-B416-50CC38C99087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,5 +135,7 @@ Global {4A2C6E0F-8A23-454F-8019-AE3DD91AA193} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} {2ACD9150-A8F4-450E-B49A-C628895992BF} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} {B8C5376B-BAFE-48B8-ABC1-111A93C033F2} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} + {4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} + {F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} EndGlobalSection EndGlobal diff --git a/src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs b/src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs new file mode 100644 index 00000000..535b3fd0 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs @@ -0,0 +1,70 @@ +using OpenFeature.Model; +using Statsig; + +namespace OpenFeature.Contrib.Providers.Statsig +{ + internal static class EvaluationContextExtensions + { + //These keys match the keys of the statsiguser object as descibed here + //https://docs.statsig.com/client/concepts/user + internal const string CONTEXT_APP_VERSION = "appVersion"; + internal const string CONTEXT_COUNTRY = "country"; + internal const string CONTEXT_EMAIL = "email"; + internal const string CONTEXT_IP = "ip"; + internal const string CONTEXT_LOCALE = "locale"; + internal const string CONTEXT_USER_AGENT = "userAgent"; + internal const string CONTEXT_PRIVATE_ATTRIBUTES = "privateAttributes"; + + public static StatsigUser AsStatsigUser(this EvaluationContext evaluationContext) + { + if (evaluationContext == null) + return null; + + var user = new StatsigUser(); + foreach (var item in evaluationContext) + { + //TODO: Await release containing this https://github.com/open-feature/dotnet-sdk/pull/231 to use TargetingKey instead of UserId + if (item.Key.ToUpperInvariant() == "USERID") + user.UserID = item.Value.AsString; + + else + switch (item.Key) + { + case CONTEXT_APP_VERSION: + user.AppVersion = item.Value.AsString; + break; + case CONTEXT_COUNTRY: + user.Country = item.Value.AsString; + break; + case CONTEXT_EMAIL: + user.Email = item.Value.AsString; + break; + case CONTEXT_IP: + user.IPAddress = item.Value.AsString; + break; + case CONTEXT_USER_AGENT: + user.UserAgent = item.Value.AsString; + break; + case CONTEXT_LOCALE: + user.Locale = item.Value.AsString; + break; + case CONTEXT_PRIVATE_ATTRIBUTES: + if (item.Value.IsStructure) + { + var privateAttributes = item.Value.AsStructure; + foreach (var items in privateAttributes) + { + user.AddPrivateAttribute(items.Key, items.Value); + } + } + break; + + default: + user.AddCustomProperty(item.Key, item.Value.AsObject); + break; + } + } + return user; + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.Statsig/OpenFeature.Contrib.Providers.Statsig.csproj b/src/OpenFeature.Contrib.Providers.Statsig/OpenFeature.Contrib.Providers.Statsig.csproj new file mode 100644 index 00000000..c77c1812 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Statsig/OpenFeature.Contrib.Providers.Statsig.csproj @@ -0,0 +1,29 @@ + + + + OpenFeature.Contrib.Provider.Statsig + 0.0.1 + $(VersionNumber) + preview + $(VersionNumber) + $(VersionNumber) + Statsig provider for .NET + README.md + Jens Kjær Henneberg + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + + + + + + + + + + diff --git a/src/OpenFeature.Contrib.Providers.Statsig/README.md b/src/OpenFeature.Contrib.Providers.Statsig/README.md new file mode 100644 index 00000000..60d996b1 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Statsig/README.md @@ -0,0 +1,96 @@ +# Statsig Feature Flag .NET Provider + +The Statsig Flag provider allows you to connect to Statsig. Please note this is a minimal implementation - only `ResolveBooleanValue` is implemented. + +# .Net SDK usage + +## Install dependencies + +The first things we will do is install the **Open Feature SDK** and the **Statsig Feature Flag provider**. + +### .NET Cli +```shell +dotnet add package OpenFeature.Contrib.Providers.Statsig +``` +### Package Manager + +```shell +NuGet\Install-Package OpenFeature.Contrib.Providers.Statsig +``` +### Package Reference + +```xml + +``` +### Packet cli + +```shell +paket add OpenFeature.Contrib.Providers.Statsig +``` + +### Cake + +```shell +// Install OpenFeature.Contrib.Providers.Statsig as a Cake Addin +#addin nuget:?package=OpenFeature.Contrib.Providers.Statsig + +// Install OpenFeature.Contrib.Providers.Statsig as a Cake Tool +#tool nuget:?package=OpenFeature.Contrib.Providers.Statsig +``` + +## Using the Statsig Provider with the OpenFeature SDK + +The following example shows how to use the Statsig provider with the OpenFeature SDK. + +```csharp +using OpenFeature; +using OpenFeature.Contrib.Providers.Statsig; +using System; + +StatsigProvider statsigProvider = new StatsigProvider("#YOUR-SDK-KEY#"); + +// Set the statsigProvider as the provider for the OpenFeature SDK +await Api.Instance.SetProviderAsync(statsigProvider); + +IFeatureClient client = OpenFeature.Api.Instance.GetClient(); + +bool isMyAwesomeFeatureEnabled = await client.GetBooleanValue("isMyAwesomeFeatureEnabled", false); + +if (isMyAwesomeFeatureEnabled) +{ + Console.WriteLine("New Feature enabled!"); +} + +``` + +### Customizing the Statsig Provider + +The Statsig provider can be customized by passing a `Action` object to the constructor. + +```csharp +var statsigProvider = new StatsigProvider("#YOUR-SDK-KEY#", options => options.LocalMode = true); +``` + +For a full list of options see the [Statsig documentation](https://docs.statsig.com/server/dotnetSDK#statsig-options). + +## EvaluationContext and Statsig User relationship + +Statsig has the concept of a [StatsigUser](https://docs.statsig.com/client/concepts/user) where you can evaluate a flag based on properties. The OpenFeature SDK has the concept of an EvaluationContext which is a dictionary of string keys and values. The Statsig provider will map the EvaluationContext to a StatsigUser. + +The following parameters are mapped to the corresponding Statsig pre-defined parameters + +| EvaluationContext Key | Statsig User Parameter | +|-----------------------|---------------------------| +| `appVersion` | `AppVersion` | +| `country` | `Country` | +| `email` | `Email` | +| `ip` | `Ip` | +| `locale` | `Locale` | +| `userAgent` | `UserAgent` | +| `privateAttributes` | `PrivateAttributes` | + +## Known issues and limitations +- Only `ResolveBooleanValue` implemented for now + +- Gate BooleanEvaluation with default value true cannot fallback to true. + https://github.com/statsig-io/dotnet-sdk/issues/33 diff --git a/src/OpenFeature.Contrib.Providers.Statsig/StatsigProvider.cs b/src/OpenFeature.Contrib.Providers.Statsig/StatsigProvider.cs new file mode 100644 index 00000000..3287302e --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Statsig/StatsigProvider.cs @@ -0,0 +1,107 @@ +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; +using Statsig; +using Statsig.Server; +using System; +using System.Threading.Tasks; + +namespace OpenFeature.Contrib.Providers.Statsig +{ + /// + /// An OpenFeature which enables the use of the Statsig Server-Side SDK for .NET + /// with OpenFeature. + /// + /// + /// var provider = new StatsigProvider("my-sdk-key"), new StatsigProviderOptions(){LocalMode = false}); + /// + /// OpenFeature.Api.Instance.SetProvider(provider); + /// + /// var client = OpenFeature.Api.Instance.GetClient(); + /// + public sealed class StatsigProvider : FeatureProvider + { + volatile bool initialized = false; + private readonly Metadata _providerMetadata = new Metadata("Statsig provider"); + private readonly string _sdkKey = "secret-"; //Dummy sdk key that works with local mode + private readonly StatsigServerOptions _options; + internal readonly ServerDriver ServerDriver; + + /// + /// Creates new instance of + /// + /// SDK Key to access Statsig. + /// The action used to configure the client. + public StatsigProvider(string sdkKey = null, Action configurationAction = null) + { + if (sdkKey != null) + { + _sdkKey = sdkKey; + } + _options = new StatsigServerOptions(); + configurationAction?.Invoke(_options); + ServerDriver = new ServerDriver(_sdkKey, _options); + } + + /// + public override Metadata GetMetadata() => _providerMetadata; + + /// + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + { + //TODO: defaultvalue = true not yet supported due to https://github.com/statsig-io/dotnet-sdk/issues/33 + if (defaultValue == true) + throw new FeatureProviderException(ErrorType.General, "defaultvalue = true not supported (https://github.com/statsig-io/dotnet-sdk/issues/33)"); + if (GetStatus() != ProviderStatus.Ready) + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, ErrorType.ProviderNotReady)); + var result = ServerDriver.CheckGateSync(context.AsStatsigUser(), flagKey); + return Task.FromResult(new ResolutionDetails(flagKey, result)); + } + + /// + public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) + { + throw new NotImplementedException(); + } + + /// + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + { + throw new NotImplementedException(); + } + + /// + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + { + throw new NotImplementedException(); + } + + /// + public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) + { + throw new NotImplementedException(); + } + + /// + public override ProviderStatus GetStatus() + { + return initialized ? ProviderStatus.Ready : ProviderStatus.NotReady; + } + + /// + public override async Task Initialize(EvaluationContext context) + { + var initResult = await ServerDriver.Initialize(); + if (initResult == InitializeResult.Success || initResult == InitializeResult.LocalMode || initResult == InitializeResult.AlreadyInitialized) + { + initialized = true; + } + } + + /// + public override Task Shutdown() + { + return ServerDriver.Shutdown(); + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.Statsig/version.txt b/src/OpenFeature.Contrib.Providers.Statsig/version.txt new file mode 100644 index 00000000..8a9ecc2e --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Statsig/version.txt @@ -0,0 +1 @@ +0.0.1 \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs b/test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs new file mode 100644 index 00000000..fa019b9a --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs @@ -0,0 +1,113 @@ +using AutoFixture.Xunit2; +using OpenFeature.Model; +using System.Collections.Generic; +using Xunit; + +namespace OpenFeature.Contrib.Providers.Statsig.Test +{ + public class EvaluationContextExtensionsTests + { + [Theory] + [AutoData] + public void AsStatsigUser_ShouldMapUserIdSuccessfully(string userId) + { + // Arrange + var evaluationContext = EvaluationContext.Builder().Set("UserID", userId).Build(); + + // Act + var statsigUser = evaluationContext.AsStatsigUser(); + + // Assert + Assert.NotNull(statsigUser); + Assert.Equal(userId, statsigUser.UserID); + } + + [Theory] + [AutoData] + public void AsStatsigUser_ShouldMapString(string key, string value) + { + // Arrange + var evaluationContext = EvaluationContext.Builder().Set(key, value).Build(); + + // Act + var statsigUser = evaluationContext.AsStatsigUser(); + + // Assert + Assert.NotNull(statsigUser); + Assert.True(statsigUser.CustomProperties.TryGetValue(key, out var mappedValue)); + Assert.Equal(value, mappedValue as string); + } + + + [Theory] + [AutoData] + public void AsStatsigUser_ShouldMapStatsigProperties(string appVersion, string country, string email, string ipAddress, string locale, string userAgent) + { + // Arrange + var evaluationContext = EvaluationContext.Builder() + .Set(EvaluationContextExtensions.CONTEXT_APP_VERSION, appVersion) + .Set(EvaluationContextExtensions.CONTEXT_COUNTRY, country) + .Set(EvaluationContextExtensions.CONTEXT_EMAIL, email) + .Set(EvaluationContextExtensions.CONTEXT_IP, ipAddress) + .Set(EvaluationContextExtensions.CONTEXT_LOCALE, locale) + .Set(EvaluationContextExtensions.CONTEXT_USER_AGENT, userAgent).Build(); + + // Act + var statsigUser = evaluationContext.AsStatsigUser(); + + // Assert + Assert.NotNull(statsigUser); + Assert.Equal(statsigUser.AppVersion, appVersion); + Assert.Equal(statsigUser.Country, country); + Assert.Equal(statsigUser.Email, email); + Assert.Equal(statsigUser.IPAddress, ipAddress); + Assert.Equal(statsigUser.Locale, locale); + Assert.Equal(statsigUser.UserAgent, userAgent); + } + + [Theory] + [AutoData] + public void AsStatsigUser_ShouldMapPrivateData(string key, string value) + { + var privateProperties = new Dictionary() { { key, new Value(value) } }; + + // Arrange + var evaluationContext = EvaluationContext.Builder().Set(EvaluationContextExtensions.CONTEXT_PRIVATE_ATTRIBUTES, new Structure(privateProperties)).Build(); + + // Act + var statsigUser = evaluationContext.AsStatsigUser(); + + // Assert + Assert.NotNull(statsigUser); + Assert.True(statsigUser.PrivateAttributes.TryGetValue(key, out var mappedValue)); + Assert.Equal(value, (mappedValue as Value)?.AsString); + } + + [Fact] + public void AsStatsigUser_ShouldHandleNullEvaluationContext() + { + // Arrange + EvaluationContext evaluationContext = null; + + // Act + var statsigUser = evaluationContext.AsStatsigUser(); + + // Assert + Assert.Null(statsigUser); + } + + [Fact] + public void AsStatsigUser_ShouldHandleEmptyUserID() + { + // Arrange + var evaluationContext = EvaluationContext.Builder().Set("UserID", "").Build(); + + // Act + var statsigUser = evaluationContext.AsStatsigUser(); + + // Assert + Assert.NotNull(statsigUser); + Assert.Equal("", statsigUser.UserID); + } + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Statsig.Test/OpenFeature.Contrib.Providers.Statsig.Test.csproj b/test/OpenFeature.Contrib.Providers.Statsig.Test/OpenFeature.Contrib.Providers.Statsig.Test.csproj new file mode 100644 index 00000000..ccfd29a0 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Statsig.Test/OpenFeature.Contrib.Providers.Statsig.Test.csproj @@ -0,0 +1,10 @@ + + + latest + + + + + + + \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs b/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs new file mode 100644 index 00000000..66d82de9 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs @@ -0,0 +1,71 @@ +using AutoFixture.Xunit2; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; +using System.Threading.Tasks; +using Xunit; +namespace OpenFeature.Contrib.Providers.Statsig.Test; + +public class StatsigProviderTest +{ + private StatsigProvider statsigProvider; + + public StatsigProviderTest() + { + statsigProvider = new StatsigProvider("secret-", x => x.LocalMode = true); + } + + [Fact] + public async Task StatsigProvider_Initialized_HasCorrectStatusAsync() + { + Assert.Equal(ProviderStatus.NotReady, statsigProvider.GetStatus()); + await statsigProvider.Initialize(null); + Assert.Equal(ProviderStatus.Ready, statsigProvider.GetStatus()); + } + + [Theory] + [InlineAutoData(true, true)] + [InlineAutoData(false, false)] + public async Task GetBooleanValue_ForFeatureWithContext(bool flagValue, bool expectedValue, string userId, string flagName) + { + await statsigProvider.Initialize(null); + var ec = EvaluationContext.Builder().Set("UserID", userId).Build(); + statsigProvider.ServerDriver.OverrideGate(flagName, flagValue, userId); + Assert.Equal(expectedValue, statsigProvider.ResolveBooleanValue(flagName, false, ec).Result.Value); + } + + [Theory] + [InlineAutoData(true, false)] + [InlineAutoData(false, false)] + public async Task GetBooleanValue_ForFeatureWithNoContext_ReturnsFalse(bool flagValue, bool expectedValue, string flagName) + { + await statsigProvider.Initialize(null); + statsigProvider.ServerDriver.OverrideGate(flagName, flagValue); + Assert.Equal(expectedValue, statsigProvider.ResolveBooleanValue(flagName, false).Result.Value); + } + + [Theory] + [AutoData] + public async Task GetBooleanValue_ForFeatureWithDefaultTrue_ThrowsException(string flagName) + { + await statsigProvider.Initialize(null); + Assert.ThrowsAny(() => statsigProvider.ResolveBooleanValue(flagName, true).Result.Value); + } + + [Fact] + public async Task TestConcurrentOperation() + { + // Arrange + var concurrencyTestClass = new StatsigProvider(); + const int numberOfThreads = 50; + + // Act + var tasks = new Task[numberOfThreads]; + for (int i = 0; i < numberOfThreads; i++) + { + tasks[i] = concurrencyTestClass.Initialize(null); + } + + await Task.WhenAll(tasks); + } +} From c4957103a8629066b14c82ded4e61a5f51f5124a Mon Sep 17 00:00:00 2001 From: Jens Henneberg Date: Fri, 8 Mar 2024 11:56:47 +1300 Subject: [PATCH 2/4] Cleanup of one test Signed-off-by: Jens Henneberg --- .../StatsigProviderTest.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs b/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs index 66d82de9..75705324 100644 --- a/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs +++ b/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs @@ -28,9 +28,12 @@ public async Task StatsigProvider_Initialized_HasCorrectStatusAsync() [InlineAutoData(false, false)] public async Task GetBooleanValue_ForFeatureWithContext(bool flagValue, bool expectedValue, string userId, string flagName) { + // Arrange await statsigProvider.Initialize(null); var ec = EvaluationContext.Builder().Set("UserID", userId).Build(); statsigProvider.ServerDriver.OverrideGate(flagName, flagValue, userId); + + // Act & Assert Assert.Equal(expectedValue, statsigProvider.ResolveBooleanValue(flagName, false, ec).Result.Value); } @@ -39,8 +42,11 @@ public async Task GetBooleanValue_ForFeatureWithContext(bool flagValue, bool exp [InlineAutoData(false, false)] public async Task GetBooleanValue_ForFeatureWithNoContext_ReturnsFalse(bool flagValue, bool expectedValue, string flagName) { + // Arrange await statsigProvider.Initialize(null); statsigProvider.ServerDriver.OverrideGate(flagName, flagValue); + + // Act & Assert Assert.Equal(expectedValue, statsigProvider.ResolveBooleanValue(flagName, false).Result.Value); } @@ -48,18 +54,21 @@ public async Task GetBooleanValue_ForFeatureWithNoContext_ReturnsFalse(bool flag [AutoData] public async Task GetBooleanValue_ForFeatureWithDefaultTrue_ThrowsException(string flagName) { + // Arrange await statsigProvider.Initialize(null); + + // Act & Assert Assert.ThrowsAny(() => statsigProvider.ResolveBooleanValue(flagName, true).Result.Value); } [Fact] - public async Task TestConcurrentOperation() + public async Task TestConcurrentInitilization_DoesntThrowException() { // Arrange var concurrencyTestClass = new StatsigProvider(); const int numberOfThreads = 50; - // Act + // Act & Assert var tasks = new Task[numberOfThreads]; for (int i = 0; i < numberOfThreads; i++) { From 902cfb5f7de7c70b79a73fcec13c21108a3a1d3a Mon Sep 17 00:00:00 2001 From: Jens Henneberg Date: Thu, 14 Mar 2024 09:28:24 +1300 Subject: [PATCH 3/4] Updated OpenFeature to version 1.5.* Signed-off-by: Jens Henneberg --- build/Common.props | 44 ++++++------ .../EvaluationContextExtensions.cs | 71 +++++++++---------- .../EvaluationContextExtensionsTests.cs | 4 +- .../StatsigProviderTest.cs | 2 +- 4 files changed, 58 insertions(+), 63 deletions(-) diff --git a/build/Common.props b/build/Common.props index ef35f745..c97c4d0b 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,33 +1,33 @@ - - - + + + - - 7.3 - true - + + 7.3 + true + - - full - true - + + full + true + - - true - + + true + - - - - [1.4,) - + + [1.5,) + - - - + + + diff --git a/src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs b/src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs index 535b3fd0..4c1495ab 100644 --- a/src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs +++ b/src/OpenFeature.Contrib.Providers.Statsig/EvaluationContextExtensions.cs @@ -20,49 +20,44 @@ public static StatsigUser AsStatsigUser(this EvaluationContext evaluationContext if (evaluationContext == null) return null; - var user = new StatsigUser(); + var user = new StatsigUser() { UserID = evaluationContext.TargetingKey }; foreach (var item in evaluationContext) { - //TODO: Await release containing this https://github.com/open-feature/dotnet-sdk/pull/231 to use TargetingKey instead of UserId - if (item.Key.ToUpperInvariant() == "USERID") - user.UserID = item.Value.AsString; - - else - switch (item.Key) - { - case CONTEXT_APP_VERSION: - user.AppVersion = item.Value.AsString; - break; - case CONTEXT_COUNTRY: - user.Country = item.Value.AsString; - break; - case CONTEXT_EMAIL: - user.Email = item.Value.AsString; - break; - case CONTEXT_IP: - user.IPAddress = item.Value.AsString; - break; - case CONTEXT_USER_AGENT: - user.UserAgent = item.Value.AsString; - break; - case CONTEXT_LOCALE: - user.Locale = item.Value.AsString; - break; - case CONTEXT_PRIVATE_ATTRIBUTES: - if (item.Value.IsStructure) + switch (item.Key) + { + case CONTEXT_APP_VERSION: + user.AppVersion = item.Value.AsString; + break; + case CONTEXT_COUNTRY: + user.Country = item.Value.AsString; + break; + case CONTEXT_EMAIL: + user.Email = item.Value.AsString; + break; + case CONTEXT_IP: + user.IPAddress = item.Value.AsString; + break; + case CONTEXT_USER_AGENT: + user.UserAgent = item.Value.AsString; + break; + case CONTEXT_LOCALE: + user.Locale = item.Value.AsString; + break; + case CONTEXT_PRIVATE_ATTRIBUTES: + if (item.Value.IsStructure) + { + var privateAttributes = item.Value.AsStructure; + foreach (var items in privateAttributes) { - var privateAttributes = item.Value.AsStructure; - foreach (var items in privateAttributes) - { - user.AddPrivateAttribute(items.Key, items.Value); - } + user.AddPrivateAttribute(items.Key, items.Value); } - break; + } + break; - default: - user.AddCustomProperty(item.Key, item.Value.AsObject); - break; - } + default: + user.AddCustomProperty(item.Key, item.Value.AsObject); + break; + } } return user; } diff --git a/test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs b/test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs index fa019b9a..6e678f83 100644 --- a/test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs +++ b/test/OpenFeature.Contrib.Providers.Statsig.Test/EvaluationContextExtensionsTests.cs @@ -12,7 +12,7 @@ public class EvaluationContextExtensionsTests public void AsStatsigUser_ShouldMapUserIdSuccessfully(string userId) { // Arrange - var evaluationContext = EvaluationContext.Builder().Set("UserID", userId).Build(); + var evaluationContext = EvaluationContext.Builder().SetTargetingKey(userId).Build(); // Act var statsigUser = evaluationContext.AsStatsigUser(); @@ -100,7 +100,7 @@ public void AsStatsigUser_ShouldHandleNullEvaluationContext() public void AsStatsigUser_ShouldHandleEmptyUserID() { // Arrange - var evaluationContext = EvaluationContext.Builder().Set("UserID", "").Build(); + var evaluationContext = EvaluationContext.Builder().SetTargetingKey("").Build(); // Act var statsigUser = evaluationContext.AsStatsigUser(); diff --git a/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs b/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs index 75705324..e08ef74e 100644 --- a/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs +++ b/test/OpenFeature.Contrib.Providers.Statsig.Test/StatsigProviderTest.cs @@ -30,7 +30,7 @@ public async Task GetBooleanValue_ForFeatureWithContext(bool flagValue, bool exp { // Arrange await statsigProvider.Initialize(null); - var ec = EvaluationContext.Builder().Set("UserID", userId).Build(); + var ec = EvaluationContext.Builder().SetTargetingKey(userId).Build(); statsigProvider.ServerDriver.OverrideGate(flagName, flagValue, userId); // Act & Assert From 972672ce9b24488f504049897b96cb6ef6b28b53 Mon Sep 17 00:00:00 2001 From: Jens Henneberg Date: Thu, 14 Mar 2024 09:40:20 +1300 Subject: [PATCH 4/4] Fix formatting of Common.props Signed-off-by: Jens Henneberg --- build/Common.props | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/build/Common.props b/build/Common.props index c97c4d0b..52e8ce0f 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,33 +1,33 @@ - - - + + + - - 7.3 - true - + + 7.3 + true + - - full - true - + + full + true + - - true - + + true + - - - - [1.5,) - + + [1.5,) + - - - - + + + + \ No newline at end of file