Skip to content
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

feat: Statsing provider #163

Merged
merged 4 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)");
beeme1mr marked this conversation as resolved.
Show resolved Hide resolved
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
Loading