diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index af6251e9..9d3d441d 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -322,6 +322,9 @@ OptimizelyDecisionContext.cs + + + ForcedDecisionsStore.cs OptimizelyForcedDecision.cs diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 67c0d250..3dfdfbc9 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -345,6 +345,9 @@ OptimizelyDecisionContext.cs + + + ForcedDecisionsStore.cs OptimizelyForcedDecision.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index d2ca8243..740e5d4d 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -96,6 +96,7 @@ + OptimizelyForcedDecision.cs diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index bbf3bb1c..0202fe84 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -307,6 +307,9 @@ OptimizelyDecisionContext.cs + + ForcedDecisionsStore.cs + OptimizelyForcedDecision.cs diff --git a/OptimizelySDK.Tests/ConfigTest/AtomicProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/FallbackProjectConfigManagerTest.cs similarity index 100% rename from OptimizelySDK.Tests/ConfigTest/AtomicProjectConfigManagerTest.cs rename to OptimizelySDK.Tests/ConfigTest/FallbackProjectConfigManagerTest.cs diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index fd2521dc..f746ac3f 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -82,10 +82,10 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval() OptimizelyUserContextMock = new Mock(optlyObject, WhitelistedUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(GenericUserId); // user excluded without audiences and whitelisting - Assert.IsNull(decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes()).ResultObject); + Assert.IsNull(decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig).ResultObject); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(WhitelistedUserId); - var actualVariation = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes()); + var actualVariation = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once); @@ -110,7 +110,7 @@ public void TestGetVariationLogsErrorWhenUserProfileMapItsNull() var options = new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS }; OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(GenericUserId); - var variationResult = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes(), options); + var variationResult = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, options); Assert.AreEqual(variationResult.DecisionReasons.ToReport(true)[0], "We were unable to get a user profile map from the UserProfileService."); Assert.AreEqual(variationResult.DecisionReasons.ToReport(true)[1], "Audiences for experiment \"etag3\" collectively evaluated to FALSE"); Assert.AreEqual(variationResult.DecisionReasons.ToReport(true)[2], "User \"genericUserId\" does not meet conditions to be in experiment \"etag3\"."); @@ -136,7 +136,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes()); + decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" does not meet conditions to be in experiment \"{1}\".", GenericUserId, experiment.Key)), Times.Once); @@ -144,7 +144,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(UserProfileId); // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation - decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes()); + decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig); BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -234,7 +234,7 @@ public void TestBucketReturnsVariationStoredInUserProfile() OptimizelyUserContextMock = new Mock(optlyObject, WhitelistedUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(UserProfileId); - var actualVariation = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes()); + var actualVariation = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig); Assertions.AreEqual(variation, actualVariation.ResultObject); @@ -316,7 +316,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile() OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(UserProfileId); - Assert.IsTrue(TestData.CompareObjects(variation.ResultObject, decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes()).ResultObject)); + Assert.IsTrue(TestData.CompareObjects(variation.ResultObject, decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig).ResultObject)); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("Saved variation \"{0}\" of experiment \"{1}\" for user \"{2}\".", variation.ResultObject.Id, experiment.Id, UserProfileId)), Times.Once); @@ -374,7 +374,7 @@ public void TestGetVariationSavesANewUserProfile() UserProfileServiceMock.Object, LoggerMock.Object); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(UserProfileId); - var actualVariation = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, new UserAttributes()); + var actualVariation = decisionService.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig); Assertions.AreEqual(variation.ResultObject, actualVariation.ResultObject); @@ -578,12 +578,12 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(), LoggerMock.Object); - OptimizelyUserContextMock = new Mock(optlyObject, WhitelistedUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); + OptimizelyUserContextMock = new Mock(optlyObject, WhitelistedUserId, userAttributes, ErrorHandlerMock.Object, LoggerMock.Object); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns("user1"); DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), - OptimizelyUserContextMock.Object, ProjectConfig, userAttributes, It.IsAny())).Returns(variation); + OptimizelyUserContextMock.Object, ProjectConfig, It.IsAny())).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, OptimizelyUserContextMock.Object, userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }); @@ -604,11 +604,10 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(), LoggerMock.Object); - OptimizelyUserContextMock = new Mock(optlyObject, WhitelistedUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); + OptimizelyUserContextMock = new Mock(optlyObject, WhitelistedUserId, userAttributes, ErrorHandlerMock.Object, LoggerMock.Object); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns("user1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("group_experiment_1"), OptimizelyUserContextMock.Object, ProjectConfig, - userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("group_experiment_1"), OptimizelyUserContextMock.Object, ProjectConfig)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, OptimizelyUserContextMock.Object, userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }); @@ -625,7 +624,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuckete var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1"); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns("user1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny())) + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny())) .Returns(Result.NullResult(null)); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); @@ -975,7 +974,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR OptimizelyUserContextMock = new Mock(optlyObject, WhitelistedUserId, userAttributes, ErrorHandlerMock.Object, LoggerMock.Object); OptimizelyUserContextMock.Setup(ouc => ouc.GetUserId()).Returns(UserProfileId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, userAttributes, It.IsAny())).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, OptimizelyUserContextMock.Object, ProjectConfig, It.IsAny())).Returns(variation); var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, OptimizelyUserContextMock.Object, userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }); // The user is bucketed into feature experiment's variation. diff --git a/OptimizelySDK.Tests/ForcedDecisionsStoreTest.cs b/OptimizelySDK.Tests/ForcedDecisionsStoreTest.cs new file mode 100644 index 00000000..6cf2dfc8 --- /dev/null +++ b/OptimizelySDK.Tests/ForcedDecisionsStoreTest.cs @@ -0,0 +1,123 @@ +/** + * + * Copyright 2021, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NUnit.Framework; + +namespace OptimizelySDK.Tests +{ + [TestFixture] + public class ForcedDecisionsStoreTest + { + [Test] + public void ForcedDecisionStoreGetSetForcedDecisionWithBothRuleAndFlagKey() + { + var expectedForcedDecision1 = new OptimizelyForcedDecision("sample_variation_key"); + var expectedForcedDecision2 = new OptimizelyForcedDecision("sample_variation_key_2"); + var context1 = new OptimizelyDecisionContext("flag_key", "rule_key"); + var context2 = new OptimizelyDecisionContext("flag_key", "rule_key1"); + var forcedDecisionStore = new ForcedDecisionsStore(); + forcedDecisionStore[context1] = expectedForcedDecision1; + forcedDecisionStore[context2] = expectedForcedDecision2; + + Assert.AreEqual(forcedDecisionStore.Count, 2); + Assert.AreEqual(forcedDecisionStore[context1].VariationKey, expectedForcedDecision1.VariationKey); + Assert.AreEqual(forcedDecisionStore[context2].VariationKey, expectedForcedDecision2.VariationKey); + } + + [Test] + public void ForcedDecisionStoreNullFlagKeyForcedDecisionContext() + { + var expectedForcedDecision = new OptimizelyForcedDecision("sample_variation_key"); + var context = new OptimizelyDecisionContext(null, "rule_key"); + var forcedDecisionStore = new ForcedDecisionsStore(); + forcedDecisionStore[context] = expectedForcedDecision; + + Assert.AreEqual(forcedDecisionStore.Count, 0); + } + + [Test] + public void ForcedDecisionStoreNullContextForcedDecisionContext() + { + var expectedForcedDecision = new OptimizelyForcedDecision("sample_variation_key"); + OptimizelyDecisionContext context = null; + var forcedDecisionStore = new ForcedDecisionsStore(); + forcedDecisionStore[context] = expectedForcedDecision; + + Assert.AreEqual(forcedDecisionStore.Count, 0); + } + + [Test] + public void ForcedDecisionStoreGetForcedDecisionWithBothRuleAndFlagKey() + { + var expectedForcedDecision1 = new OptimizelyForcedDecision("sample_variation_key"); + var context1 = new OptimizelyDecisionContext("flag_key", "rule_key"); + var NullFlagKeyContext = new OptimizelyDecisionContext(null, "rule_key"); + var forcedDecisionStore = new ForcedDecisionsStore(); + forcedDecisionStore[context1] = expectedForcedDecision1; + + Assert.AreEqual(forcedDecisionStore.Count, 1); + Assert.AreEqual(forcedDecisionStore[context1].VariationKey, expectedForcedDecision1.VariationKey); + Assert.IsNull(forcedDecisionStore[NullFlagKeyContext]); + } + + [Test] + public void ForcedDecisionStoreRemoveForcedDecisionTrue() + { + var expectedForcedDecision1 = new OptimizelyForcedDecision("sample_variation_key"); + var expectedForcedDecision2 = new OptimizelyForcedDecision("sample_variation_key_2"); + var context1 = new OptimizelyDecisionContext("flag_key", "rule_key"); + var context2 = new OptimizelyDecisionContext("flag_key", "rule_key1"); + var forcedDecisionStore = new ForcedDecisionsStore(); + forcedDecisionStore[context1] = expectedForcedDecision1; + forcedDecisionStore[context2] = expectedForcedDecision2; + + Assert.AreEqual(forcedDecisionStore.Count, 2); + Assert.IsTrue(forcedDecisionStore.Remove(context2)); + Assert.AreEqual(forcedDecisionStore.Count, 1); + Assert.AreEqual(forcedDecisionStore[context1].VariationKey, expectedForcedDecision1.VariationKey); + Assert.IsNull(forcedDecisionStore[context2]); + } + + [Test] + public void ForcedDecisionStoreRemoveForcedDecisionContextRuleKeyNotMatched() + { + var expectedForcedDecision = new OptimizelyForcedDecision("sample_variation_key"); + var contextNotMatched = new OptimizelyDecisionContext("flag_key", ""); + var context = new OptimizelyDecisionContext("flag_key", "rule_key"); + var forcedDecisionStore = new ForcedDecisionsStore(); + forcedDecisionStore[context] = expectedForcedDecision; + + Assert.AreEqual(forcedDecisionStore.Count, 1); + Assert.IsFalse(forcedDecisionStore.Remove(contextNotMatched)); + Assert.AreEqual(forcedDecisionStore.Count, 1); + } + + [Test] + public void ForcedDecisionStoreRemoveAllForcedDecisionContext() + { + var expectedForcedDecision = new OptimizelyForcedDecision("sample_variation_key"); + var context = new OptimizelyDecisionContext("flag_key", "rule_key"); + var forcedDecisionStore = new ForcedDecisionsStore(); + forcedDecisionStore[context] = expectedForcedDecision; + + Assert.AreEqual(forcedDecisionStore.Count, 1); + forcedDecisionStore.RemoveAll(); + Assert.AreEqual(forcedDecisionStore.Count, 0); + } + + } +} diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index 231e256b..2a776bcb 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -75,7 +75,7 @@ - + @@ -93,6 +93,7 @@ + diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 3e9f0483..05e50bc9 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -2438,7 +2438,7 @@ public void TestActivateListener(UserAttributes userAttributes) Mock mockUserContext = new Mock(OptimizelyMock.Object, TestUserId, userAttributes, ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), It.IsAny(), userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), It.IsAny())).Returns(variation); DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, It.IsAny(), It.IsAny())).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); @@ -2452,8 +2452,8 @@ public void TestActivateListener(UserAttributes userAttributes) optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); // Calling Activate and IsFeatureEnabled. - optly.Invoke("Activate", experimentKey, TestUserId, userAttributes); - optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, userAttributes); + optStronglyTyped.Activate(experimentKey, TestUserId, userAttributes); + optStronglyTyped.IsFeatureEnabled(featureKey, TestUserId, userAttributes); // Verify that all the registered callbacks are called once for both Activate and IsFeatureEnabled. EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Exactly(2)); @@ -2521,7 +2521,7 @@ public void TestTrackListener(UserAttributes userAttributes, EventTags eventTags Mock mockUserContext = new Mock(Optimizely, TestUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); // Adding notification listeners. var notificationType = NotificationCenter.NotificationType.Track; @@ -2559,7 +2559,7 @@ public void TestActivateSendsDecisionNotificationWithActualVariationKey() NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); @@ -2596,7 +2596,7 @@ public void TestActivateSendsDecisionNotificationWithVariationKeyAndTypeFeatureT NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); @@ -2668,7 +2668,7 @@ public void TestGetVariationSendsDecisionNotificationWithActualVariationKey() Mock mockUserContext = new Mock(optStronglyTyped, TestUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2707,7 +2707,7 @@ public void TestGetVariationSendsDecisionNotificationWithVariationKeyAndTypeFeat Mock mockUserContext = new Mock(optStronglyTyped, TestUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, It.IsAny(), Config)).Returns(variation); optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2735,11 +2735,7 @@ public void TestGetVariationSendsDecisionNotificationWithNullVariationKey() NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - Mock mockUserContext = new Mock(optStronglyTyped, TestUserId, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object); - - mockUserContext.Setup(ouc => ouc.GetUserId()).Returns(TestUserId); - - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Result.NullResult(null)); + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Result.NullResult(null)); //DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, null)).Returns(Result.NullResult(null)); optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 0c694eda..d5ab832a 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -186,16 +186,17 @@ public void TestDecide() } [Test] - public void TestForcedDecisionReturnsCorrectGetDecisionKey() + public void TestForcedDecisionReturnsCorrectFlagAndRuleKeys() { var user = Optimizely.CreateUserContext(UserID); var context = new OptimizelyDecisionContext("flag", null); - - Assert.AreEqual("flag-$opt$-$opt-null-rule-key", context.DecisionKey); + Assert.AreEqual("flag", context.FlagKey); + Assert.Null(context.RuleKey); context = new OptimizelyDecisionContext("flag", "ruleKey"); - Assert.AreEqual("flag-$opt$-ruleKey", context.DecisionKey); + Assert.AreEqual("flag", context.FlagKey); + Assert.AreEqual("ruleKey", context.RuleKey); } [Test] @@ -226,7 +227,7 @@ public void TestGetForcedDecisionReturnsNullIfInvalidConfig() [Test] public void TestSetForcedDecisionReturnsFalseForNullConfig() { - var optly = new Optimizely(new FallbackProjectConfigManager(null)); + var optly = new Optimizely(new FallbackProjectConfigManager(null), logger: LoggerMock.Object); var user = optly.CreateUserContext(UserID); @@ -235,7 +236,7 @@ public void TestSetForcedDecisionReturnsFalseForNullConfig() var result = user.SetForcedDecision(context, decision); Assert.IsFalse(result); - //TODO: should assert logger is called + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Optimizely SDK not configured properly yet."), Times.Once); } [Test] @@ -310,7 +311,7 @@ public void TestFindValidatedForcedDecisionReturnsCorrectDecisionWithNullVariati var context = new OptimizelyDecisionContext("flagKey", "ruleKey"); - var result = user.FindValidatedForcedDecision(context); + var result = user.FindValidatedForcedDecision(context, null); Assertions.AreEqual(expectedResult, result); } diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs index 455bbc55..03faf85d 100644 --- a/OptimizelySDK.Tests/ProjectConfigTest.cs +++ b/OptimizelySDK.Tests/ProjectConfigTest.cs @@ -427,44 +427,43 @@ public void TestFlagVariations() { var allVariations = Config?.FlagVariationMap; - var expectedVariations1 = new List>>(); - var expectedVariationList = new List + var expectedVariationDict = new Dictionary { - new Variation - { - FeatureEnabled = true, - Id = "7722260071", - Key = "group_exp_1_var_1", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_1_v1" } } - }, - new Variation - { - FeatureEnabled = true, - Id = "7722360022", - Key = "group_exp_1_var_2", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_1_v2" } } - }, - new Variation - { - FeatureEnabled = false, - Id = "7713030086", - Key = "group_exp_2_var_1", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_2_v1" } } - }, - new Variation - { - FeatureEnabled = false, - Id = "7725250007", - Key = "group_exp_2_var_2", - FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_2_v2" } } + { "group_exp_1_var_1", new Variation + { + FeatureEnabled = true, + Id = "7722260071", + Key = "group_exp_1_var_1", + FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_1_v1" } } + } + }, + { "group_exp_1_var_2", new Variation + { + FeatureEnabled = true, + Id = "7722360022", + Key = "group_exp_1_var_2", + FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_1_v2" } } + } + }, + { "group_exp_2_var_1", new Variation + { + FeatureEnabled = false, + Id = "7713030086", + Key = "group_exp_2_var_1", + FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_2_v1" } } + } + }, + { "group_exp_2_var_2", new Variation + { + FeatureEnabled = false, + Id = "7725250007", + Key = "group_exp_2_var_2", + FeatureVariableUsageInstances = new List { new FeatureVariableUsage { Id = "155563", Value= "groupie_2_v2" } } + } } }; - expectedVariations1.Add(new Dictionary> { { "boolean_feature", expectedVariationList } }); - - var variations1 = allVariations.Where(v => v.Key == "boolean_feature").Select(v => new Dictionary> { { v.Key, v.Value } }).ToList(); - - TestData.CompareObjects(expectedVariations1, variations1); - Assertions.AreEquivalent(expectedVariations1, variations1); + var filteredActualFlagVariations = allVariations["boolean_feature"]; + TestData.CompareObjects(expectedVariationDict, filteredActualFlagVariations); } [Test] diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index feef3ab6..0796aa0b 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -81,28 +81,27 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil /// Get a Variation of an Experiment for a user to be allocated into. /// /// The Experiment the user will be bucketed into. - /// The userId of the user. - /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// Optimizely user context. + /// Project config. /// The Variation the user is allocated into. public virtual Result GetVariation(Experiment experiment, OptimizelyUserContext user, - ProjectConfig config, - UserAttributes filteredAttributes) + ProjectConfig config) { - return GetVariation(experiment, user, config, filteredAttributes, new OptimizelyDecideOption[] { }); + return GetVariation(experiment, user, config, new OptimizelyDecideOption[] { }); } /// /// Get a Variation of an Experiment for a user to be allocated into. /// /// The Experiment the user will be bucketed into. - /// The userId of the user. - /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// optimizely user context. + /// Project Config. + /// An array of decision options. /// The Variation the user is allocated into. public virtual Result GetVariation(Experiment experiment, OptimizelyUserContext user, ProjectConfig config, - UserAttributes filteredAttributes, OptimizelyDecideOption[] options) { var reasons = new DecisionReasons(); @@ -159,6 +158,7 @@ public virtual Result GetVariation(Experiment experiment, ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } + var filteredAttributes = user.GetAttributes(); var doesUserMeetAudienceConditionsResult = ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger); reasons += doesUserMeetAudienceConditionsResult.DecisionReasons; if (doesUserMeetAudienceConditionsResult.ResultObject) @@ -449,20 +449,68 @@ public virtual Result GetVariationForFeatureRollout(FeatureFlag return Result.NullResult(reasons); } + var userId = user.GetUserId(); + var attributes = user.GetAttributes(); + var index = 0; while (index < rolloutRulesLength) { - var decisionResult = GetVariationFromDeliveryRule(config, featureFlag.Key, rolloutRules, index, user); - reasons += decisionResult.DecisionReasons; + // To skip rules + var skipToEveryoneElse = false; + + //Check forced decision first + var rule = rolloutRules[index]; + var decisionContext = new OptimizelyDecisionContext(featureFlag.Key, rule.Key); + var forcedDecisionResponse = user.FindValidatedForcedDecision(decisionContext, config); + + reasons += forcedDecisionResponse.DecisionReasons; + if (forcedDecisionResponse.ResultObject != null) + { + return Result.NewResult(new FeatureDecision(rule, forcedDecisionResponse.ResultObject, null), reasons); + } + + // Regular decision - if (decisionResult.ResultObject?.Variation?.Key != null) + // Get Bucketing ID from user attributes. + var bucketingIdResult = GetBucketingId(userId, attributes); + reasons += bucketingIdResult.DecisionReasons; + + var everyoneElse = index == rolloutRulesLength - 1; + + var loggingKey = everyoneElse ? "Everyone Else" : string.Format("{0}", index + 1); + + // Evaluate if user meets the audience condition of this rollout rule + var doesUserMeetAudienceConditionsResult = ExperimentUtils.DoesUserMeetAudienceConditions(config, rule, attributes, LOGGING_KEY_TYPE_RULE, rule.Key, Logger); + reasons += doesUserMeetAudienceConditionsResult.DecisionReasons; + if (doesUserMeetAudienceConditionsResult.ResultObject) + { + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" meets condition for targeting rule \"{loggingKey}\".")); + + var bucketedVariation = Bucketer.Bucket(config, rule, bucketingIdResult.ResultObject, userId); + reasons += bucketedVariation?.DecisionReasons; + + if (bucketedVariation?.ResultObject?.Key != null) + { + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is in the traffic group of targeting rule \"{loggingKey}\".")); + + return Result.NewResult(new FeatureDecision(rule, bucketedVariation.ResultObject, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons); + } + else if (!everyoneElse) + { + //skip this logging for everyoneElse rule since this has a message not for everyoneElse + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is not in the traffic group for targeting rule \"{loggingKey}\". Checking EveryoneElse rule now.")); + skipToEveryoneElse = true; + } + } + else { - return Result.NewResult(new FeatureDecision(rolloutRules[index], decisionResult.ResultObject.Variation, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons); + Logger.Log(LogLevel.DEBUG, reasons.AddInfo($"User \"{userId}\" does not meet the conditions for targeting rule \"{loggingKey}\".")); } // the last rule is special for "Everyone Else" - index = decisionResult.SkipToEveryoneElse ? (rolloutRulesLength - 1) : (index + 1); + index = skipToEveryoneElse ? (rolloutRulesLength - 1) : (index + 1); } + return Result.NullResult(reasons); } @@ -498,138 +546,41 @@ public virtual Result GetVariationForFeatureExperiment(FeatureF foreach (var experimentId in featureFlag.ExperimentIds) { var experiment = config.GetExperimentFromId(experimentId); + Variation decisionVariation = null; if (string.IsNullOrEmpty(experiment.Key)) continue; - - var variationResult = GetVariationFromExperiment(config, featureFlag, experiment, user, options); - reasons += variationResult.DecisionReasons; - - if (variationResult?.ResultObject?.Experiment != null && variationResult?.ResultObject?.Variation?.Id != null) + + var forcedDecisionResponse = user.FindValidatedForcedDecision( + new OptimizelyDecisionContext(featureFlag.Key, experiment?.Key), + config); + reasons += forcedDecisionResponse.DecisionReasons; + + if (forcedDecisionResponse?.ResultObject != null) { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); - return Result.NewResult(new FeatureDecision(experiment, variationResult.ResultObject.Variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), reasons); + decisionVariation = forcedDecisionResponse.ResultObject; + } + else + { + var decisionResponse = GetVariation(experiment, user, config, options); + + reasons += decisionResponse?.DecisionReasons; + decisionVariation = decisionResponse.ResultObject; } - } - - Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); - return Result.NullResult(reasons); - } - - - - private Result GetVariationFromExperiment(ProjectConfig config, FeatureFlag flag, Experiment experiment, OptimizelyUserContext user, OptimizelyDecideOption[] options) - { - var reasons = new DecisionReasons(); - if (flag.ExperimentIds.Any()) - { - foreach (var expId in flag.ExperimentIds) + if (!string.IsNullOrEmpty(decisionVariation?.Id)) { - config.ExperimentIdMap.TryGetValue(expId, out var exp); - - var decisionVariation = GetVariationFromExperimentRule(config, flag.Key, experiment, user, options); - reasons += decisionVariation?.DecisionReasons; - - var variation = decisionVariation?.ResultObject; - - if (variation != null) - { - var featureDecision = new FeatureDecision(exp, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); - return Result.NewResult(featureDecision, reasons); - } + var featureDecision = new FeatureDecision(experiment, decisionVariation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); + return Result.NewResult(featureDecision, reasons); } } + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); return Result.NullResult(reasons); } - private Result GetVariationFromDeliveryRule(ProjectConfig config, string key, List rules, int ruleIndex, OptimizelyUserContext user) - { - var reasons = new DecisionReasons(); - - bool skipToEveryoneElse = false; - - //Check forced decision first - var rule = rules[ruleIndex]; - var decisionContext = new OptimizelyDecisionContext(key, rule.Key); - var forcedDecisionResponse = user.FindValidatedForcedDecision(decisionContext); - - reasons += forcedDecisionResponse.DecisionReasons; - if (forcedDecisionResponse.ResultObject != null) - { - return Result.NewResult(new FeatureDecision(rule, forcedDecisionResponse.ResultObject, null), skipToEveryoneElse, reasons); - } - - // Regular decision - var userId = user.GetUserId(); - var attributes = user.GetAttributes(); - - // Get Bucketing ID from user attributes. - var bucketingIdResult = GetBucketingId(userId, attributes); - reasons += bucketingIdResult.DecisionReasons; - - var everyoneElse = ruleIndex == rules.Count - 1; - - var loggingKey = everyoneElse ? "Everyone Else" : ruleIndex + 1 + ""; - - Result bucketedVariation = null; - - // Evaluate if user meets the audience condition of this rollout rule - var doesUserMeetAudienceConditionsResult = ExperimentUtils.DoesUserMeetAudienceConditions(config, rule, attributes, LOGGING_KEY_TYPE_RULE, rule.Key, Logger); - reasons += doesUserMeetAudienceConditionsResult.DecisionReasons; - if (doesUserMeetAudienceConditionsResult.ResultObject) - { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" meets condition for targeting rule \"{loggingKey}\".")); - - bucketedVariation = Bucketer.Bucket(config, rule, bucketingIdResult.ResultObject, userId); - reasons += bucketedVariation?.DecisionReasons; - - if (bucketedVariation?.ResultObject?.Key != null) - { - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is in the traffic group of targeting rule \"{loggingKey}\".")); - } - else if (!everyoneElse) - { - //skip this loggng for everyoneElse rule since this has a message not for everyoneElse - Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is not in the traffic group for targeting rule \"{loggingKey}\". Checking EveryoneElse rule now.")); - skipToEveryoneElse = true; - } - } - else - { - Logger.Log(LogLevel.DEBUG, reasons.AddInfo($"User \"{userId}\" does not meet the conditions for targeting rule \"{loggingKey}\".")); - } - - return Result.NewResult(new FeatureDecision(rule, bucketedVariation?.ResultObject, null), skipToEveryoneElse, reasons); - } - private Result GetVariationFromExperimentRule(ProjectConfig config, string key, Experiment experiment, OptimizelyUserContext user, OptimizelyDecideOption[] options) - { - var reasons = new DecisionReasons(); - - var ruleKey = experiment != null ? experiment.Key : null; - - var decisionContext = new OptimizelyDecisionContext(key, ruleKey); - - var forcedDecisionResponse = user.FindValidatedForcedDecision(decisionContext); - - reasons += forcedDecisionResponse.DecisionReasons; - - var variation = forcedDecisionResponse?.ResultObject; - - if (variation != null) - { - return Result.NewResult(variation, reasons); - } - - var decisionResponse = GetVariation(experiment, user, config, user.GetAttributes(), options); - - reasons += decisionResponse?.DecisionReasons; - - return Result.NewResult(decisionResponse?.ResultObject, reasons); - } - /// /// Get the variation the user is bucketed into for the FeatureFlag /// @@ -681,7 +632,7 @@ public virtual Result GetVariationForFeature(FeatureFlag featur } Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); - return Result.NullResult(reasons); + return Result.NewResult(new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons); ; } /// diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index 5f28fa71..07045ae3 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -205,11 +205,10 @@ private Dictionary> _VariationIdMap private Dictionary> ExperimentFeatureMap = new Dictionary>(); /// - /// Associated array of flags to experiments + /// Associated dictionary of flags to variations key value. /// - - private Dictionary> _FlagVariationMap = new Dictionary>(); - public Dictionary> FlagVariationMap { get { return _FlagVariationMap; } } + private Dictionary> _FlagVariationMap = new Dictionary>(); + public Dictionary> FlagVariationMap { get { return _FlagVariationMap; } } //========================= Interfaces =========================== @@ -355,73 +354,36 @@ private void Initialize() } } - // Adding experiments in experiment-feature map. + var flagToVariationsMap = new Dictionary>(); + // Adding experiments in experiment-feature map and flag variation map to use. foreach (var feature in FeatureFlags) { + var variationKeyToVariationDict = new Dictionary(); foreach (var experimentId in feature.ExperimentIds ?? new List()) { + foreach (var variationDictKV in ExperimentIdMap[experimentId].VariationKeyToVariationMap) { + variationKeyToVariationDict[variationDictKV.Key] = variationDictKV.Value; + } + if (ExperimentFeatureMap.ContainsKey(experimentId)) + { ExperimentFeatureMap[experimentId].Add(feature.Id); + } else + { ExperimentFeatureMap[experimentId] = new List { feature.Id }; - } - } - _FlagVariationMap = GetFlagVariationMap(); - } - - /// - /// Get the Flag variation map to use - /// - /// A map of flag key to variations - private Dictionary> GetFlagVariationMap() - { - var variationsDict = new Dictionary>(); - - foreach (var flag in FeatureFlags) - { - var variationIdToVariationsDict = new Dictionary(); - - foreach (var rule in GetAllRulesForFlag(flag)) - { - foreach (var variation in rule.Variations) - { - if (!variationIdToVariationsDict.ContainsKey(variation.Id)) - { - variationIdToVariationsDict.Add(variation.Id, variation); - } } } - // Grab all the variations from the flag experiments and rollouts and add to flagVariationsMap - variationsDict[flag.Key] = variationIdToVariationsDict.Values.ToList(); - } - return variationsDict; - } - - /// - /// Retrieves all the rules for a given feature flag - /// - /// Feature flag to use - /// A list of experiments - private List GetAllRulesForFlag(FeatureFlag flag) - { - var rules = new List(); - - RolloutIdMap.TryGetValue(flag.RolloutId, out var rollout); - - foreach (var expId in flag.ExperimentIds) - { - if (ExperimentIdMap.TryGetValue(expId, out var rule)) - { - rules.Add(rule); + if (RolloutIdMap.TryGetValue(feature.RolloutId, out var rolloutRules)) { + var rolloutRulesVariations = rolloutRules.Experiments.SelectMany(ex => ex.Variations); + foreach (var rolloutRuleVariation in rolloutRulesVariations) { + variationKeyToVariationDict[rolloutRuleVariation.Key] = rolloutRuleVariation; + } } + + flagToVariationsMap[feature.Key] = variationKeyToVariationDict; } - - if (rollout != null) - { - rules.AddRange(rollout.Experiments); - } - - return rules; + _FlagVariationMap = flagToVariationsMap; } /// @@ -658,16 +620,12 @@ public FeatureFlag GetFeatureFlagFromKey(string featureKey) /// public Variation GetFlagVariationByKey(string flagKey, string variationKey) { - if (_FlagVariationMap.TryGetValue(flagKey, out var variations)) - { - foreach (var variation in variations) - { - if (variation.Key == variationKey) - { - return variation; - } - } + if (FlagVariationMap.TryGetValue(flagKey, out var variationsKeyMap)) { + + variationsKeyMap.TryGetValue(variationKey, out var variation); + return variation; } + return null; } diff --git a/OptimizelySDK/Entity/Result.cs b/OptimizelySDK/Entity/Result.cs index 04a30d6c..17a63133 100644 --- a/OptimizelySDK/Entity/Result.cs +++ b/OptimizelySDK/Entity/Result.cs @@ -21,12 +21,7 @@ public class Result { public T ResultObject; public DecisionReasons DecisionReasons; - public bool SkipToEveryoneElse; - public static Result NewResult(T resultObject, bool skipToEveryoneElse, DecisionReasons decisionReasons) - { - return new Result { DecisionReasons = decisionReasons, ResultObject = resultObject, SkipToEveryoneElse = skipToEveryoneElse }; - } public static Result NewResult(T resultObject, DecisionReasons decisionReasons) { return new Result { DecisionReasons = decisionReasons, ResultObject = resultObject }; diff --git a/OptimizelySDK/Event/Builder/EventBuilder.cs b/OptimizelySDK/Event/Builder/EventBuilder.cs index 613a9edc..ff224a31 100644 --- a/OptimizelySDK/Event/Builder/EventBuilder.cs +++ b/OptimizelySDK/Event/Builder/EventBuilder.cs @@ -132,7 +132,7 @@ private Dictionary GetImpressionParams(Experiment experiment, st new Dictionary { { Params.CAMPAIGN_ID, experiment?.LayerId }, - { Params.EXPERIMENT_ID, experiment?.Id}, + { Params.EXPERIMENT_ID, experiment?.Id ?? string.Empty }, { Params.VARIATION_ID, variationId } } }; diff --git a/OptimizelySDK/ForcedDecisionsStore.cs b/OptimizelySDK/ForcedDecisionsStore.cs new file mode 100644 index 00000000..da0fbac8 --- /dev/null +++ b/OptimizelySDK/ForcedDecisionsStore.cs @@ -0,0 +1,94 @@ +/* + * Copyright 2021, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; + +namespace OptimizelySDK +{ + /// + /// ForcedDecisionsStore defines helper methods that are used for optimizelyDecisionContext object manipulation. + /// + public class ForcedDecisionsStore + { + private Dictionary ForcedDecisionsMap { get; set; } + private static ForcedDecisionsStore NullForcedDecisionStore; + + /// + /// Instantiates a NULL object when ForcedDecisionStore first time is used. + /// + static ForcedDecisionsStore() + { + NullForcedDecisionStore = new ForcedDecisionsStore(); + } + public ForcedDecisionsStore() + { + ForcedDecisionsMap = new Dictionary(); + } + + /// + /// This method will return instance of ForcedDecisionStore that won't be accessible from outside. + /// Instead of copying everytime or putting NULL for every forced decision condition, this approach looks fine to me. + /// + /// + internal static ForcedDecisionsStore NullForcedDecision() + { + return NullForcedDecisionStore; + } + + public ForcedDecisionsStore(ForcedDecisionsStore forcedDecisionsStore) + { + ForcedDecisionsMap = new Dictionary(forcedDecisionsStore.ForcedDecisionsMap); + } + + public int Count + { + get + { + return ForcedDecisionsMap.Count; + } + } + public bool Remove(OptimizelyDecisionContext context) + { + return ForcedDecisionsMap.Remove(context.GetKey()); + } + + public void RemoveAll() + { + ForcedDecisionsMap.Clear(); + } + + public OptimizelyForcedDecision this[OptimizelyDecisionContext context] + { + get + { + if (context != null && context.FlagKey != null + && ForcedDecisionsMap.TryGetValue(context.GetKey(), out OptimizelyForcedDecision flagForcedDecision)) + { + return flagForcedDecision; + } + return null; + } + set + { + if (context != null && context.FlagKey != null) + { + ForcedDecisionsMap[context.GetKey()] = value; + } + } + + } + } +} diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 144fd082..a7265fa7 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -378,14 +378,16 @@ private Variation GetVariation(string experimentKey, string userId, ProjectConfi Experiment experiment = config.GetExperimentFromKey(experimentKey); if (experiment.Key == null) return null; - var variation = DecisionService.GetVariation(experiment, CreateUserContext(userId, userAttributes), config, userAttributes).ResultObject; + userAttributes = userAttributes ?? new UserAttributes(); + + var userContext = CreateUserContext(userId, userAttributes); + var variation = DecisionService.GetVariation(experiment, userContext, config)?.ResultObject; var decisionInfo = new Dictionary { { "experimentKey", experimentKey }, { "variationKey", variation?.Key }, }; - userAttributes = userAttributes ?? new UserAttributes(); var decisionNotificationType = config.IsFeatureExperiment(experiment.Id) ? DecisionNotificationTypes.FEATURE_TEST : DecisionNotificationTypes.AB_TEST; NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, decisionNotificationType, userId, userAttributes, decisionInfo); @@ -443,25 +445,6 @@ public Variation GetForcedVariation(string experimentKey, string userId) return DecisionService.GetForcedVariation(experimentKey, userId, config).ResultObject; } - public Variation GetFlagVariationByKey(string flagKey, string variationKey) - { - var flagVariationMap = ProjectConfigManager?.GetConfig().FlagVariationMap; - if (flagVariationMap.ContainsKey(flagKey) == true) - { - flagVariationMap.TryGetValue(flagKey, out var variations); - - foreach (var variation in variations) - { - if (variation.Key.Equals(variationKey)) - { - return variation; - } - } - } - - return null; - } - #region FeatureFlag APIs /// @@ -778,7 +761,7 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, FeatureDecision decision = null; var decisionContext = new OptimizelyDecisionContext(flag.Key); - var forcedDecisionVariation = user.FindValidatedForcedDecision(decisionContext); + var forcedDecisionVariation = user.FindValidatedForcedDecision(decisionContext, config); decisionReasons += forcedDecisionVariation.DecisionReasons; if (forcedDecisionVariation.ResultObject != null) diff --git a/OptimizelySDK/OptimizelyDecisionContext.cs b/OptimizelySDK/OptimizelyDecisionContext.cs index fafec889..d487483b 100644 --- a/OptimizelySDK/OptimizelyDecisionContext.cs +++ b/OptimizelySDK/OptimizelyDecisionContext.cs @@ -16,25 +16,33 @@ namespace OptimizelySDK { + /// + /// OptimizelyDecisionContext contains flag key and rule key to be used for setting + /// and getting forced decision. + /// public class OptimizelyDecisionContext { public const string OPTI_NULL_RULE_KEY = "$opt-null-rule-key"; public const string OPTI_KEY_DIVIDER = "-$opt$-"; + private string flagKey; private string ruleKey; private string decisionKey; - public OptimizelyDecisionContext(string flagKey, string ruleKey = null) + public string FlagKey { get { return flagKey; } } + public string RuleKey { get { return ruleKey; } } + + public OptimizelyDecisionContext(string flagKey, string ruleKey= null) { this.flagKey = flagKey; this.ruleKey = ruleKey; - this.decisionKey = string.Format("{0}{1}{2}", flagKey, OPTI_KEY_DIVIDER, ruleKey ?? OPTI_NULL_RULE_KEY); } - public string FlagKey { get { return flagKey; } } + public string GetKey() + { + return string.Format("{0}{1}{2}", FlagKey, OPTI_KEY_DIVIDER, RuleKey ?? OPTI_NULL_RULE_KEY); + } - public string RuleKey { get { return ruleKey; } } - public string DecisionKey { get { return decisionKey; } } } } diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index e151529a..c8791237 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -92,6 +92,7 @@ + diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 7ef49293..9b7f7a0e 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -41,29 +41,24 @@ public class OptimizelyUserContext // Optimizely object to be used. private Optimizely Optimizely; - private Dictionary ForcedDecisionsMap { get; set; } + private ForcedDecisionsStore ForcedDecisionsStore { get; set; } - public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger) + public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger) : + this(optimizely, userId, userAttributes, null, errorHandler, logger) { - ErrorHandler = errorHandler; - Logger = logger; - Optimizely = optimizely; - Attributes = userAttributes ?? new UserAttributes(); - ForcedDecisionsMap = new Dictionary(); - UserId = userId; } - public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, Dictionary forcedDecisions, IErrorHandler errorHandler, ILogger logger) + public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, ForcedDecisionsStore forcedDecisionsStore, IErrorHandler errorHandler, ILogger logger) { ErrorHandler = errorHandler; Logger = logger; Optimizely = optimizely; Attributes = userAttributes ?? new UserAttributes(); - ForcedDecisionsMap = forcedDecisions ?? new Dictionary(); + ForcedDecisionsStore = forcedDecisionsStore ?? new ForcedDecisionsStore(); UserId = userId; } - private OptimizelyUserContext Copy() => new OptimizelyUserContext(Optimizely, UserId, GetAttributes(), ForcedDecisionsMap, ErrorHandler, Logger); + private OptimizelyUserContext Copy() => new OptimizelyUserContext(Optimizely, UserId, GetAttributes(), GetForcedDecisionsStore(), ErrorHandler, Logger); /// /// Returns Optimizely instance associated with the UserContext. @@ -98,6 +93,27 @@ public UserAttributes GetAttributes() return copiedAttributes; } + /// + /// Returns copy of ForcedDecisionsStore associated with UserContext. + /// + /// copy of ForcedDecisionsStore. + public ForcedDecisionsStore GetForcedDecisionsStore() + { + ForcedDecisionsStore copiedForcedDecisionsStore = null; + lock (mutex) + { + if (ForcedDecisionsStore.Count == 0) + { + copiedForcedDecisionsStore = ForcedDecisionsStore.NullForcedDecision(); + } else + { + copiedForcedDecisionsStore = new ForcedDecisionsStore(ForcedDecisionsStore); + } + } + + return copiedForcedDecisionsStore; + } + /// /// Set an attribute for a given key. /// @@ -211,9 +227,8 @@ public virtual void TrackEvent(string eventName, /// /// Set a forced decision. /// - /// The flag key. - /// The rule key. - /// The variation key. + /// The context object containing flag and rule key. + /// OptimizelyForcedDecision object containing variation key. /// public bool SetForcedDecision(OptimizelyDecisionContext context, OptimizelyForcedDecision decision) { @@ -225,7 +240,7 @@ public bool SetForcedDecision(OptimizelyDecisionContext context, OptimizelyForce lock (mutex) { - ForcedDecisionsMap[context.DecisionKey] = decision; + ForcedDecisionsStore[context] = decision; } return true; @@ -234,8 +249,7 @@ public bool SetForcedDecision(OptimizelyDecisionContext context, OptimizelyForce /// /// Gets a forced variation /// - /// The flag key - /// The rule key + /// The context object containing flag and rule key. /// The variation key for a forced decision public OptimizelyForcedDecision GetForcedDecision(OptimizelyDecisionContext context) { @@ -251,7 +265,7 @@ public OptimizelyForcedDecision GetForcedDecision(OptimizelyDecisionContext cont return null; } - if (ForcedDecisionsMap.Count == 0) + if (ForcedDecisionsStore.Count == 0) { return null; } @@ -260,7 +274,7 @@ public OptimizelyForcedDecision GetForcedDecision(OptimizelyDecisionContext cont lock (mutex) { - ForcedDecisionsMap.TryGetValue(context.DecisionKey, out decision); + decision = ForcedDecisionsStore[context]; } return decision; } @@ -268,14 +282,13 @@ public OptimizelyForcedDecision GetForcedDecision(OptimizelyDecisionContext cont /// /// Removes a forced decision. /// - /// The flag key. - /// + /// The context object containing flag and rule key. /// Whether the item was removed. public bool RemoveForcedDecision(OptimizelyDecisionContext context) { if (context == null || context.FlagKey == null) { - Logger.Log(LogLevel.WARN, "flagKey cannot be null"); + Logger.Log(LogLevel.WARN, "FlagKey cannot be null"); return false; } @@ -284,8 +297,11 @@ public bool RemoveForcedDecision(OptimizelyDecisionContext context) Logger.Log(LogLevel.ERROR, DecisionMessage.SDK_NOT_READY); return false; } - - return ForcedDecisionsMap.Remove(context.DecisionKey); + + lock (mutex) + { + return ForcedDecisionsStore.Remove(context); + } } /// @@ -302,7 +318,7 @@ public bool RemoveAllForcedDecisions() lock (mutex) { - ForcedDecisionsMap.Clear(); + ForcedDecisionsStore.RemoveAll(); } return true; } @@ -310,19 +326,19 @@ public bool RemoveAllForcedDecisions() /// /// Finds a validated forced decision. /// - /// The flag key. - /// The rule key. + /// Object containing flag and rule key of which forced decision is set. + /// The Project config. /// A result with the variation - public Result FindValidatedForcedDecision(OptimizelyDecisionContext context) + public Result FindValidatedForcedDecision(OptimizelyDecisionContext context, ProjectConfig config) { DecisionReasons reasons = new DecisionReasons(); var forcedDecision = GetForcedDecision(context); - if (forcedDecision != null) + if (config != null && forcedDecision != null) { var loggingKey = context.RuleKey != null ? "flag (" + context.FlagKey + "), rule (" + context.RuleKey + ")" : "flag (" + context.FlagKey + ")"; var variationKey = forcedDecision.VariationKey; - var variation = Optimizely.GetFlagVariationByKey(context.FlagKey, variationKey); + var variation = config.GetFlagVariationByKey(context.FlagKey, variationKey); if (variation != null) { reasons.AddInfo("Decided by forced decision."); diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs index 675d1dd5..9a6f0701 100644 --- a/OptimizelySDK/ProjectConfig.cs +++ b/OptimizelySDK/ProjectConfig.cs @@ -119,9 +119,9 @@ public interface ProjectConfig Dictionary RolloutIdMap { get; } /// - /// Associative array of Flag to Variation in the datafile + /// Associative dictionary of Flag to Variation key and Variation in the datafile /// - Dictionary> FlagVariationMap { get; } + Dictionary> FlagVariationMap { get; } //========================= Datafile Entities ===========================