Skip to content

Commit 2d153ef

Browse files
authored
Merge pull request #15 from fossapps/forcing-experiments
feat(experiments): ability to force experiments
2 parents 9ab5fca + 345945b commit 2d153ef

File tree

5 files changed

+146
-23
lines changed

5 files changed

+146
-23
lines changed

Sdk.Test/FeatureClientTest.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ public void Setup()
5050
var mockWorker = new Mock<IFeatureWorker>();
5151
mockWorker.Setup(x => x.GetRunningFeatures()).Returns(runningFeatures);
5252
var mockUserData = new Mock<IUserDataRepo>();
53+
mockUserData.Setup(x => x.GetExperimentsForcedA()).Returns(new List<string>
54+
{
55+
"ALL_A-1",
56+
"ALL_A-2",
57+
"ALL_A-3",
58+
});
59+
mockUserData.Setup(x => x.GetExperimentsForcedB()).Returns(new List<string>
60+
{
61+
"ALL_B-1",
62+
"ALL_B-2",
63+
"ALL_B-3",
64+
});
5365
mockUserData.Setup(x => x.GetUserId()).Returns(GetUuid);
5466
_userDataRepo = mockUserData.Object;
5567
_featureWorker = mockWorker.Object;
@@ -127,5 +139,57 @@ public void TestFeatureWithAllBAlwaysReturnsB()
127139
Assert.AreEqual(0, dictionary['A']);
128140
Assert.AreEqual(1000000, dictionary['B']);
129141
}
142+
143+
[Test]
144+
public void TestFeatureWithOverrideExperimentsToA()
145+
{
146+
var dictionary = new Dictionary<char, int> {['A'] = 0, ['B'] = 0, ['X'] = 0, ['Z'] = 0};
147+
var client = new FeatureClient(_featureWorker, _userDataRepo);
148+
for (var i = 0; i < 100; i++)
149+
{
150+
var variant = client.GetVariant("ALL_A-1");
151+
dictionary[variant] = dictionary[variant] + 1;
152+
}
153+
for (var i = 0; i < 100; i++)
154+
{
155+
var variant = client.GetVariant("ALL_A-2");
156+
dictionary[variant] = dictionary[variant] + 1;
157+
}
158+
for (var i = 0; i < 100; i++)
159+
{
160+
var variant = client.GetVariant("ALL_A-3");
161+
dictionary[variant] = dictionary[variant] + 1;
162+
}
163+
Assert.AreEqual(300, dictionary['A']);
164+
Assert.AreEqual(0, dictionary['B']);
165+
Assert.AreEqual(0, dictionary['X']);
166+
Assert.AreEqual(0, dictionary['Z']);
167+
}
168+
169+
[Test]
170+
public void TestFeatureWithOverrideExperimentsToB()
171+
{
172+
var dictionary = new Dictionary<char, int> {['A'] = 0, ['B'] = 0, ['X'] = 0, ['Z'] = 0};
173+
var client = new FeatureClient(_featureWorker, _userDataRepo);
174+
for (var i = 0; i < 100; i++)
175+
{
176+
var variant = client.GetVariant("ALL_B-1");
177+
dictionary[variant] = dictionary[variant] + 1;
178+
}
179+
for (var i = 0; i < 100; i++)
180+
{
181+
var variant = client.GetVariant("ALL_B-2");
182+
dictionary[variant] = dictionary[variant] + 1;
183+
}
184+
for (var i = 0; i < 100; i++)
185+
{
186+
var variant = client.GetVariant("ALL_B-3");
187+
dictionary[variant] = dictionary[variant] + 1;
188+
}
189+
Assert.AreEqual(300, dictionary['B']);
190+
Assert.AreEqual(0, dictionary['A']);
191+
Assert.AreEqual(0, dictionary['X']);
192+
Assert.AreEqual(0, dictionary['Z']);
193+
}
130194
}
131195
}

Sdk/FeatureContainer.cs

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Threading.Tasks;
77
using FossApps.FeatureManager;
88
using FossApps.FeatureManager.Models;
9-
using Microsoft.Extensions.DependencyInjection;
109

1110
namespace Fossapps.FeatureManager
1211
{
@@ -60,11 +59,6 @@ private async Task<List<RunningFeature>> GetFeatures()
6059
}
6160
}
6261

63-
public interface IUserDataRepo
64-
{
65-
public string GetUserId();
66-
}
67-
6862
// this is scoped
6963
public class FeatureClient
7064
{
@@ -83,6 +77,14 @@ private RunningFeature GetFeatureById(string featId)
8377
}
8478
public char GetVariant(string featId)
8579
{
80+
if (_userDataRepo.GetExperimentsForcedB().Any(x => x == featId))
81+
{
82+
return 'B';
83+
}
84+
if (_userDataRepo.GetExperimentsForcedA().Any(x => x == featId))
85+
{
86+
return 'A';
87+
}
8688
var feature = GetFeatureById(featId);
8789

8890
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
117119
return (int) (number % (ulong) numberOfBuckets) + 1;
118120
}
119121
}
120-
121-
public static class SetupFeatures
122-
{
123-
public static void SetupFeatureClients<TUserDataImplementation>(this IServiceCollection collection, string endpoint, TimeSpan syncInterval) where TUserDataImplementation : class, IUserDataRepo
124-
{
125-
var worker = new FeatureWorker(endpoint, syncInterval);
126-
worker.Init();
127-
collection.AddScoped<IUserDataRepo, TUserDataImplementation>();
128-
collection.AddSingleton<IFeatureWorker>(worker);
129-
collection.AddScoped<FeatureClient>();
130-
}
131-
132-
public static bool IsFeatureOn(this FeatureClient instance, string featId)
133-
{
134-
return instance.GetVariant(featId) == 'B';
135-
}
136-
}
137122
}

Sdk/Sdk.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
1112
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.21" />
1213
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.5" />
1314
</ItemGroup>

Sdk/SetupClient.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace Fossapps.FeatureManager
5+
{
6+
public static class SetupClient
7+
{
8+
public static void SetupFeatureClients<TUserDataImplementation>(this IServiceCollection collection, string endpoint, TimeSpan syncInterval) where TUserDataImplementation : class, IUserDataRepo
9+
{
10+
var worker = new FeatureWorker(endpoint, syncInterval);
11+
worker.Init();
12+
collection.AddScoped<IUserDataRepo, TUserDataImplementation>();
13+
collection.AddSingleton<IFeatureWorker>(worker);
14+
collection.AddScoped<FeatureClient>();
15+
}
16+
17+
public static bool IsFeatureOn(this FeatureClient instance, string featId)
18+
{
19+
return instance.GetVariant(featId) == 'B';
20+
}
21+
}
22+
}

Sdk/UserDataRepo.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Microsoft.AspNetCore.Http;
4+
5+
namespace Fossapps.FeatureManager
6+
{
7+
public interface IUserDataRepo
8+
{
9+
public string GetUserId();
10+
11+
public IEnumerable<string> GetExperimentsForcedA()
12+
{
13+
return new List<string>();
14+
}
15+
16+
public IEnumerable<string> GetExperimentsForcedB()
17+
{
18+
return new List<string>();
19+
}
20+
}
21+
public abstract class UserDataRepoBase : IUserDataRepo
22+
{
23+
private readonly IHttpContextAccessor _contextAccessor;
24+
public abstract string GetUserId();
25+
26+
protected UserDataRepoBase(IHttpContextAccessor contextAccessor)
27+
{
28+
_contextAccessor = contextAccessor;
29+
}
30+
31+
public IEnumerable<string> GetExperimentsForcedA()
32+
{
33+
if (!_contextAccessor.HttpContext.Request.Headers.TryGetValue("X-Forced-Features-A", out var features))
34+
{
35+
return new List<string>();
36+
}
37+
38+
return features.ToString().Split(",").ToList();
39+
}
40+
41+
public IEnumerable<string> GetExperimentsForcedB()
42+
{
43+
if (!_contextAccessor.HttpContext.Request.Headers.TryGetValue("X-Forced-Features-B", out var features))
44+
{
45+
return new List<string>();
46+
}
47+
48+
return features.ToString().Split(",").ToList();
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)