From 345945b623ddd58d477b4cb7c319bde8457c5817 Mon Sep 17 00:00:00 2001 From: Nishchal Date: Sun, 12 Jul 2020 00:43:21 +0700 Subject: [PATCH] feat(experiments): ability to force experiments --- Sdk.Test/FeatureClientTest.cs | 64 +++++++++++++++++++++++++++++++++++ Sdk/FeatureContainer.cs | 31 +++++------------ Sdk/Sdk.csproj | 1 + Sdk/SetupClient.cs | 22 ++++++++++++ Sdk/UserDataRepo.cs | 51 ++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 23 deletions(-) create mode 100644 Sdk/SetupClient.cs create mode 100644 Sdk/UserDataRepo.cs diff --git a/Sdk.Test/FeatureClientTest.cs b/Sdk.Test/FeatureClientTest.cs index 289b000..10452ca 100644 --- a/Sdk.Test/FeatureClientTest.cs +++ b/Sdk.Test/FeatureClientTest.cs @@ -50,6 +50,18 @@ public void Setup() var mockWorker = new Mock(); mockWorker.Setup(x => x.GetRunningFeatures()).Returns(runningFeatures); var mockUserData = new Mock(); + mockUserData.Setup(x => x.GetExperimentsForcedA()).Returns(new List + { + "ALL_A-1", + "ALL_A-2", + "ALL_A-3", + }); + mockUserData.Setup(x => x.GetExperimentsForcedB()).Returns(new List + { + "ALL_B-1", + "ALL_B-2", + "ALL_B-3", + }); mockUserData.Setup(x => x.GetUserId()).Returns(GetUuid); _userDataRepo = mockUserData.Object; _featureWorker = mockWorker.Object; @@ -127,5 +139,57 @@ public void TestFeatureWithAllBAlwaysReturnsB() Assert.AreEqual(0, dictionary['A']); Assert.AreEqual(1000000, dictionary['B']); } + + [Test] + public void TestFeatureWithOverrideExperimentsToA() + { + var dictionary = new Dictionary {['A'] = 0, ['B'] = 0, ['X'] = 0, ['Z'] = 0}; + var client = new FeatureClient(_featureWorker, _userDataRepo); + for (var i = 0; i < 100; i++) + { + var variant = client.GetVariant("ALL_A-1"); + dictionary[variant] = dictionary[variant] + 1; + } + for (var i = 0; i < 100; i++) + { + var variant = client.GetVariant("ALL_A-2"); + dictionary[variant] = dictionary[variant] + 1; + } + for (var i = 0; i < 100; i++) + { + var variant = client.GetVariant("ALL_A-3"); + dictionary[variant] = dictionary[variant] + 1; + } + Assert.AreEqual(300, dictionary['A']); + Assert.AreEqual(0, dictionary['B']); + Assert.AreEqual(0, dictionary['X']); + Assert.AreEqual(0, dictionary['Z']); + } + + [Test] + public void TestFeatureWithOverrideExperimentsToB() + { + var dictionary = new Dictionary {['A'] = 0, ['B'] = 0, ['X'] = 0, ['Z'] = 0}; + var client = new FeatureClient(_featureWorker, _userDataRepo); + for (var i = 0; i < 100; i++) + { + var variant = client.GetVariant("ALL_B-1"); + dictionary[variant] = dictionary[variant] + 1; + } + for (var i = 0; i < 100; i++) + { + var variant = client.GetVariant("ALL_B-2"); + dictionary[variant] = dictionary[variant] + 1; + } + for (var i = 0; i < 100; i++) + { + var variant = client.GetVariant("ALL_B-3"); + dictionary[variant] = dictionary[variant] + 1; + } + Assert.AreEqual(300, dictionary['B']); + Assert.AreEqual(0, dictionary['A']); + Assert.AreEqual(0, dictionary['X']); + Assert.AreEqual(0, dictionary['Z']); + } } } diff --git a/Sdk/FeatureContainer.cs b/Sdk/FeatureContainer.cs index 0ba5884..4f83389 100644 --- a/Sdk/FeatureContainer.cs +++ b/Sdk/FeatureContainer.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using FossApps.FeatureManager; using FossApps.FeatureManager.Models; -using Microsoft.Extensions.DependencyInjection; namespace Fossapps.FeatureManager { @@ -60,11 +59,6 @@ private async Task> GetFeatures() } } - public interface IUserDataRepo - { - public string GetUserId(); - } - // this is scoped public class FeatureClient { @@ -83,6 +77,14 @@ private RunningFeature GetFeatureById(string featId) } public char GetVariant(string featId) { + if (_userDataRepo.GetExperimentsForcedB().Any(x => x == featId)) + { + return 'B'; + } + if (_userDataRepo.GetExperimentsForcedA().Any(x => x == featId)) + { + return 'A'; + } var feature = GetFeatureById(featId); if (feature == null || !feature.Allocation.HasValue || string.IsNullOrEmpty(feature.RunToken) || string.IsNullOrEmpty(feature.FeatureToken)) @@ -117,21 +119,4 @@ private static int GetBucket(string userToken, string bucketToken, int numberOfB return (int) (number % (ulong) numberOfBuckets) + 1; } } - - public static class SetupFeatures - { - public static void SetupFeatureClients(this IServiceCollection collection, string endpoint, TimeSpan syncInterval) where TUserDataImplementation : class, IUserDataRepo - { - var worker = new FeatureWorker(endpoint, syncInterval); - worker.Init(); - collection.AddScoped(); - collection.AddSingleton(worker); - collection.AddScoped(); - } - - public static bool IsFeatureOn(this FeatureClient instance, string featId) - { - return instance.GetVariant(featId) == 'B'; - } - } } diff --git a/Sdk/Sdk.csproj b/Sdk/Sdk.csproj index c607822..a7e8378 100644 --- a/Sdk/Sdk.csproj +++ b/Sdk/Sdk.csproj @@ -8,6 +8,7 @@ + diff --git a/Sdk/SetupClient.cs b/Sdk/SetupClient.cs new file mode 100644 index 0000000..053e0d1 --- /dev/null +++ b/Sdk/SetupClient.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Fossapps.FeatureManager +{ + public static class SetupClient + { + public static void SetupFeatureClients(this IServiceCollection collection, string endpoint, TimeSpan syncInterval) where TUserDataImplementation : class, IUserDataRepo + { + var worker = new FeatureWorker(endpoint, syncInterval); + worker.Init(); + collection.AddScoped(); + collection.AddSingleton(worker); + collection.AddScoped(); + } + + public static bool IsFeatureOn(this FeatureClient instance, string featId) + { + return instance.GetVariant(featId) == 'B'; + } + } +} diff --git a/Sdk/UserDataRepo.cs b/Sdk/UserDataRepo.cs new file mode 100644 index 0000000..5005079 --- /dev/null +++ b/Sdk/UserDataRepo.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace Fossapps.FeatureManager +{ + public interface IUserDataRepo + { + public string GetUserId(); + + public IEnumerable GetExperimentsForcedA() + { + return new List(); + } + + public IEnumerable GetExperimentsForcedB() + { + return new List(); + } + } + public abstract class UserDataRepoBase : IUserDataRepo + { + private readonly IHttpContextAccessor _contextAccessor; + public abstract string GetUserId(); + + protected UserDataRepoBase(IHttpContextAccessor contextAccessor) + { + _contextAccessor = contextAccessor; + } + + public IEnumerable GetExperimentsForcedA() + { + if (!_contextAccessor.HttpContext.Request.Headers.TryGetValue("X-Forced-Features-A", out var features)) + { + return new List(); + } + + return features.ToString().Split(",").ToList(); + } + + public IEnumerable GetExperimentsForcedB() + { + if (!_contextAccessor.HttpContext.Request.Headers.TryGetValue("X-Forced-Features-B", out var features)) + { + return new List(); + } + + return features.ToString().Split(",").ToList(); + } + } +}