Skip to content

Commit 98028e9

Browse files
feat: Statsing provider (#163)
Signed-off-by: Jens Henneberg <[email protected]>
1 parent 533dfa6 commit 98028e9

12 files changed

+523
-3
lines changed

.github/component_owners.yml

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ components:
1818
src/OpenFeature.Contrib.Providers.FeatureManagement:
1919
- ericpattison
2020
- toddbaert
21+
src/OpenFeature.Contrib.Providers.Statsig:
22+
- jenshenneberg
2123

2224
# test/
2325
test/OpenFeature.Contrib.Hooks.Otel.Test:
@@ -37,6 +39,8 @@ components:
3739
test/OpenFeature.Contrib.Providers.FeatureManagement.Test:
3840
- ericpattison
3941
- toddbaert
42+
test/src/OpenFeature.Contrib.Providers.Statsig.Test:
43+
- jenshenneberg
4044

4145
ignored-authors:
4246
- renovate-bot

.release-please-manifest.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.1.5",
55
"src/OpenFeature.Contrib.Providers.Flagsmith": "0.1.5",
66
"src/OpenFeature.Contrib.Providers.ConfigCat": "0.0.2",
7-
"src/OpenFeature.Contrib.Providers.FeatureManagement": "0.0.1"
7+
"src/OpenFeature.Contrib.Providers.FeatureManagement": "0.0.1",
8+
"src/OpenFeature.Contrib.Providers.Statsig": "0.0.1"
89
}

DotnetSdkContrib.sln

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide
3737
EndProject
3838
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}"
3939
EndProject
40+
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}"
41+
EndProject
42+
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}"
43+
EndProject
4044
Global
4145
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4246
Debug|Any CPU = Debug|Any CPU
@@ -103,6 +107,14 @@ Global
103107
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
104108
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
105109
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.Build.0 = Release|Any CPU
110+
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
111+
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.Build.0 = Debug|Any CPU
112+
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Release|Any CPU.ActiveCfg = Release|Any CPU
113+
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Release|Any CPU.Build.0 = Release|Any CPU
114+
{F3080350-B0AB-4D59-B416-50CC38C99087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
115+
{F3080350-B0AB-4D59-B416-50CC38C99087}.Debug|Any CPU.Build.0 = Debug|Any CPU
116+
{F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.ActiveCfg = Release|Any CPU
117+
{F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.Build.0 = Release|Any CPU
106118
EndGlobalSection
107119
GlobalSection(SolutionProperties) = preSolution
108120
HideSolutionNode = FALSE
@@ -123,5 +135,7 @@ Global
123135
{4A2C6E0F-8A23-454F-8019-AE3DD91AA193} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
124136
{2ACD9150-A8F4-450E-B49A-C628895992BF} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
125137
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
138+
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
139+
{F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
126140
EndGlobalSection
127141
EndGlobal

build/Common.props

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax.
2525
-->
2626
<!-- 0.5+ -->
27-
<OpenFeatureVer>[1.4,)</OpenFeatureVer>
27+
<OpenFeatureVer>[1.5,)</OpenFeatureVer>
2828
</PropertyGroup>
2929

3030
<ItemGroup Condition="'$(OS)' == 'Unix'">
3131
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
3232
</ItemGroup>
33-
</Project>
33+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using OpenFeature.Model;
2+
using Statsig;
3+
4+
namespace OpenFeature.Contrib.Providers.Statsig
5+
{
6+
internal static class EvaluationContextExtensions
7+
{
8+
//These keys match the keys of the statsiguser object as descibed here
9+
//https://docs.statsig.com/client/concepts/user
10+
internal const string CONTEXT_APP_VERSION = "appVersion";
11+
internal const string CONTEXT_COUNTRY = "country";
12+
internal const string CONTEXT_EMAIL = "email";
13+
internal const string CONTEXT_IP = "ip";
14+
internal const string CONTEXT_LOCALE = "locale";
15+
internal const string CONTEXT_USER_AGENT = "userAgent";
16+
internal const string CONTEXT_PRIVATE_ATTRIBUTES = "privateAttributes";
17+
18+
public static StatsigUser AsStatsigUser(this EvaluationContext evaluationContext)
19+
{
20+
if (evaluationContext == null)
21+
return null;
22+
23+
var user = new StatsigUser() { UserID = evaluationContext.TargetingKey };
24+
foreach (var item in evaluationContext)
25+
{
26+
switch (item.Key)
27+
{
28+
case CONTEXT_APP_VERSION:
29+
user.AppVersion = item.Value.AsString;
30+
break;
31+
case CONTEXT_COUNTRY:
32+
user.Country = item.Value.AsString;
33+
break;
34+
case CONTEXT_EMAIL:
35+
user.Email = item.Value.AsString;
36+
break;
37+
case CONTEXT_IP:
38+
user.IPAddress = item.Value.AsString;
39+
break;
40+
case CONTEXT_USER_AGENT:
41+
user.UserAgent = item.Value.AsString;
42+
break;
43+
case CONTEXT_LOCALE:
44+
user.Locale = item.Value.AsString;
45+
break;
46+
case CONTEXT_PRIVATE_ATTRIBUTES:
47+
if (item.Value.IsStructure)
48+
{
49+
var privateAttributes = item.Value.AsStructure;
50+
foreach (var items in privateAttributes)
51+
{
52+
user.AddPrivateAttribute(items.Key, items.Value);
53+
}
54+
}
55+
break;
56+
57+
default:
58+
user.AddCustomProperty(item.Key, item.Value.AsObject);
59+
break;
60+
}
61+
}
62+
return user;
63+
}
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<PackageId>OpenFeature.Contrib.Provider.Statsig</PackageId>
5+
<VersionNumber>0.0.1</VersionNumber><!--x-release-please-version -->
6+
<VersionPrefix>$(VersionNumber)</VersionPrefix>
7+
<VersionSuffix>preview</VersionSuffix>
8+
<AssemblyVersion>$(VersionNumber)</AssemblyVersion>
9+
<FileVersion>$(VersionNumber)</FileVersion>
10+
<Description>Statsig provider for .NET</Description>
11+
<PackageReadmeFile>README.md</PackageReadmeFile>
12+
<Authors>Jens Kjær Henneberg</Authors>
13+
</PropertyGroup>
14+
<ItemGroup>
15+
<!-- make the internal methods visble to our test project -->
16+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
17+
<_Parameter1>$(MSBuildProjectName).Test</_Parameter1>
18+
</AssemblyAttribute>
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<PackageReference Include="Statsig" Version="1.23.1" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<None Include="README.md" Pack="true" PackagePath="\"/>
27+
</ItemGroup>
28+
29+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Statsig Feature Flag .NET Provider
2+
3+
The Statsig Flag provider allows you to connect to Statsig. Please note this is a minimal implementation - only `ResolveBooleanValue` is implemented.
4+
5+
# .Net SDK usage
6+
7+
## Install dependencies
8+
9+
The first things we will do is install the **Open Feature SDK** and the **Statsig Feature Flag provider**.
10+
11+
### .NET Cli
12+
```shell
13+
dotnet add package OpenFeature.Contrib.Providers.Statsig
14+
```
15+
### Package Manager
16+
17+
```shell
18+
NuGet\Install-Package OpenFeature.Contrib.Providers.Statsig
19+
```
20+
### Package Reference
21+
22+
```xml
23+
<PackageReference Include="OpenFeature.Contrib.Providers.Statsig" />
24+
```
25+
### Packet cli
26+
27+
```shell
28+
paket add OpenFeature.Contrib.Providers.Statsig
29+
```
30+
31+
### Cake
32+
33+
```shell
34+
// Install OpenFeature.Contrib.Providers.Statsig as a Cake Addin
35+
#addin nuget:?package=OpenFeature.Contrib.Providers.Statsig
36+
37+
// Install OpenFeature.Contrib.Providers.Statsig as a Cake Tool
38+
#tool nuget:?package=OpenFeature.Contrib.Providers.Statsig
39+
```
40+
41+
## Using the Statsig Provider with the OpenFeature SDK
42+
43+
The following example shows how to use the Statsig provider with the OpenFeature SDK.
44+
45+
```csharp
46+
using OpenFeature;
47+
using OpenFeature.Contrib.Providers.Statsig;
48+
using System;
49+
50+
StatsigProvider statsigProvider = new StatsigProvider("#YOUR-SDK-KEY#");
51+
52+
// Set the statsigProvider as the provider for the OpenFeature SDK
53+
await Api.Instance.SetProviderAsync(statsigProvider);
54+
55+
IFeatureClient client = OpenFeature.Api.Instance.GetClient();
56+
57+
bool isMyAwesomeFeatureEnabled = await client.GetBooleanValue("isMyAwesomeFeatureEnabled", false);
58+
59+
if (isMyAwesomeFeatureEnabled)
60+
{
61+
Console.WriteLine("New Feature enabled!");
62+
}
63+
64+
```
65+
66+
### Customizing the Statsig Provider
67+
68+
The Statsig provider can be customized by passing a `Action<StatsigServerOptions>` object to the constructor.
69+
70+
```csharp
71+
var statsigProvider = new StatsigProvider("#YOUR-SDK-KEY#", options => options.LocalMode = true);
72+
```
73+
74+
For a full list of options see the [Statsig documentation](https://docs.statsig.com/server/dotnetSDK#statsig-options).
75+
76+
## EvaluationContext and Statsig User relationship
77+
78+
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.
79+
80+
The following parameters are mapped to the corresponding Statsig pre-defined parameters
81+
82+
| EvaluationContext Key | Statsig User Parameter |
83+
|-----------------------|---------------------------|
84+
| `appVersion` | `AppVersion` |
85+
| `country` | `Country` |
86+
| `email` | `Email` |
87+
| `ip` | `Ip` |
88+
| `locale` | `Locale` |
89+
| `userAgent` | `UserAgent` |
90+
| `privateAttributes` | `PrivateAttributes` |
91+
92+
## Known issues and limitations
93+
- Only `ResolveBooleanValue` implemented for now
94+
95+
- Gate BooleanEvaluation with default value true cannot fallback to true.
96+
https://github.com/statsig-io/dotnet-sdk/issues/33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using OpenFeature.Constant;
2+
using OpenFeature.Error;
3+
using OpenFeature.Model;
4+
using Statsig;
5+
using Statsig.Server;
6+
using System;
7+
using System.Threading.Tasks;
8+
9+
namespace OpenFeature.Contrib.Providers.Statsig
10+
{
11+
/// <summary>
12+
/// An OpenFeature <see cref="FeatureProvider"/> which enables the use of the Statsig Server-Side SDK for .NET
13+
/// with OpenFeature.
14+
/// </summary>
15+
/// <example>
16+
/// var provider = new StatsigProvider("my-sdk-key"), new StatsigProviderOptions(){LocalMode = false});
17+
///
18+
/// OpenFeature.Api.Instance.SetProvider(provider);
19+
///
20+
/// var client = OpenFeature.Api.Instance.GetClient();
21+
/// </example>
22+
public sealed class StatsigProvider : FeatureProvider
23+
{
24+
volatile bool initialized = false;
25+
private readonly Metadata _providerMetadata = new Metadata("Statsig provider");
26+
private readonly string _sdkKey = "secret-"; //Dummy sdk key that works with local mode
27+
private readonly StatsigServerOptions _options;
28+
internal readonly ServerDriver ServerDriver;
29+
30+
/// <summary>
31+
/// Creates new instance of <see cref="StatsigProvider"/>
32+
/// </summary>
33+
/// <param name="sdkKey">SDK Key to access Statsig.</param>
34+
/// <param name="configurationAction">The action used to configure the client.</param>
35+
public StatsigProvider(string sdkKey = null, Action<StatsigServerOptions> configurationAction = null)
36+
{
37+
if (sdkKey != null)
38+
{
39+
_sdkKey = sdkKey;
40+
}
41+
_options = new StatsigServerOptions();
42+
configurationAction?.Invoke(_options);
43+
ServerDriver = new ServerDriver(_sdkKey, _options);
44+
}
45+
46+
/// <inheritdoc/>
47+
public override Metadata GetMetadata() => _providerMetadata;
48+
49+
/// <inheritdoc/>
50+
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null)
51+
{
52+
//TODO: defaultvalue = true not yet supported due to https://github.com/statsig-io/dotnet-sdk/issues/33
53+
if (defaultValue == true)
54+
throw new FeatureProviderException(ErrorType.General, "defaultvalue = true not supported (https://github.com/statsig-io/dotnet-sdk/issues/33)");
55+
if (GetStatus() != ProviderStatus.Ready)
56+
return Task.FromResult(new ResolutionDetails<bool>(flagKey, defaultValue, ErrorType.ProviderNotReady));
57+
var result = ServerDriver.CheckGateSync(context.AsStatsigUser(), flagKey);
58+
return Task.FromResult(new ResolutionDetails<bool>(flagKey, result));
59+
}
60+
61+
/// <inheritdoc/>
62+
public override Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null)
63+
{
64+
throw new NotImplementedException();
65+
}
66+
67+
/// <inheritdoc/>
68+
public override Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null)
69+
{
70+
throw new NotImplementedException();
71+
}
72+
73+
/// <inheritdoc/>
74+
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null)
75+
{
76+
throw new NotImplementedException();
77+
}
78+
79+
/// <inheritdoc/>
80+
public override Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null)
81+
{
82+
throw new NotImplementedException();
83+
}
84+
85+
/// <inheritdoc/>
86+
public override ProviderStatus GetStatus()
87+
{
88+
return initialized ? ProviderStatus.Ready : ProviderStatus.NotReady;
89+
}
90+
91+
/// <inheritdoc/>
92+
public override async Task Initialize(EvaluationContext context)
93+
{
94+
var initResult = await ServerDriver.Initialize();
95+
if (initResult == InitializeResult.Success || initResult == InitializeResult.LocalMode || initResult == InitializeResult.AlreadyInitialized)
96+
{
97+
initialized = true;
98+
}
99+
}
100+
101+
/// <inheritdoc/>
102+
public override Task Shutdown()
103+
{
104+
return ServerDriver.Shutdown();
105+
}
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.0.1

0 commit comments

Comments
 (0)