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 ===========================