From 8d8b2e279c77cf027b66b7d10eb75ac19f7a0618 Mon Sep 17 00:00:00 2001 From: msohailhussain Date: Fri, 14 Jun 2019 12:16:12 -0700 Subject: [PATCH] Revert "refac(decisions): Move forced variations into DecisionService. (#162)" This reverts commit 6441b001aabcf1fdc50fa550cc51227867b4bd45. --- OptimizelySDK.Tests/DecisionServiceTest.cs | 146 +----------------- OptimizelySDK.Tests/ProjectConfigTest.cs | 135 +++++++++++++++- OptimizelySDK/Bucketing/DecisionService.cs | 132 ++-------------- OptimizelySDK/Config/DatafileProjectConfig.cs | 123 ++++++++++++++- OptimizelySDK/Optimizely.cs | 67 +++----- OptimizelySDK/ProjectConfig.cs | 25 +++ 6 files changed, 313 insertions(+), 315 deletions(-) diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index d6a5aff2..d92dd495 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -148,8 +148,7 @@ public void TestGetForcedVariationWithInvalidVariation() {userId, invalidVariationKey } }; - var experiment = new Experiment - { + var experiment = new Experiment { Id = "1234", Key = "exp_key", Status = "Running", @@ -481,8 +480,7 @@ public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() { var booleanFeature = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var featureFlag = new FeatureFlag - { + var featureFlag = new FeatureFlag { Id = booleanFeature.Id, Key = booleanFeature.Key, RolloutId = booleanFeature.RolloutId, @@ -575,8 +573,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuckete public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() { var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var invalidRolloutFeature = new FeatureFlag - { + var invalidRolloutFeature = new FeatureFlag { RolloutId = "invalid_rollout_id", Id = featureFlag.Id, Key = featureFlag.Key, @@ -875,142 +872,5 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR } #endregion // GetVariationForFeature Tests - - #region Forced variation Tests - [Test] - public void TestSetGetForcedVariation() - { - var userId = "test_user"; - var invalidUserId = "invalid_user"; - var experimentKey = "test_experiment"; - var experimentKey2 = "group_experiment_1"; - var invalidExperimentKey = "invalid_experiment"; - var expectedVariationKey = "control"; - var expectedVariationKey2 = "group_exp_1_var_1"; - var invalidVariationKey = "invalid_variation"; - - var userAttributes = new UserAttributes - { - {"device_type", "iPhone" }, - {"location", "San Francisco" } - }; - - var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(), LoggerMock.Object); - optlyObject.Activate("test_experiment", "test_user", userAttributes); - - // invalid experiment key should return a null variation - Assert.False(DecisionService.SetForcedVariation(invalidExperimentKey, userId, expectedVariationKey, Config)); - Assert.Null(DecisionService.GetForcedVariation(invalidExperimentKey, userId, Config)); - - // setting a null variation should return a null variation - Assert.True(DecisionService.SetForcedVariation(experimentKey, userId, null, Config)); - Assert.Null(DecisionService.GetForcedVariation(experimentKey, userId, Config)); - - // setting an invalid variation should return a null variation - Assert.False(DecisionService.SetForcedVariation(experimentKey, userId, invalidVariationKey, Config)); - Assert.Null(DecisionService.GetForcedVariation(experimentKey, userId, Config)); - - // confirm the forced variation is returned after a set - Assert.True(DecisionService.SetForcedVariation(experimentKey, userId, expectedVariationKey, Config)); - var actualForcedVariation = DecisionService.GetForcedVariation(experimentKey, userId, Config); - Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); - - // check multiple sets - Assert.True(DecisionService.SetForcedVariation(experimentKey2, userId, expectedVariationKey2, Config)); - var actualForcedVariation2 = DecisionService.GetForcedVariation(experimentKey2, userId, Config); - Assert.AreEqual(expectedVariationKey2, actualForcedVariation2.Key); - // make sure the second set does not overwrite the first set - actualForcedVariation = DecisionService.GetForcedVariation(experimentKey, userId, Config); - Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); - // make sure unsetting the second experiment-to-variation mapping does not unset the - // first experiment-to-variation mapping - Assert.True(DecisionService.SetForcedVariation(experimentKey2, userId, null, Config)); - actualForcedVariation = DecisionService.GetForcedVariation(experimentKey, userId, Config); - Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); - - // an invalid user ID should return a null variation - Assert.Null(DecisionService.GetForcedVariation(experimentKey, invalidUserId, Config)); - } - - // test that all the logs in setForcedVariation are getting called - [Test] - public void TestSetForcedVariationLogs() - { - var userId = "test_user"; - var experimentKey = "test_experiment"; - var experimentId = "7716830082"; - var invalidExperimentKey = "invalid_experiment"; - var variationKey = "control"; - var variationId = "7722370027"; - var invalidVariationKey = "invalid_variation"; - - DecisionService.SetForcedVariation(invalidExperimentKey, userId, variationKey, Config); - DecisionService.SetForcedVariation(experimentKey, userId, null, Config); - DecisionService.SetForcedVariation(experimentKey, userId, invalidVariationKey, Config); - DecisionService.SetForcedVariation(experimentKey, userId, variationKey, Config); - - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(4)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, string.Format(@"Experiment key ""{0}"" is not in datafile.", invalidExperimentKey))); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Variation mapped to experiment ""{0}"" has been removed for user ""{1}"".", experimentKey, userId))); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, string.Format(@"No variation key ""{0}"" defined in datafile for experiment ""{1}"".", invalidVariationKey, experimentKey))); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); - } - - // test that all the logs in getForcedVariation are getting called - [Test] - public void TestGetForcedVariationLogs() - { - var userId = "test_user"; - var invalidUserId = "invalid_user"; - var experimentKey = "test_experiment"; - var experimentId = "7716830082"; - var invalidExperimentKey = "invalid_experiment"; - var pausedExperimentKey = "paused_experiment"; - var variationKey = "control"; - var variationId = "7722370027"; - - DecisionService.SetForcedVariation(experimentKey, userId, variationKey, Config); - DecisionService.GetForcedVariation(experimentKey, invalidUserId, Config); - DecisionService.GetForcedVariation(invalidExperimentKey, userId, Config); - DecisionService.GetForcedVariation(pausedExperimentKey, userId, Config); - DecisionService.GetForcedVariation(experimentKey, userId, Config); - - LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(5)); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"User ""{0}"" is not in the forced variation map.", invalidUserId))); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, string.Format(@"Experiment key ""{0}"" is not in datafile.", invalidExperimentKey))); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"No experiment ""{0}"" mapped to user ""{1}"" in the forced variation map.", pausedExperimentKey, userId))); - LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId))); - } - - [Test] - public void TestSetForcedVariationMultipleSets() - { - - Assert.True(DecisionService.SetForcedVariation("test_experiment", "test_user_1", "variation", Config)); - Assert.AreEqual(DecisionService.GetForcedVariation("test_experiment", "test_user_1", Config).Key, "variation"); - - // same user, same experiment, different variation - Assert.True(DecisionService.SetForcedVariation("test_experiment", "test_user_1", "control", Config)); - Assert.AreEqual(DecisionService.GetForcedVariation("test_experiment", "test_user_1", Config).Key, "control"); - - // same user, different experiment - Assert.True(DecisionService.SetForcedVariation("group_experiment_1", "test_user_1", "group_exp_1_var_1", Config)); - Assert.AreEqual(DecisionService.GetForcedVariation("group_experiment_1", "test_user_1", Config).Key, "group_exp_1_var_1"); - - // different user - Assert.True(DecisionService.SetForcedVariation("test_experiment", "test_user_2", "variation", Config)); - Assert.AreEqual(DecisionService.GetForcedVariation("test_experiment", "test_user_2", Config).Key, "variation"); - - // different user, different experiment - Assert.True(DecisionService.SetForcedVariation("group_experiment_1", "test_user_2", "group_exp_1_var_1", Config)); - Assert.AreEqual(DecisionService.GetForcedVariation("group_experiment_1", "test_user_2", Config).Key, "group_exp_1_var_1"); - - // make sure the first user forced variations are still valid - Assert.AreEqual(DecisionService.GetForcedVariation("test_experiment", "test_user_1", Config).Key, "control"); - Assert.AreEqual(DecisionService.GetForcedVariation("group_experiment_1", "test_user_1", Config).Key, "group_exp_1_var_1"); - } - - #endregion // Forced variation Tests } } \ No newline at end of file diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs index db2dfaee..47f47d2b 100644 --- a/OptimizelySDK.Tests/ProjectConfigTest.cs +++ b/OptimizelySDK.Tests/ProjectConfigTest.cs @@ -644,10 +644,143 @@ public void TempProjectConfigTest() Assert.AreEqual("1592310167", config.AccountId); } - // Test set/get forced variation for the following cases: + // test set/get forced variation for the following cases: // - valid and invalid user ID // - valid and invalid experiment key // - valid and invalid variation key, null variation key + [Test] + public void TestSetGetForcedVariation() + { + var userId = "test_user"; + var invalidUserId = "invalid_user"; + var experimentKey = "test_experiment"; + var experimentKey2 = "group_experiment_1"; + var invalidExperimentKey = "invalid_experiment"; + var expectedVariationKey = "control"; + var expectedVariationKey2 = "group_exp_1_var_1"; + var invalidVariationKey = "invalid_variation"; + + var userAttributes = new UserAttributes + { + {"device_type", "iPhone" }, + {"location", "San Francisco" } + }; + + var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(), LoggerMock.Object); + optlyObject.Activate("test_experiment", "test_user", userAttributes); + + // invalid experiment key should return a null variation + Assert.False(Config.SetForcedVariation(invalidExperimentKey, userId, expectedVariationKey)); + Assert.Null(Config.GetForcedVariation(invalidExperimentKey, userId)); + + // setting a null variation should return a null variation + Assert.True(Config.SetForcedVariation(experimentKey, userId, null)); + Assert.Null(Config.GetForcedVariation(experimentKey, userId)); + + // setting an invalid variation should return a null variation + Assert.False(Config.SetForcedVariation(experimentKey, userId, invalidVariationKey)); + Assert.Null(Config.GetForcedVariation(experimentKey, userId)); + + // confirm the forced variation is returned after a set + Assert.True(Config.SetForcedVariation(experimentKey, userId, expectedVariationKey)); + var actualForcedVariation = Config.GetForcedVariation(experimentKey, userId); + Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); + + // check multiple sets + Assert.True(Config.SetForcedVariation(experimentKey2, userId, expectedVariationKey2)); + var actualForcedVariation2 = Config.GetForcedVariation(experimentKey2, userId); + Assert.AreEqual(expectedVariationKey2, actualForcedVariation2.Key); + // make sure the second set does not overwrite the first set + actualForcedVariation = Config.GetForcedVariation(experimentKey, userId); + Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); + // make sure unsetting the second experiment-to-variation mapping does not unset the + // first experiment-to-variation mapping + Assert.True(Config.SetForcedVariation(experimentKey2, userId, null)); + actualForcedVariation = Config.GetForcedVariation(experimentKey, userId); + Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); + + // an invalid user ID should return a null variation + Assert.Null(Config.GetForcedVariation(experimentKey, invalidUserId)); + } + + // test that all the logs in setForcedVariation are getting called + [Test] + public void TestSetForcedVariationLogs() + { + var userId = "test_user"; + var experimentKey = "test_experiment"; + var experimentId = "7716830082"; + var invalidExperimentKey = "invalid_experiment"; + var variationKey = "control"; + var variationId = "7722370027"; + var invalidVariationKey = "invalid_variation"; + + Config.SetForcedVariation(invalidExperimentKey, userId, variationKey); + Config.SetForcedVariation(experimentKey, userId, null); + Config.SetForcedVariation(experimentKey, userId, invalidVariationKey); + Config.SetForcedVariation(experimentKey, userId, variationKey); + + LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(4)); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, string.Format(@"Experiment key ""{0}"" is not in datafile.", invalidExperimentKey))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Variation mapped to experiment ""{0}"" has been removed for user ""{1}"".", experimentKey, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, string.Format(@"No variation key ""{0}"" defined in datafile for experiment ""{1}"".", invalidVariationKey, experimentKey))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); + } + + // test that all the logs in getForcedVariation are getting called + [Test] + public void TestGetForcedVariationLogs() + { + var userId = "test_user"; + var invalidUserId = "invalid_user"; + var experimentKey = "test_experiment"; + var experimentId = "7716830082"; + var invalidExperimentKey = "invalid_experiment"; + var pausedExperimentKey = "paused_experiment"; + var variationKey = "control"; + var variationId = "7722370027"; + + Config.SetForcedVariation(experimentKey, userId, variationKey); + Config.GetForcedVariation(experimentKey, invalidUserId); + Config.GetForcedVariation(invalidExperimentKey, userId); + Config.GetForcedVariation(pausedExperimentKey, userId); + Config.GetForcedVariation(experimentKey, userId); + + LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(5)); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"User ""{0}"" is not in the forced variation map.", invalidUserId))); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, string.Format(@"Experiment key ""{0}"" is not in datafile.", invalidExperimentKey))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"No experiment ""{0}"" mapped to user ""{1}"" in the forced variation map.", pausedExperimentKey, userId))); + LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId))); + } + + [Test] + public void TestSetForcedVariationMultipleSets() + { + Assert.True(Config.SetForcedVariation("test_experiment", "test_user_1", "variation")); + Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_1").Key, "variation"); + + // same user, same experiment, different variation + Assert.True(Config.SetForcedVariation("test_experiment", "test_user_1", "control")); + Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_1").Key, "control"); + + // same user, different experiment + Assert.True(Config.SetForcedVariation("group_experiment_1", "test_user_1", "group_exp_1_var_1")); + Assert.AreEqual(Config.GetForcedVariation("group_experiment_1", "test_user_1").Key, "group_exp_1_var_1"); + + // different user + Assert.True(Config.SetForcedVariation("test_experiment", "test_user_2", "variation")); + Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_2").Key, "variation"); + + // different user, different experiment + Assert.True(Config.SetForcedVariation("group_experiment_1", "test_user_2", "group_exp_1_var_1")); + Assert.AreEqual(Config.GetForcedVariation("group_experiment_1", "test_user_2").Key, "group_exp_1_var_1"); + + // make sure the first user forced variations are still valid + Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_1").Key, "control"); + Assert.AreEqual(Config.GetForcedVariation("group_experiment_1", "test_user_1").Key, "group_exp_1_var_1"); + } + [Test] public void TestVariationFeatureEnabledProperty() { diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index ed8a8fa7..1fd75b15 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -39,15 +39,7 @@ public class DecisionService private IErrorHandler ErrorHandler; private UserProfileService UserProfileService; private ILogger Logger; - - /// - /// Associative array of user IDs to an associative array - /// of experiments to variations.This contains all the forced variations - /// set by the user by calling setForcedVariation (it is not the same as the - /// whitelisting forcedVariations data structure in the Experiments class). - /// - private Dictionary> ForcedVariationMap; - + /// /// Initialize a decision service for the Optimizely client. /// @@ -61,7 +53,6 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil ErrorHandler = errorHandler; UserProfileService = userProfileService; Logger = logger; - ForcedVariationMap = new Dictionary>(); } /// @@ -76,13 +67,13 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null; // check if a forced variation is set - var forcedVariation = GetForcedVariation(experiment.Key, userId, config); + var forcedVariation = config.GetForcedVariation(experiment.Key, userId); if (forcedVariation != null) return forcedVariation; var variation = GetWhitelistedVariation(experiment, userId); - if (variation != null) return variation; + if (variation != null) return variation; UserProfile userProfile = null; if (UserProfileService != null) { @@ -128,7 +119,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj } else Logger.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."); - } + } return variation; } @@ -137,107 +128,6 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj return null; } - /// - /// Gets the forced variation for the given user and experiment. - /// - /// The experiment key - /// The user ID - /// Project Config - /// Variation entity which the given user and experiment should be forced into. - public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) - { - if (ForcedVariationMap.ContainsKey(userId) == false) - { - Logger.Log(LogLevel.DEBUG, string.Format(@"User ""{0}"" is not in the forced variation map.", userId)); - return null; - } - - Dictionary experimentToVariationMap = ForcedVariationMap[userId]; - - string experimentId = config.GetExperimentFromKey(experimentKey).Id; - - // this case is logged in getExperimentFromKey - if (string.IsNullOrEmpty(experimentId)) - return null; - - if (experimentToVariationMap.ContainsKey(experimentId) == false) - { - Logger.Log(LogLevel.DEBUG, string.Format(@"No experiment ""{0}"" mapped to user ""{1}"" in the forced variation map.", experimentKey, userId)); - return null; - } - - string variationId = experimentToVariationMap[experimentId]; - - if (string.IsNullOrEmpty(variationId)) - { - Logger.Log(LogLevel.DEBUG, string.Format(@"No variation mapped to experiment ""{0}"" in the forced variation map.", experimentKey)); - return null; - } - - string variationKey = config.GetVariationFromId(experimentKey, variationId).Key; - - // this case is logged in getVariationFromKey - if (string.IsNullOrEmpty(variationKey)) - return null; - - Logger.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId)); - - Variation variation = config.GetVariationFromKey(experimentKey, variationKey); - - return variation; - } - - /// - /// Sets an associative array of user IDs to an associative array of experiments to forced variations. - /// - /// The experiment key - /// The user ID - /// The variation key - /// Project Config - /// A boolean value that indicates if the set completed successfully. - public bool SetForcedVariation(string experimentKey, string userId, string variationKey, ProjectConfig config) - { - // Empty variation key is considered as invalid. - if (variationKey != null && variationKey.Length == 0) - { - Logger.Log(LogLevel.DEBUG, "Variation key is invalid."); - return false; - } - - var experimentId = config.GetExperimentFromKey(experimentKey).Id; - - // this case is logged in getExperimentFromKey - if (string.IsNullOrEmpty(experimentId)) - return false; - - // clear the forced variation if the variation key is null - if (variationKey == null) - { - if (ForcedVariationMap.ContainsKey(userId) && ForcedVariationMap[userId].ContainsKey(experimentId)) - ForcedVariationMap[userId].Remove(experimentId); - - Logger.Log(LogLevel.DEBUG, string.Format(@"Variation mapped to experiment ""{0}"" has been removed for user ""{1}"".", experimentKey, userId)); - return true; - } - - string variationId = config.GetVariationFromKey(experimentKey, variationKey).Id; - - // this case is logged in getVariationFromKey - if (string.IsNullOrEmpty(variationId)) - return false; - - // Add User if not exist. - if (ForcedVariationMap.ContainsKey(userId) == false) - ForcedVariationMap[userId] = new Dictionary(); - - // Add/Replace Experiment to Variation ID map. - ForcedVariationMap[userId][experimentId] = variationId; - - Logger.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId)); - return true; - } - - /// /// Get the variation the user has been whitelisted into. /// @@ -255,7 +145,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) string forcedVariationKey = userIdToVariationKeyMap[userId]; Variation forcedVariation = experiment.VariationKeyToVariationMap.ContainsKey(forcedVariationKey) - ? experiment.VariationKeyToVariationMap[forcedVariationKey] + ? experiment.VariationKeyToVariationMap[forcedVariationKey] : null; if (forcedVariation != null) @@ -292,8 +182,8 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi { string variationId = decision.VariationId; - Variation savedVariation = config.ExperimentIdMap[experimentId].VariationIdToVariationMap.ContainsKey(variationId) - ? config.ExperimentIdMap[experimentId].VariationIdToVariationMap[variationId] + Variation savedVariation = config.ExperimentIdMap[experimentId].VariationIdToVariationMap.ContainsKey(variationId) + ? config.ExperimentIdMap[experimentId].VariationIdToVariationMap[variationId] : null; if (savedVariation == null) @@ -386,12 +276,12 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature Variation variation = null; var rolloutRulesLength = rollout.Experiments.Count; - + // Get Bucketing ID from user attributes. string bucketingId = GetBucketingId(userId, filteredAttributes); // For all rules before the everyone else rule - for (int i = 0; i < rolloutRulesLength - 1; i++) + for (int i=0; i < rolloutRulesLength - 1; i++) { var rolloutRule = rollout.Experiments[i]; if (ExperimentUtils.IsUserInExperiment(config, rolloutRule, filteredAttributes, Logger)) @@ -492,7 +382,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, s Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); return decision; } - + Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); } @@ -524,4 +414,4 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes) return bucketingId; } } -} +} \ No newline at end of file diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index 940c44ee..bd9b6e87 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -102,14 +102,14 @@ public enum OPTLYSDKVersion /// /// Associative array of experiment ID to Experiment(s) in the datafile /// - private Dictionary _ExperimentIdMap + private Dictionary _ExperimentIdMap = new Dictionary(); public Dictionary ExperimentIdMap { get { return _ExperimentIdMap; } } /// /// Associative array of experiment key to associative array of variation key to variations /// - private Dictionary> _VariationKeyMap + private Dictionary> _VariationKeyMap = new Dictionary>(); public Dictionary> VariationKeyMap { get { return _VariationKeyMap; } } @@ -117,7 +117,7 @@ private Dictionary> _VariationKeyMap /// /// Associative array of experiment key to associative array of variation ID to variations /// - private Dictionary> _VariationIdMap + private Dictionary> _VariationIdMap = new Dictionary>(); public Dictionary> VariationIdMap { get { return _VariationIdMap; } } @@ -139,6 +139,14 @@ private Dictionary> _VariationIdMap private Dictionary _AudienceIdMap; public Dictionary AudienceIdMap { get { return _AudienceIdMap; } } + /// + /// Associative array of user IDs to an associative array + /// of experiments to variations.This contains all the forced variations + /// set by the user by calling setForcedVariation (it is not the same as the + /// whitelisting forcedVariations data structure in the Experiments class). + /// + private Dictionary> _ForcedVariationMap; + public Dictionary> ForcedVariationMap { get { return _ForcedVariationMap; } } /// /// Associative array of Feature Key to Feature(s) in the datafile @@ -194,7 +202,7 @@ private Dictionary> _VariationIdMap /// Associative list of Attributes. /// public Attribute[] Attributes { get; set; } - + /// /// Associative list of Audiences. /// @@ -233,6 +241,7 @@ private void Initialize() FeatureFlags = FeatureFlags ?? new FeatureFlag[0]; Rollouts = Rollouts ?? new Rollout[0]; + _ForcedVariationMap = new Dictionary>(); _GroupIdMap = ConfigParser.GenerateMap(entities: Groups, getKey: g => g.Id.ToString(), clone: true); _ExperimentKeyMap = ConfigParser.GenerateMap(entities: Experiments, getKey: e => e.Key, clone: true); _EventKeyMap = ConfigParser.GenerateMap(entities: Events, getKey: e => e.Key, clone: true); @@ -254,7 +263,7 @@ private void Initialize() experiment.GroupId = group.Id; experiment.GroupPolicy = group.Policy; } - + // RJE: I believe that this is equivalent to this: // $this->_experimentKeyMap = array_merge($this->_experimentKeyMap, $experimentsInGroup); foreach (string key in experimentsInGroup.Keys) @@ -284,7 +293,7 @@ private void Initialize() { _VariationKeyMap[rolloutRule.Key] = new Dictionary(); _VariationIdMap[rolloutRule.Key] = new Dictionary(); - + if (rolloutRule.Variations != null) { foreach (var variation in rolloutRule.Variations) @@ -455,7 +464,7 @@ public Variation GetVariationFromKey(string experimentKey, string variationKey) _VariationKeyMap[experimentKey].ContainsKey(variationKey)) return _VariationKeyMap[experimentKey][variationKey]; - string message = string.Format(@"No variation key ""{0}"" defined in datafile for experiment ""{1}"".", + string message = string.Format(@"No variation key ""{0}"" defined in datafile for experiment ""{1}"".", variationKey, experimentKey); Logger.Log(LogLevel.ERROR, message); ErrorHandler.HandleError(new Exceptions.InvalidVariationException("Provided variation is not in datafile.")); @@ -482,6 +491,104 @@ public Variation GetVariationFromId(string experimentKey, string variationId) return new Variation(); } + /// + /// Gets the forced variation for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// Variation entity which the given user and experiment should be forced into. + public Variation GetForcedVariation(string experimentKey, string userId) + { + if (_ForcedVariationMap.ContainsKey(userId) == false) + { + Logger.Log(LogLevel.DEBUG, string.Format(@"User ""{0}"" is not in the forced variation map.", userId)); + return null; + } + + Dictionary experimentToVariationMap = _ForcedVariationMap[userId]; + + string experimentId = GetExperimentFromKey(experimentKey).Id; + + // this case is logged in getExperimentFromKey + if (string.IsNullOrEmpty(experimentId)) + return null; + + if (experimentToVariationMap.ContainsKey(experimentId) == false) + { + Logger.Log(LogLevel.DEBUG, string.Format(@"No experiment ""{0}"" mapped to user ""{1}"" in the forced variation map.", experimentKey, userId)); + return null; + } + + string variationId = experimentToVariationMap[experimentId]; + + if (string.IsNullOrEmpty(variationId)) + { + Logger.Log(LogLevel.DEBUG, string.Format(@"No variation mapped to experiment ""{0}"" in the forced variation map.", experimentKey)); + return null; + } + + string variationKey = GetVariationFromId(experimentKey, variationId).Key; + + // this case is logged in getVariationFromKey + if (string.IsNullOrEmpty(variationKey)) + return null; + + Logger.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId)); + + Variation variation = GetVariationFromKey(experimentKey, variationKey); + + return variation; + } + + /// + /// Sets an associative array of user IDs to an associative array of experiments to forced variations. + /// + /// The experiment key + /// The user ID + /// The variation key + /// A boolean value that indicates if the set completed successfully. + public bool SetForcedVariation(string experimentKey, string userId, string variationKey) + { + // Empty variation key is considered as invalid. + if (variationKey != null && variationKey.Length == 0) + { + Logger.Log(LogLevel.DEBUG, "Variation key is invalid."); + return false; + } + + var experimentId = GetExperimentFromKey(experimentKey).Id; + + // this case is logged in getExperimentFromKey + if (string.IsNullOrEmpty(experimentId)) + return false; + + // clear the forced variation if the variation key is null + if (variationKey == null) + { + if (_ForcedVariationMap.ContainsKey(userId) && _ForcedVariationMap[userId].ContainsKey(experimentId)) + _ForcedVariationMap[userId].Remove(experimentId); + + Logger.Log(LogLevel.DEBUG, string.Format(@"Variation mapped to experiment ""{0}"" has been removed for user ""{1}"".", experimentKey, userId)); + return true; + } + + string variationId = GetVariationFromKey(experimentKey, variationKey).Id; + + // this case is logged in getVariationFromKey + if (string.IsNullOrEmpty(variationId)) + return false; + + // Add User if not exist. + if (_ForcedVariationMap.ContainsKey(userId) == false) + _ForcedVariationMap[userId] = new Dictionary(); + + // Add/Replace Experiment to Variation ID map. + _ForcedVariationMap[userId][experimentId] = variationId; + + Logger.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId)); + return true; + } + /// /// Get the feature from the key /// @@ -521,7 +628,7 @@ public Rollout GetRolloutFromId(string rolloutId) /// Attribute ID corresponding to the provided attribute key. Attribute key if it is a reserved attribute public string GetAttributeId(string attributeKey) { - + var hasReservedPrefix = attributeKey.StartsWith(RESERVED_ATTRIBUTE_PREFIX); if (_AttributeKeyMap.ContainsKey(attributeKey)) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 7112f0a1..c0d48033 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -51,10 +51,8 @@ public class Optimizely : IOptimizely public bool IsValid { get; private set; } - public static String SDK_VERSION - { - get - { + public static String SDK_VERSION { + get { // Example output: "2.1.0" . Should be kept in synch with NuGet package version. #if NET35 Assembly assembly = Assembly.GetExecutingAssembly(); @@ -69,10 +67,8 @@ public static String SDK_VERSION } } - public static String SDK_TYPE - { - get - { + public static String SDK_TYPE { + get { return "csharp-sdk"; } } @@ -98,25 +94,19 @@ public Optimizely(string datafile, UserProfileService userProfileService = null, bool skipJsonValidation = false) { - try - { + try { IsValid = false; // invalid until proven valid Initialize(eventDispatcher, logger, errorHandler, userProfileService); - if (ValidateInputs(datafile, skipJsonValidation)) - { + if (ValidateInputs(datafile, skipJsonValidation)) { var config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); IsValid = true; ProjectConfigManager = new FallbackProjectConfigManager(config); - } - else - { + } else { Logger.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."); } - } - catch (Exception ex) - { + } catch (Exception ex) { string error = String.Empty; if (ex.GetType() == typeof(ConfigParseException)) error = ex.Message; @@ -259,7 +249,7 @@ private bool ValidateInputs(string datafile, bool skipJsonValidation) public void Track(string eventKey, string userId, UserAttributes userAttributes = null, EventTags eventTags = null) { var config = ProjectConfigManager?.GetConfig(); - if (!IsValid && config == null) + if (!IsValid && config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'track'."); return; @@ -276,7 +266,7 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes var eevent = config.GetEvent(eventKey); - if (eevent.Key == null) + if (eevent.Key == null) { Logger.Log(LogLevel.INFO, string.Format("Not tracking user {0} for event {1}.", userId, eventKey)); return; @@ -294,11 +284,11 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes Logger.Log(LogLevel.DEBUG, string.Format("Dispatching conversion event to URL {0} with params {1}.", conversionEvent.Url, conversionEvent.GetParamsAsJson())); - try + try { EventDispatcher.DispatchEvent(conversionEvent); - } - catch (Exception exception) + } + catch (Exception exception) { Logger.Log(LogLevel.ERROR, string.Format("Unable to dispatch conversion event. Error {0}", exception.Message)); } @@ -331,8 +321,7 @@ public Variation GetVariation(string experimentKey, string userId, UserAttribute /// null|Variation Representing variation private Variation GetVariation(string experimentKey, string userId, ProjectConfig config, UserAttributes userAttributes = null) { - if (!IsValid && config == null) - { + if (!IsValid && config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetVariation'."); return null; } @@ -386,7 +375,7 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia { EXPERIMENT_KEY, experimentKey } }; - return ValidateStringInputs(inputValues) && DecisionService.SetForcedVariation(experimentKey, userId, variationKey, config); + return ValidateStringInputs(inputValues) && config.SetForcedVariation(experimentKey, userId, variationKey); } /// @@ -398,8 +387,7 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia public Variation GetForcedVariation(string experimentKey, string userId) { var config = ProjectConfigManager?.GetConfig(); - if (!IsValid && config == null) - { + if (!IsValid && config == null) { return null; } @@ -412,7 +400,7 @@ public Variation GetForcedVariation(string experimentKey, string userId) if (!ValidateStringInputs(inputValues)) return null; - return DecisionService.GetForcedVariation(experimentKey, userId, config); + return config.GetForcedVariation(experimentKey, userId); } #region FeatureFlag APIs @@ -428,8 +416,7 @@ public Variation GetForcedVariation(string experimentKey, string userId) public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttributes userAttributes = null) { var config = ProjectConfigManager?.GetConfig(); - if (!IsValid && config == null) - { + if (!IsValid && config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetVariation'."); @@ -500,7 +487,7 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri /// The user's attributes /// Variable type /// string | null Feature variable value - public virtual T GetFeatureVariableValueForType(string featureKey, string variableKey, string userId, + public virtual T GetFeatureVariableValueForType(string featureKey, string variableKey, string userId, UserAttributes userAttributes, FeatureVariable.VariableType variableType, ProjectConfig config) { @@ -601,8 +588,7 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var { var config = ProjectConfigManager?.GetConfig(); - if (!IsValid && config == null) - { + if (!IsValid && config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'activate'."); return null; } @@ -621,8 +607,7 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var public double? GetFeatureVariableDouble(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) { var config = ProjectConfigManager?.GetConfig(); - if (!IsValid && config == null) - { + if (!IsValid && config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'activate'."); return null; } @@ -641,8 +626,7 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var public int? GetFeatureVariableInteger(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) { var config = ProjectConfigManager?.GetConfig(); - if (!IsValid && config == null) - { + if (!IsValid && config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'activate'."); return null; } @@ -661,8 +645,7 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var public string GetFeatureVariableString(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) { var config = ProjectConfigManager?.GetConfig(); - if (!IsValid && config == null) - { + if (!IsValid && config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'activate'."); return null; } @@ -758,7 +741,7 @@ private bool ValidateStringInputs(Dictionary inputs) inputs.Remove(USER_ID); } - foreach (var input in inputs) + foreach(var input in inputs) { if (string.IsNullOrEmpty(input.Value)) { @@ -769,7 +752,7 @@ private bool ValidateStringInputs(Dictionary inputs) return isValid; } - + private object GetTypeCastedVariableValue(string value, FeatureVariable.VariableType type) { object result = null; diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs index 9b04249a..148a74d7 100644 --- a/OptimizelySDK/ProjectConfig.cs +++ b/OptimizelySDK/ProjectConfig.cs @@ -96,6 +96,14 @@ public interface ProjectConfig /// Dictionary AudienceIdMap { get; } + /// + /// Associative array of user IDs to an associative array + /// of experiments to variations.This contains all the forced variations + /// set by the user by calling setForcedVariation (it is not the same as the + /// whitelisting forcedVariations data structure in the Experiments class). + /// + Dictionary> ForcedVariationMap { get; } + /// /// Associative array of Feature Key to Feature(s) in the datafile /// @@ -211,6 +219,23 @@ public interface ProjectConfig /// entity if key or ID is invalid Variation GetVariationFromId(string experimentKey, string variationId); + /// + /// Gets the forced variation for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// Variation entity which the given user and experiment should be forced into. + Variation GetForcedVariation(string experimentKey, string userId); + + /// + /// Sets an associative array of user IDs to an associative array of experiments to forced variations. + /// + /// The experiment key + /// The user ID + /// The variation key + /// A boolean value that indicates if the set completed successfully. + bool SetForcedVariation(string experimentKey, string userId, string variationKey); + /// /// Get the feature from the key ///