Skip to content

Commit

Permalink
feat: Statsing provider (open-feature#163)
Browse files Browse the repository at this point in the history
Signed-off-by: Jens Henneberg <[email protected]>
  • Loading branch information
jenshenneberg authored Mar 13, 2024
1 parent 533dfa6 commit 98028e9
Show file tree
Hide file tree
Showing 12 changed files with 523 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
14 changes: 14 additions & 0 deletions DotnetSdkContrib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
4 changes: 2 additions & 2 deletions build/Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax.
-->
<!-- 0.5+ -->
<OpenFeatureVer>[1.4,)</OpenFeatureVer>
<OpenFeatureVer>[1.5,)</OpenFeatureVer>
</PropertyGroup>

<ItemGroup Condition="'$(OS)' == 'Unix'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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() { UserID = evaluationContext.TargetingKey };
foreach (var item in evaluationContext)
{
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>OpenFeature.Contrib.Provider.Statsig</PackageId>
<VersionNumber>0.0.1</VersionNumber><!--x-release-please-version -->
<VersionPrefix>$(VersionNumber)</VersionPrefix>
<VersionSuffix>preview</VersionSuffix>
<AssemblyVersion>$(VersionNumber)</AssemblyVersion>
<FileVersion>$(VersionNumber)</FileVersion>
<Description>Statsig provider for .NET</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Authors>Jens Kjær Henneberg</Authors>
</PropertyGroup>
<ItemGroup>
<!-- make the internal methods visble to our test project -->
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(MSBuildProjectName).Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Statsig" Version="1.23.1" />
</ItemGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>

</Project>
96 changes: 96 additions & 0 deletions src/OpenFeature.Contrib.Providers.Statsig/README.md
Original file line number Diff line number Diff line change
@@ -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
<PackageReference Include="OpenFeature.Contrib.Providers.Statsig" />
```
### 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<StatsigServerOptions>` 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
107 changes: 107 additions & 0 deletions src/OpenFeature.Contrib.Providers.Statsig/StatsigProvider.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// An OpenFeature <see cref="FeatureProvider"/> which enables the use of the Statsig Server-Side SDK for .NET
/// with OpenFeature.
/// </summary>
/// <example>
/// var provider = new StatsigProvider("my-sdk-key"), new StatsigProviderOptions(){LocalMode = false});
///
/// OpenFeature.Api.Instance.SetProvider(provider);
///
/// var client = OpenFeature.Api.Instance.GetClient();
/// </example>
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;

/// <summary>
/// Creates new instance of <see cref="StatsigProvider"/>
/// </summary>
/// <param name="sdkKey">SDK Key to access Statsig.</param>
/// <param name="configurationAction">The action used to configure the client.</param>
public StatsigProvider(string sdkKey = null, Action<StatsigServerOptions> configurationAction = null)
{
if (sdkKey != null)
{
_sdkKey = sdkKey;
}
_options = new StatsigServerOptions();
configurationAction?.Invoke(_options);
ServerDriver = new ServerDriver(_sdkKey, _options);
}

/// <inheritdoc/>
public override Metadata GetMetadata() => _providerMetadata;

/// <inheritdoc/>
public override Task<ResolutionDetails<bool>> 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<bool>(flagKey, defaultValue, ErrorType.ProviderNotReady));
var result = ServerDriver.CheckGateSync(context.AsStatsigUser(), flagKey);
return Task.FromResult(new ResolutionDetails<bool>(flagKey, result));
}

/// <inheritdoc/>
public override Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null)
{
throw new NotImplementedException();
}

/// <inheritdoc/>
public override Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null)
{
throw new NotImplementedException();
}

/// <inheritdoc/>
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null)
{
throw new NotImplementedException();
}

/// <inheritdoc/>
public override Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null)
{
throw new NotImplementedException();
}

/// <inheritdoc/>
public override ProviderStatus GetStatus()
{
return initialized ? ProviderStatus.Ready : ProviderStatus.NotReady;
}

/// <inheritdoc/>
public override async Task Initialize(EvaluationContext context)
{
var initResult = await ServerDriver.Initialize();
if (initResult == InitializeResult.Success || initResult == InitializeResult.LocalMode || initResult == InitializeResult.AlreadyInitialized)
{
initialized = true;
}
}

/// <inheritdoc/>
public override Task Shutdown()
{
return ServerDriver.Shutdown();
}
}
}
1 change: 1 addition & 0 deletions src/OpenFeature.Contrib.Providers.Statsig/version.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0.1
Loading

0 comments on commit 98028e9

Please sign in to comment.