diff --git a/Gibe.AbTest.Tests/AbTestRepositoryTests.cs b/Gibe.AbTest.Tests/AbTestRepositoryTests.cs index a8bf2b2..db46ba9 100644 --- a/Gibe.AbTest.Tests/AbTestRepositoryTests.cs +++ b/Gibe.AbTest.Tests/AbTestRepositoryTests.cs @@ -11,11 +11,53 @@ namespace Gibe.AbTest.Tests [TestFixture] public class AbTestRepositoryTests { + private const string ValidExperimentId = "sq0S2zdKQhWEmBc6sQ4sfQ"; + private const string InvalidExperimentId = "NotRealId"; + [Test] - public void Test() + public void GetEnabledExperiments_Returns_All_Enabled_Experiments() { - var repo = new AbTestRepository(new DefaultDatabaseProvider("GibeCommerce")); - var experiments = repo.GetExperiments().ToArray(); + var experiments = Repo().GetEnabledExperiments().ToArray(); + + Assert.That(experiments.Length, Is.EqualTo(1)); + } + + [Test] + public void GetExperiment_Returns_Experiment_When_Id_Exists() + { + var experiment = Repo().GetExperiment(ValidExperimentId); + + Assert.That(experiment, Is.Not.Null); + Assert.That(experiment.Id, Is.EqualTo(ValidExperimentId)); + } + + [Test] + public void GetExperiment_Returns_Null_When_Id_Does_Not_Exist() + { + var experiment = Repo().GetExperiment(InvalidExperimentId); + + Assert.That(experiment, Is.Null); + } + + [Test] + public void GetVariations_Returns_All_Variations_For_Experiment_When_Id_Exists() + { + var variations = Repo().GetVariations(ValidExperimentId).ToArray(); + + Assert.That(variations.Count(), Is.EqualTo(2)); + Assert.That(variations.All(v => v.ExperimentId == ValidExperimentId), Is.True); + } + + [Test] + public void GetVariations_Returns_Empty_Enumerable_For_Experiment_When_Id_Does_Not_Exist() + { + var variations = Repo().GetVariations(InvalidExperimentId); + + Assert.That(variations.Count(), Is.EqualTo(0)); } + + private IAbTestRepository Repo() => new AbTestRepository(new DefaultDatabaseProvider("GibeCommerce")); + + } } diff --git a/Gibe.AbTest.Tests/AbTestTests.cs b/Gibe.AbTest.Tests/AbTestTests.cs index 3eb0416..dbb6e43 100644 --- a/Gibe.AbTest.Tests/AbTestTests.cs +++ b/Gibe.AbTest.Tests/AbTestTests.cs @@ -1,6 +1,7 @@ -using System; +using NUnit.Framework; +using System; +using System.Collections.Generic; using System.Linq; -using NUnit.Framework; namespace Gibe.AbTest.Tests { @@ -9,39 +10,86 @@ public class AbTestTests { private const string MobileUserAgent = "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"; private const string DesktopUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0"; + private IAbTestingService _abTestingService; + private IAbTestingService _simpleAbTestingService; + + [SetUp] + public void Setup() + { + _abTestingService = new FakeAbTestingService(new List + { + new Experiment("Ex1", "Exp1", "Experiment 1", 1, true, DateTime.Now, null, + new []{ + new Variation(1, 0, 1, true, "{Exp1:'Variant 1'}", "Exp1", false), + new Variation(2, 1, 1, true, "{Exp1:'Variant 2'}", "Exp1", false) + }), + new Experiment("Ex2", "Exp2", "Experiment 2", 1, true, DateTime.Now, null, + new []{ + new Variation(3, 0, 1, true, "{Exp2:'Variant 1'}", "Exp2", false), + new Variation(4, 1, 1, true, "{Exp2:'Variant 2'}", "Exp2", true) + }), + new Experiment("Ex3", "Exp3", "Experiment 3", 1, false, DateTime.Now, null, + new []{ + new Variation(5, 0, 1, true, "{Exp3:'Variant 1'}", "Exp3", true), + new Variation(6, 1, 1, true, "{Exp3:'Variant 2'}", "Exp3", true) + }) + }); + + _simpleAbTestingService = new FakeAbTestingService(new List + { + new Experiment("Ex1", "Exp1", "Experiment 1", 1, true, DateTime.Now, null, + new []{ + new Variation(1, 0, 1, true, "{Exp1:'Variant 1'}", "Exp1", false), + new Variation(2, 1, 1, true, "{Exp1:'Variant 2'}", "Exp1", false) + }) + }); + } [Test] - public void AssignVariation_assigns_first_experiment_variation_when_random_number_is_0() + public void AssignVariation_Assigns_First_Experiment_Variation_When_Random_Number_Is_0() { - var fakeAbTestingService = new FakeAbTestingService(); + var abTest = new AbTest(_simpleAbTestingService, new FakeRandomNumber(new [] { 0, 0 })); - var abTest = new AbTest(fakeAbTestingService, new FakeRandomNumber(new [] { 0, 0 })); + var variation = abTest.AssignRandomVariation(DesktopUserAgent); + Assert.AreEqual(_simpleAbTestingService.GetEnabledExperiments().First().Variations.First().Id, variation.Id); + } - var variation = abTest.AssignVariation(MobileUserAgent); + [Test] + public void AssignVariation_Assigns_Second_Experiment_Variation_When_Random_Number_Is_1() + { - Assert.AreEqual(fakeAbTestingService.GetExperiments().First().Variations.First().Id, variation.Id); + var abTest = new AbTest(_simpleAbTestingService, new FakeRandomNumber(new[] { 1, 1 })); + + var variation = abTest.AssignRandomVariation(DesktopUserAgent); + + Assert.AreEqual(_simpleAbTestingService.GetEnabledExperiments().First().Variations.ElementAt(1).Id, variation.Id); } [Test] - public void AssignVariation_assigns_second_experiment_variation_when_random_number_is_1() + public void AssignVariation_Assigns_Second_Experiment_Variation_When_Random_Number_Is_2() { - var fakeAbTestingService = new FakeAbTestingService(); + var abTest = new AbTest(_simpleAbTestingService, new FakeRandomNumber(new[] { 2, 2 })); - var abTest = new AbTest(fakeAbTestingService, new FakeRandomNumber(new[] { 1, 1 })); + var variation = abTest.AssignRandomVariation(DesktopUserAgent); + Assert.AreEqual(_simpleAbTestingService.GetEnabledExperiments().First().Variations.ElementAt(1).Id, variation.Id); + } + + [Test] + public void AssignVariation_Assigns_Given_Experiment_First_Variation() + { + var abTest = new AbTest(_abTestingService, new FakeRandomNumber(new[] { 0, 0, 0 })); - var variation = abTest.AssignVariation(MobileUserAgent); + var variation = abTest.AssignVariationByExperimentKey("Exp1"); - Assert.AreEqual(fakeAbTestingService.GetExperiments().ElementAt(1).Variations.ElementAt(1).Id, variation.Id); + Assert.That(_abTestingService.GetEnabledExperiments().First(x => x.Key == "Exp1").Variations.First().Id, Is.EqualTo(variation.Id)); } [Test] public void AssignVariation_does_not_assign_mobile_user_to_desktop_variant() { - var fakeAbTestingService = new FakeAbTestingService(); - - var abTest = new AbTest(fakeAbTestingService, new FakeRandomNumber(new[] { 1, 1 })); + var abTest = new AbTest(_abTestingService, new FakeRandomNumber(new[] { 1, 1, 1 })); - var variation = abTest.AssignVariation(MobileUserAgent); + var variation = abTest.AssignRandomVariation(MobileUserAgent); Assert.IsFalse(variation.DesktopOnly); } @@ -49,30 +97,36 @@ public void AssignVariation_does_not_assign_mobile_user_to_desktop_variant() [Test] public void AssignVariation_assigns_desktop_user_to_desktop_variant() { - var fakeAbTestingService = new FakeAbTestingService(); - - var abTest = new AbTest(fakeAbTestingService, new FakeRandomNumber(new[] { 1, 1 })); + var abTest = new AbTest(_abTestingService, new FakeRandomNumber(new[] { 1, 1, 1 })); - var variation = abTest.AssignVariation(DesktopUserAgent); + var variation = abTest.AssignRandomVariation(DesktopUserAgent); Assert.IsTrue(variation.DesktopOnly); } + [Test] + public void AssignVariations_Assigns_First_Experiment_Variation_From_Each_Enabled_Experiment() + { + var abTest = new AbTest(_abTestingService, new FakeRandomNumber(new[] { 0, 0, 0 })); + + var variations = abTest.AllCurrentVariations().ToList(); + + Assert.That(_abTestingService.GetEnabledExperiments().Where(e => e.Enabled).Select(e => e.Variations.First().Id), Is.EqualTo(variations.Select(v => v.Id))); + Assert.That(variations.Count, Is.EqualTo(2)); + } + [Test] public void GetAssignedVariation_returns_variation_from_AbTestingService() { - const string experimentId = "ABC"; + const string experimentId = "Exp4"; const int variationNo = 1; - var fakeAbTestingService = new FakeAbTestingService(); + var abTest = new AbTest(_abTestingService, new FakeRandomNumber(new int[] {})); - var abTest = new AbTest(fakeAbTestingService, new FakeRandomNumber(new int[] {})); - - var variation = abTest.GetAssignedVariation(experimentId, variationNo); + var variation = abTest.Variation(experimentId, variationNo); Assert.AreEqual(experimentId, variation.ExperimentId); Assert.AreEqual(variationNo, variation.VariationNumber); - } } } diff --git a/Gibe.AbTest.Tests/AbTestingServiceTests.cs b/Gibe.AbTest.Tests/AbTestingServiceTests.cs index a7778b6..03e3aa2 100644 --- a/Gibe.AbTest.Tests/AbTestingServiceTests.cs +++ b/Gibe.AbTest.Tests/AbTestingServiceTests.cs @@ -18,7 +18,7 @@ public void GetExperiments_Returns_Empty_Experiment_When_No_Experiments() { var fakeAbTestingService = new FakeAbTestRepository(new List(), new List()); - var experiments = Service(fakeAbTestingService).GetExperiments(); + var experiments = Service(fakeAbTestingService).GetEnabledExperiments(); var expected = new Experiment(new ExperimentDto { diff --git a/Gibe.AbTest.Tests/App.config b/Gibe.AbTest.Tests/App.config index e24dfdc..960840b 100644 --- a/Gibe.AbTest.Tests/App.config +++ b/Gibe.AbTest.Tests/App.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/Gibe.AbTest.Tests/ExperimentServiceTests.cs b/Gibe.AbTest.Tests/ExperimentServiceTests.cs new file mode 100644 index 0000000..ac14d5e --- /dev/null +++ b/Gibe.AbTest.Tests/ExperimentServiceTests.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using System.Linq; +using Gibe.Cookies; +using Moq; +using NUnit.Framework; + +namespace Gibe.AbTest.Tests +{ + [TestFixture] + public class ExperimentServiceTests + { + private const string CookieKey = "GCEXP"; + + private Mock _cookieService; + private IAbTest _abTest; + + [SetUp] + public void Setup() + { + _cookieService = new Mock(); + _abTest = new FakeAbTest( + new List{ + new Variation(1, 0, 1,true,"{Test:'test1'}", "vapBwUPvTEuGcEVEKThGCA", false), + new Variation(2, 1, 1,true,"{Test:'test1'}", "vapBwUPvTEuGcEVEKThGCA", false), + new Variation(3, 0, 1,true,"{Test:'test2'}", "vapBwUPvTEuGcEVEKThGCB", false), + new Variation(4, 1, 1,true,"{Test:'test2'}", "vapBwUPvTEuGcEVEKThGCB", false), + new Variation(5, 0, 1,true,"{Test:'test3'}", "vapBwUPvTEuGcEVEKThGCC", false), + new Variation(6, 1, 1,true,"{Test:'test3'}", "vapBwUPvTEuGcEVEKThGCC", false) + }); + } + + private IExperimentService ExperimentService() + { + return new DefaultExperimentService(_cookieService.Object, _abTest, new ExperimentCookieValueFactory(_abTest)); + } + + [Test] + public void IsCurrentUserInExperiment_Returns_True_When_Cookie_Contains_Experiment() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1"); + + var result = ExperimentService().IsCurrentUserInExperiment(); + + Assert.That(result, Is.True); + } + + [Test] + public void IsCurrentUserInExperiment_Returns_False_When_No_Experiment_Cookie_Found() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns(""); + + var result = ExperimentService().IsCurrentUserInExperiment(); + + Assert.That(result, Is.False); + } + + [Test] + public void CurrentUserVariations_Returns_Enumerable_Of_Variations_Based_On_The_Cookie_Value() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1"); + + var results = ExperimentService().CurrentUserVariations(); + + AssertVariations(results, "vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1"); + } + + [Test] + public void CurrentUserVariation_Returns_Variation_For_ExperiementId_Based_On_The_Cookie_Value() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1"); + + var results = ExperimentService().CurrentUserVariation("vapBwUPvTEuGcEVEKThGCA"); + + AssertVariations(new []{results}, "vapBwUPvTEuGcEVEKThGCA~0"); + } + + [Test] + public void AssignUserVariations_Assigns_User_To_Random_Variations_For_Each_Experiment() + { + + var results = ExperimentService().AssignUserVariations(); + + Assert.That(results.Count(), Is.EqualTo(3)); + } + + [Test] + public void AssignUserVariations_Updates_The_Cookie_Value_For_The_Requested_Variation_Only() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + + var results = ExperimentService().AssignUserVariations("vapBwUPvTEuGcEVEKThGCA~1"); + + AssertVariations(results, "vapBwUPvTEuGcEVEKThGCA~1-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + } + + [Test] + public void AssignUserVariations_Does_Not_Change_The_Cookie_Value_For_The_Requested_Variation_When_The_Same() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + + var results = ExperimentService().AssignUserVariations("vapBwUPvTEuGcEVEKThGCA~0"); + + AssertVariations(results, "vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + } + + [Test] + public void AssignUserVariations_Does_Not_Change_The_Cookie_Value_When_The_Requested_Experiment_Does_Not_Exist() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + + var results = ExperimentService().AssignUserVariations("vapBwUPvTEuGcEVEKThGCD~0"); + + AssertVariations(results, "vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + } + + [Test] + public void AssignCurrentUserToVariation_Does_Not_Change_The_Cookie_Value_When_The_Requested_Variation_Does_Not_Exist() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + + var results = ExperimentService().AssignUserVariations("vapBwUPvTEuGcEVEKThGCA~3"); + + AssertVariations(results, "vapBwUPvTEuGcEVEKThGCA~0-vapBwUPvTEuGcEVEKThGCB~1-vapBwUPvTEuGcEVEKThGCC~0"); + } + + [Test] + public void AssignUserVariations_Assigns_User_To_Experiments_Not_Currently_In_When_Updating_Variation() + { + _cookieService.Setup(s => s.GetValue(It.IsAny())).Returns("vapBwUPvTEuGcEVEKThGCA~0"); + + var results = ExperimentService().AssignUserVariations("vapBwUPvTEuGcEVEKThGCA~1"); + + AssertVariations(results, "vapBwUPvTEuGcEVEKThGCA~1-vapBwUPvTEuGcEVEKThGCB~0-vapBwUPvTEuGcEVEKThGCC~0"); + } + + private static void AssertVariations(IEnumerable results, string variationsString) + { + var variations = variationsString.Split('-') + .Select(e => new Variation(0, int.Parse(e.Split('~')[1]), 1, true, "", e.Split('~')[0], false)); + + foreach (var variation in variations) + { + Assert.That(results.Any(r => r.ExperimentId == variation.ExperimentId && r.VariationNumber == variation.VariationNumber)); + } + Assert.That(results.Count(), Is.EqualTo(variations.Count())); + } + } +} + diff --git a/Gibe.AbTest.Tests/Gibe.AbTest.Tests.csproj b/Gibe.AbTest.Tests/Gibe.AbTest.Tests.csproj index f77a67f..af42eaa 100644 --- a/Gibe.AbTest.Tests/Gibe.AbTest.Tests.csproj +++ b/Gibe.AbTest.Tests/Gibe.AbTest.Tests.csproj @@ -35,10 +35,16 @@ 4 + + ..\packages\Gibe.Cookies.1.0.356\lib\Gibe.Cookies.dll + ..\packages\Gibe.NPoco.1.0.159\lib\Gibe.NPoco.dll True + + ..\packages\Moq.4.2.1502.0911\lib\net40\Moq.dll + ..\packages\NPoco.2.5.77\lib\net40\NPoco.dll True @@ -62,6 +68,7 @@ + diff --git a/Gibe.AbTest.Tests/packages.config b/Gibe.AbTest.Tests/packages.config index bc3117e..d463cc1 100644 --- a/Gibe.AbTest.Tests/packages.config +++ b/Gibe.AbTest.Tests/packages.config @@ -1,6 +1,8 @@  + + \ No newline at end of file diff --git a/Gibe.AbTest/AbTest.cs b/Gibe.AbTest/AbTest.cs index f23e3ae..3edcdfc 100644 --- a/Gibe.AbTest/AbTest.cs +++ b/Gibe.AbTest/AbTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; namespace Gibe.AbTest @@ -9,24 +10,56 @@ public class AbTest : IAbTest private readonly IAbTestingService _abTestingService; private readonly IRandomNumber _randomNumber; - public AbTest(IAbTestingService abTestingService, IRandomNumber randomNumber) + public AbTest(IAbTestingService abTestingService, + IRandomNumber randomNumber) { _abTestingService = abTestingService; _randomNumber = randomNumber; } - public Variation AssignVariation(string userAgent) + public IEnumerable AllExperiments() { - var experiments = _abTestingService.GetExperiments().Where(x => x.Enabled); - var selectedExperiment = RandomlySelectOption(experiments); + return _abTestingService.GetEnabledExperiments(); + } + + public Variation AssignRandomVariation(string userAgent) + { + var experiments = _abTestingService.GetEnabledExperiments(); + var selectedExperiment = RandomlySelectOption(FilterExperiments(experiments, userAgent)); return RandomlySelectOption(FilterVariations(selectedExperiment.Variations, userAgent)); } - public Variation GetAssignedVariation(string experimentId, int variationNumber) + public Variation AssignVariationByExperimentKey(string experimentKey) + { + var experiment = _abTestingService.GetEnabledExperiments() + .First(x => x.Key == experimentKey); + return RandomlySelectOption(experiment.Variations); + } + + public IEnumerable AllCurrentVariations() + { + var experiments = _abTestingService.GetEnabledExperiments(); + foreach (var experiment in experiments) + { + yield return RandomlySelectOption(experiment.Variations); + } + } + + public Variation Variation(string experimentId, int variationNumber) { return _abTestingService.GetVariation(experimentId, variationNumber); } + private IEnumerable FilterExperiments(IEnumerable experments, string userAgent) + { + var filtered = experments.Where(e => e.Variations.Any(v => v.DesktopOnly) && !userAgent.Contains("Mobi") || !e.Variations.All(v => v.DesktopOnly)); + if (!filtered.Any()) + { + return experments.Take(1); //TODO: We should not return anything if there are no correct matches, this requires a refactor to use IEnumerables everywhere + } + return filtered; + } + private IEnumerable FilterVariations(IEnumerable variations, string userAgent) { var filtered = variations.Where(v => v.DesktopOnly && !userAgent.Contains("Mobi") || !v.DesktopOnly); @@ -37,18 +70,19 @@ private IEnumerable FilterVariations(IEnumerable variation return filtered; } + private T RandomlySelectOption(IEnumerable options) where T : IWeighted { var opts = options.ToArray(); var totalWeights = opts.Sum(o => o.Weight); var selectedNumber = _randomNumber.Number(totalWeights); - var currrentWeight = 0; - foreach (var t in opts) + var currentWeight = 0; + foreach (var o in opts) { - currrentWeight += t.Weight; - if (currrentWeight > selectedNumber) - return t; + currentWeight += o.Weight; + if (currentWeight > selectedNumber) + return o; } return opts.Last(); } diff --git a/Gibe.AbTest/AbTestRepository.cs b/Gibe.AbTest/AbTestRepository.cs index ea85457..677f708 100644 --- a/Gibe.AbTest/AbTestRepository.cs +++ b/Gibe.AbTest/AbTestRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Gibe.AbTest.Dto; using Gibe.NPoco; @@ -17,31 +18,27 @@ public ExperimentDto GetExperiment(string id) { using (var db = _databaseProvider.GetDatabase()) { - return db.Single("WHERE Id = @0", id); + return db.SingleOrDefault("WHERE Id = @0", id); } } - public IEnumerable GetExperiments() + public IEnumerable GetEnabledExperiments() { using (var db = _databaseProvider.GetDatabase()) { - return db.Query("FROM AbExperiment"); + var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + return db.Fetch($"FROM AbExperiment " + + $"WHERE [Enabled] = 1 " + + $"AND '{now}' >= [StartDate] OR StartDate IS NULL " + + $"AND '{now}' < [EndDate] OR StartDate IS NULL "); } } - - public VariationDto GetVariation(int id) - { - using (var db = _databaseProvider.GetDatabase()) - { - return db.Single("WHERE Id = @0", id); - } - } - + public IEnumerable GetVariations(string experimentId) { using (var db = _databaseProvider.GetDatabase()) { - return db.Query("WHERE ExperimentId = @0", experimentId); + return db.Fetch("WHERE ExperimentId = @0", experimentId); } } } diff --git a/Gibe.AbTest/AbTestingService.cs b/Gibe.AbTest/AbTestingService.cs index 082bec6..1d657f8 100644 --- a/Gibe.AbTest/AbTestingService.cs +++ b/Gibe.AbTest/AbTestingService.cs @@ -1,4 +1,5 @@ -using Gibe.AbTest.Dto; +using System; +using Gibe.AbTest.Dto; using System.Collections.Generic; using System.Linq; @@ -13,15 +14,15 @@ public AbTestingService(IAbTestRepository abTestRepository) _abTestRepository = abTestRepository; } - public IEnumerable GetExperiments() + public IEnumerable GetEnabledExperiments() { - var experiments = _abTestRepository.GetExperiments() + var experiments = _abTestRepository.GetEnabledExperiments() .Select(x => new Experiment(x, GetVariations(x.Id).ToArray())); if (experiments.Any()) { return experiments; - } + } return new[] { EmptyExperiment() }; } @@ -33,7 +34,7 @@ public IEnumerable GetVariations(string experimentId) if (variations.Any()) { return variations.Select(v => new Variation(v)); - } + } return new[] { EmptyVariation() }; } @@ -46,10 +47,10 @@ public Variation GetVariation(string experimentId, int variationNumber) if (variation != null) { return new Variation(variation); - } + } return EmptyVariation(); - } + } private Experiment EmptyExperiment() { diff --git a/Gibe.AbTest/Attributes/NotCachedAttribute.cs b/Gibe.AbTest/Attributes/NotCachedAttribute.cs index 78e3af2..d83492d 100644 --- a/Gibe.AbTest/Attributes/NotCachedAttribute.cs +++ b/Gibe.AbTest/Attributes/NotCachedAttribute.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Gibe.AbTest.Attributes { diff --git a/Gibe.AbTest/CachingAbTestingService.cs b/Gibe.AbTest/CachingAbTestingService.cs index 797c72f..43b601f 100644 --- a/Gibe.AbTest/CachingAbTestingService.cs +++ b/Gibe.AbTest/CachingAbTestingService.cs @@ -26,16 +26,16 @@ public Variation GetVariation(string experimentId, int variationNumber) public IEnumerable GetVariations(string experimentId) { - return GetExperiments().First(x => x.Id == experimentId).Variations; + return GetEnabledExperiments().First(x => x.Id == experimentId).Variations; } - public IEnumerable GetExperiments() + public IEnumerable GetEnabledExperiments() { if (_cache.Exists(CacheKey)) { return _cache.Get(CacheKey); } - var experiments = _abTestingService.GetExperiments().ToArray(); + var experiments = _abTestingService.GetEnabledExperiments().ToArray(); _cache.Add(CacheKey, experiments, TimeSpan.FromMinutes(15)); return experiments; } diff --git a/Gibe.AbTest/DefaultExperimentService.cs b/Gibe.AbTest/DefaultExperimentService.cs new file mode 100644 index 0000000..c3a3621 --- /dev/null +++ b/Gibe.AbTest/DefaultExperimentService.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gibe.Cookies; + +namespace Gibe.AbTest +{ + public class DefaultExperimentService : IExperimentService + { + private const string CookieKey = "GCEXP"; + private readonly ICookieService _cookieService; + private readonly IAbTest _abTest; + private readonly IExperimentCookieValueFactory _experimentCookieValueFactory; + + public DefaultExperimentService(ICookieService cookieService, + IAbTest abTest, + IExperimentCookieValueFactory experimentCookieValueFactory) + { + _cookieService = cookieService; + _abTest = abTest; + _experimentCookieValueFactory = experimentCookieValueFactory; + } + + //TODO: Not sure we need this, being in no experiments isn't a special case + public bool IsCurrentUserInExperiment() + { + var allVariations = _abTest.AllCurrentVariations().ToList(); + var cookieVariations = CookieVariations(_cookieService.GetValue(CookieKey)).ToList(); + + return allVariations.Any(v => cookieVariations.Any(c => c.ExperimentId == v.ExperimentId)); + } + + public IEnumerable CurrentUserVariations() + { + return CookieVariations(_cookieService.GetValue(CookieKey)); + } + + public Variation CurrentUserVariation(string experimentId) + { + return CookieVariations(_cookieService.GetValue(CookieKey)) + .First(v => v.ExperimentId == experimentId); + } + + public IEnumerable AssignUserVariations() + { + var variationsFromCookie = UserVariationsFromCookie(); + + SaveVariationsCookie(variationsFromCookie); + return variationsFromCookie; + } + + ///experiments/change/?value=vapBwUPvTEuGcEVEKThGCA~0 + public IEnumerable AssignUserVariations(string value) + { + var variationToSet = CookieVariations(value).First(); + var variationsFromCookie = UserVariationsFromCookie(); + + if (variationToSet != null) + { + for (var i = 0; i < variationsFromCookie.Count; i++) + { + if (variationsFromCookie[i].ExperimentId == variationToSet.ExperimentId) + { + variationsFromCookie[i] = variationToSet; + } + } + } + + SaveVariationsCookie(variationsFromCookie); + return variationsFromCookie; + } + + + private List UserVariationsFromCookie() + { + var cookieVariations = CookieVariations(_cookieService.GetValue(CookieKey)).ToList(); + + var allVariations = _abTest.AllCurrentVariations().ToList(); + + foreach (var cookieVariation in cookieVariations) + { + for (var i = 0; i < allVariations.Count; i++) + { + if (allVariations[i].ExperimentId == cookieVariation.ExperimentId) + { + allVariations[i] = cookieVariation; + } + } + } + + return allVariations; + } + + private void SaveVariationsCookie(IEnumerable variations) + { + var variationsCookie = _experimentCookieValueFactory.ExperimentCookieValue(variations); + + _cookieService.Create(CookieKey, variationsCookie.RawValue, DateTime.Now.AddDays(120)); + } + + private IEnumerable CookieVariations(string cookieValue) + { + if (string.IsNullOrEmpty(cookieValue)) + { + return Enumerable.Empty(); + } + + return _experimentCookieValueFactory.ExperimentCookieValue(cookieValue) + .Variations(); + } + } +} diff --git a/Gibe.AbTest/ExperimentCookieValue.cs b/Gibe.AbTest/ExperimentCookieValue.cs new file mode 100644 index 0000000..a557018 --- /dev/null +++ b/Gibe.AbTest/ExperimentCookieValue.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Gibe.AbTest +{ + public class ExperimentCookieValue + { + private readonly IAbTest _abTest; + + private const string Seperator = "~"; + private const string ExperimentSeperator = "-"; + + public string RawValue; + + public ExperimentCookieValue(IAbTest abTest, string rawValue) + { + _abTest = abTest; + RawValue = rawValue; + } + + public ExperimentCookieValue(IAbTest abTest, IEnumerable experimentVariantPairs) + { + _abTest = abTest; + RawValue = string.Join(ExperimentSeperator, experimentVariantPairs.Select(v => + string.Join(Seperator, v.ExperimentId, v.VariationNumber))); + } + + public IEnumerable Variations() + { + return RawValue.Split(ExperimentSeperator.ToCharArray()) + .Select(e => + _abTest.Variation(e.Split(Seperator.ToCharArray())[0], int.Parse(e.Split(Seperator.ToCharArray())[1]))); + } + } +} diff --git a/Gibe.AbTest/ExperimentCookieValueFactory.cs b/Gibe.AbTest/ExperimentCookieValueFactory.cs new file mode 100644 index 0000000..da1813c --- /dev/null +++ b/Gibe.AbTest/ExperimentCookieValueFactory.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Gibe.AbTest +{ + public class ExperimentCookieValueFactory : IExperimentCookieValueFactory + { + private readonly IAbTest _abTest; + + public ExperimentCookieValueFactory(IAbTest abTest) + { + _abTest = abTest; + } + + public ExperimentCookieValue ExperimentCookieValue(string rawValue) + { + return new ExperimentCookieValue(_abTest, rawValue); + } + + public ExperimentCookieValue ExperimentCookieValue(IEnumerable variations) + { + return new ExperimentCookieValue(_abTest, variations); + } + } +} diff --git a/Gibe.AbTest/Gibe.AbTest.csproj b/Gibe.AbTest/Gibe.AbTest.csproj index 0fec46d..4256f20 100644 --- a/Gibe.AbTest/Gibe.AbTest.csproj +++ b/Gibe.AbTest/Gibe.AbTest.csproj @@ -34,6 +34,9 @@ ..\packages\Gibe.Caching.1.0.159\lib\Gibe.Caching.dll True + + ..\packages\Gibe.Cookies.1.0.356\lib\Gibe.Cookies.dll + ..\packages\Gibe.NPoco.1.0.159\lib\Gibe.NPoco.dll True @@ -62,11 +65,16 @@ + + + + + diff --git a/Gibe.AbTest/IAbTest.cs b/Gibe.AbTest/IAbTest.cs index 28bd6ea..2f55acf 100644 --- a/Gibe.AbTest/IAbTest.cs +++ b/Gibe.AbTest/IAbTest.cs @@ -1,27 +1,49 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Gibe.AbTest { public interface IAbTest { - Variation AssignVariation(string userAgent); - Variation GetAssignedVariation(string experimentId, int variationNumber); + IEnumerable AllExperiments(); + Variation AssignRandomVariation(string userAgent); + Variation AssignVariationByExperimentKey(string experimentKey); + IEnumerable AllCurrentVariations(); + Variation Variation(string experimentId, int variationNumber); } public class FakeAbTest : IAbTest { - public Variation AssignVariation(string userAgent) + private readonly IEnumerable _variations; + + public FakeAbTest(IEnumerable variations) + { + _variations = variations; + } + + public IEnumerable AllExperiments() + { + return new List();//TODO: chance constructor to take experiments instead of variations + } + + public Variation AssignRandomVariation(string userAgent) + { + return _variations.First(); + } + + public Variation AssignVariationByExperimentKey(string experimentKey) + { + return _variations.First(v => v.ExperimentId == experimentKey); + } + + public IEnumerable AllCurrentVariations() { - return new Variation(1, 0, 1,true,"{Test:'test'}", "ABC1", false); + return _variations.GroupBy(v => v.ExperimentId).Select(group => group.First()); } - public Variation GetAssignedVariation(string experimentId, int variationNumber) + public Variation Variation(string experimentId, int variationNumber) { - return new Variation(1, variationNumber, 1, true, "{Test:'test'}", experimentId, false); + return _variations.FirstOrDefault(v => v.ExperimentId == experimentId && v.VariationNumber == variationNumber); } } } diff --git a/Gibe.AbTest/IAbTestRepository.cs b/Gibe.AbTest/IAbTestRepository.cs index cee0d53..a54b32d 100644 --- a/Gibe.AbTest/IAbTestRepository.cs +++ b/Gibe.AbTest/IAbTestRepository.cs @@ -9,8 +9,7 @@ namespace Gibe.AbTest public interface IAbTestRepository { ExperimentDto GetExperiment(string id); - IEnumerable GetExperiments(); - VariationDto GetVariation(int id); + IEnumerable GetEnabledExperiments(); IEnumerable GetVariations(string experimentId); @@ -32,12 +31,12 @@ public ExperimentDto GetExperiment(string id) return _experiments.First(); } - public IEnumerable GetExperiments() + public IEnumerable GetEnabledExperiments() { return _experiments; } - public VariationDto GetVariation(int id) + private VariationDto GetVariation(int id) { return _variations.First(); } diff --git a/Gibe.AbTest/IAbTestingService.cs b/Gibe.AbTest/IAbTestingService.cs index 8834241..eca2753 100644 --- a/Gibe.AbTest/IAbTestingService.cs +++ b/Gibe.AbTest/IAbTestingService.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Gibe.AbTest { @@ -10,11 +7,17 @@ public interface IAbTestingService { Variation GetVariation(string experimentId, int variationNumber); IEnumerable GetVariations(string experimentId); - IEnumerable GetExperiments(); + IEnumerable GetEnabledExperiments(); } public class FakeAbTestingService : IAbTestingService { + public IEnumerable Experiments; + public FakeAbTestingService(IEnumerable experiments) + { + Experiments = experiments; + } + public Variation GetVariation(string experimentId, int variationNumber) { return new Variation(1, variationNumber, 1, true, "{Test:'test'}", experimentId, false); @@ -36,13 +39,9 @@ public IEnumerable GetVariations(string experimentId) }; } - public IEnumerable GetExperiments() + public IEnumerable GetEnabledExperiments() { - return new List - { - new Experiment("A1234", "AB1", "Desc 1", 1, true, DateTime.Now, null, GetVariations("AB1").ToArray()), - new Experiment("A2345", "AB2", "Desc 2", 1, true, DateTime.Now, null, GetVariations("AB2").ToArray()) - }; + return Experiments.Where(e => e.Enabled); } } } diff --git a/Gibe.AbTest/IExperimentCookieValueFactory.cs b/Gibe.AbTest/IExperimentCookieValueFactory.cs new file mode 100644 index 0000000..cbbe159 --- /dev/null +++ b/Gibe.AbTest/IExperimentCookieValueFactory.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Gibe.AbTest +{ + public interface IExperimentCookieValueFactory + { + ExperimentCookieValue ExperimentCookieValue(IEnumerable variations); + ExperimentCookieValue ExperimentCookieValue(string rawValue); + } +} \ No newline at end of file diff --git a/Gibe.AbTest/IExperimentService.cs b/Gibe.AbTest/IExperimentService.cs new file mode 100644 index 0000000..cc4a52d --- /dev/null +++ b/Gibe.AbTest/IExperimentService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Gibe.AbTest +{ + public interface IExperimentService + { + bool IsCurrentUserInExperiment(); + IEnumerable CurrentUserVariations(); + Variation CurrentUserVariation(string experimentId); + IEnumerable AssignUserVariations(); + IEnumerable AssignUserVariations(string value); + } +} diff --git a/Gibe.AbTest/Variation.cs b/Gibe.AbTest/Variation.cs index 9be6523..dfaecad 100644 --- a/Gibe.AbTest/Variation.cs +++ b/Gibe.AbTest/Variation.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Gibe.AbTest.Dto; +using Gibe.AbTest.Dto; using Newtonsoft.Json; namespace Gibe.AbTest @@ -30,7 +25,7 @@ public Variation(int id, int variationNumber, int weight, bool enabled, string d Definition = definition; DesktopOnly = desktopOnly; } - + public T GetDefinition() { return JsonConvert.DeserializeObject(Definition); diff --git a/Gibe.AbTest/packages.config b/Gibe.AbTest/packages.config index 6a51443..7eae9c9 100644 --- a/Gibe.AbTest/packages.config +++ b/Gibe.AbTest/packages.config @@ -1,6 +1,7 @@  +