diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs index aeeeb96c..8aa94d0b 100644 --- a/OptimizelySDK.Tests/BucketerTest.cs +++ b/OptimizelySDK.Tests/BucketerTest.cs @@ -19,6 +19,7 @@ using OptimizelySDK.Config; using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -27,6 +28,7 @@ public class BucketerTest { private Mock LoggerMock; private ProjectConfig Config; + private IDecisionReasons DecisionReasons; private const string TestUserId = "testUserId"; public string TestBucketingIdControl { get; } = "testBucketingIdControl!"; // generates bucketing number 3741 public string TestBucketingIdVariation { get; } = "123456789'"; // generates bucketing number 4567 @@ -59,6 +61,7 @@ public override string ToString() public void Initialize() { LoggerMock = new Mock(); + DecisionReasons = DefaultDecisionReasons.NewInstance(); Config = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, new ErrorHandler.NoOpErrorHandler()); } @@ -95,18 +98,16 @@ public void TestBucketValidExperimentNotInGroup() { TestBucketer bucketer = new TestBucketer(LoggerMock.Object); bucketer.SetBucketValues(new[] { 3000, 7000, 9000 }); - // control Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in variation [control] of experiment [test_experiment].")); - // variation Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(4)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -114,23 +115,24 @@ public void TestBucketValidExperimentNotInGroup() // no variation Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(6)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in no variation.")); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } [Test] public void TestBucketValidExperimentInGroup() { TestBucketer bucketer = new TestBucketer(LoggerMock.Object); - + // group_experiment_1 (20% experiment) // variation 1 bucketer.SetBucketValues(new[] { 1000, 4000 }); Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -139,7 +141,7 @@ public void TestBucketValidExperimentInGroup() // variation 2 bucketer.SetBucketValues(new[] { 1500, 7000 }); Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); @@ -148,20 +150,21 @@ public void TestBucketValidExperimentInGroup() // User not in experiment bucketer.SetBucketValues(new[] { 5000, 7000 }); Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!].")); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015].")); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(10)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } [Test] public void TestBucketInvalidExperiment() { var bucketer = new Bucketer(LoggerMock.Object); - + Assert.AreEqual(new Variation { }, - bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId)); + bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DecisionReasons)); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Never); } @@ -176,7 +179,8 @@ public void TestBucketWithBucketingId() // make sure that the bucketing ID is used for the variation bucketing and not the user ID Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation)); + bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DecisionReasons)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } // Test for invalid experiment keys, null variation should be returned @@ -187,7 +191,8 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId() var expectedVariation = new Variation(); Assert.AreEqual(expectedVariation, - bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId)); + bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DecisionReasons)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID @@ -200,7 +205,8 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId() Assert.AreEqual(expectedGroupVariation, bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), - TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup)); + TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DecisionReasons)); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } // Make sure that user gets bucketed into the rollout rule. @@ -213,7 +219,8 @@ public void TestBucketRolloutRule() var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773"); Assert.True(TestData.CompareObjects(expectedVariation, - bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId))); + bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DecisionReasons))); + Assert.AreEqual(DecisionReasons.ToReport().Count, 0); } } -} \ No newline at end of file +} diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index 0936a857..6513e6e5 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -24,6 +24,7 @@ using OptimizelySDK.Bucketing; using OptimizelySDK.Utils; using OptimizelySDK.Config; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -84,7 +85,7 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once); // no attributes provided for a experiment that has an audience Assert.IsTrue(TestData.CompareObjects(actualVariation, expectedVariation)); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -111,7 +112,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -123,7 +124,7 @@ public void TestGetForcedVariationReturnsForcedVariation() LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".", WhitelistedUserId, WhitelistedVariation.Key)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -164,7 +165,7 @@ public void TestGetForcedVariationWithInvalidVariation() string.Format("Variation \"{0}\" is not in the datafile. Not activating user \"{1}\".", invalidVariationKey, userId)), Times.Once); - BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Test] @@ -216,7 +217,7 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, userProfileService, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile." , experiment.Key, UserProfileId)), Times.Once); @@ -241,7 +242,7 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig() DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig, DefaultDecisionReasons.NewInstance())); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user." , UserProfileId, storedVariationId, experiment.Id)), Times.Once); } @@ -265,7 +266,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); @@ -317,7 +318,7 @@ public void TestGetVariationSavesANewUserProfile() }); var mockBucketer = new Mock(LoggerMock.Object); - mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); + mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny())).Returns(variation); Dictionary userProfile = null; @@ -468,7 +469,7 @@ public void TestGetVariationWithBucketingId() public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() { var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature"); - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new OptimizelyDecideOption[]{}, DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); @@ -487,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() ExperimentIds = new List { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile.")); @@ -502,7 +503,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\".")); @@ -518,10 +519,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke var userAttributes = new UserAttributes(); DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), - "user1", ProjectConfig, userAttributes)).Returns(variation); + "user1", ProjectConfig, userAttributes, It.IsAny (), It.IsAny())).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision)); @@ -541,7 +542,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -553,11 +554,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() { var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny (), It.IsAny())). Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); @@ -582,7 +583,7 @@ public void TestGetVariationForFeatureRolloutWhenNoRuleInRollouts() var decisionService = new DecisionService(new Bucketer(new NoOpLogger()), new NoOpErrorHandler(), null, new NoOpLogger()); - var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig); + var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(variation); } @@ -600,9 +601,9 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() Variables = featureFlag.Variables }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns(null); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout.")); @@ -624,10 +625,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul }; BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())).Returns(variation); + It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -648,11 +649,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny())).Returns(null); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -668,10 +669,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsNull(actualDecision); } @@ -689,11 +690,11 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Provide null attributes so that user does not qualify for audience. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -723,7 +724,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( { { "device_type", "iPhone" }, { "location", "San Francisco" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'. var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -733,7 +734,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be '177771' because of audience 'Chrome users'. expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -741,7 +742,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( // Calling with no audience. mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny())).Returns(8000); - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned variation id should be of everyone else rule because of no audience. expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -752,7 +753,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }, ProjectConfig); + }, ProjectConfig, DefaultDecisionReasons.NewInstance()); // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule. Assert.Null(actualDecision); @@ -768,21 +769,21 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule() var variation = everyoneElseRule.Variations[0]; var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId)).Returns(variation); - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId)).Returns(null); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId, It.IsAny())).Returns(variation); + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId, It.IsAny())).Returns(null); var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Returned variation id should be of everyone else rule as it passes audience Id checking. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.True(TestData.CompareObjects(expectedDecision, actualDecision)); // Returned variation id should be null. - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.Null(actualDecision); // Returned variation id should be null as it fails audience Id checking. everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id }; - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance()); Assert.Null(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions for targeting rule \"1\"."), Times.Once); @@ -807,7 +808,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(expectedDecision); + It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny())).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -826,9 +827,9 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(null); + It.IsAny(), ProjectConfig, It.IsAny(), It.IsAny())).Returns(null); DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), - It.IsAny(), ProjectConfig)).Returns(expectedDecision); + It.IsAny(), ProjectConfig, It.IsAny())).Returns(expectedDecision); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); @@ -844,8 +845,8 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature"); var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns(null); var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -866,8 +867,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR { "browser_type", "chrome" } }; - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny(), It.IsAny())).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); // The user is bucketed into feature experiment's variation. Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -877,8 +878,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR var rolloutVariation = rolloutExperiment.Variations[0]; var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig); + BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny(), It.IsAny())).Returns(rolloutVariation); + var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance()); // The user is bucketed into feature rollout's variation. diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs index bf8f5f17..d5248e7c 100644 --- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs +++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs @@ -84,7 +84,7 @@ public void TestNewDecision() [Test] public void TestNewDecisionReasonWithIncludeReasons() { - var decisionReasons = DefaultDecisionReasons.NewInstance(new List() { OptimizelyDecideOption.INCLUDE_REASONS }); + var decisionReasons = DefaultDecisionReasons.NewInstance(new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS }); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 1c18ff46..c8a436a2 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -21,6 +21,7 @@ using OptimizelySDK.ErrorHandler; using OptimizelySDK.Event.Dispatcher; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; using System; namespace OptimizelySDK.Tests @@ -104,6 +105,38 @@ public void SetAttributeNoAttribute() Assert.AreEqual(newAttributes["k2"], true); } + [Test] + public void SetAttributeOverride() + { + var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + user.SetAttribute("k1", "v1"); + user.SetAttribute("house", "v2"); + + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], "v1"); + Assert.AreEqual(newAttributes["house"], "v2"); + } + + [Test] + public void SetAttributeNullValue() + { + var attributes = new UserAttributes() { { "k1", null } }; + OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + + var newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], null); + + user.SetAttribute("k1", true); + newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], true); + + user.SetAttribute("k1", null); + newAttributes = user.UserAttributes; + Assert.AreEqual(newAttributes["k1"], null); + } + [Test] public void SetAttributeToOverrideAttribute() { @@ -120,5 +153,63 @@ public void SetAttributeToOverrideAttribute() Assert.AreEqual(user.UserAttributes["k1"], true); } + #region decide + + [Test] + public void TestDecide() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + var decision = user.Decide(flagKey); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.AreEqual(decision.Reasons.Length, 0); + } + + [Test] + public void DecideInvalidFlagKey() + { + var flagKey = "invalid_feature"; + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisionExpected = OptimizelyDecision.NewErrorDecision( + flagKey, + user, + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey), + ErrorHandlerMock.Object, + LoggerMock.Object); + var decision = user.Decide(flagKey); + + Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected)); + } + + [Test] + public void DecideWhenConfigIsNull() + { + Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + + var flagKey = "multi_variate_feature"; + var decisionExpected = OptimizelyDecision.NewErrorDecision( + flagKey, + new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object), + DecisionMessage.SDK_NOT_READY, + ErrorHandlerMock.Object, + LoggerMock.Object); + var user = optimizely.CreateUserContext(UserID); + var decision = user.Decide(flagKey); + + Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected)); + } + #endregion } } diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs index b8944687..be62727a 100644 --- a/OptimizelySDK/Bucketing/Bucketer.cs +++ b/OptimizelySDK/Bucketing/Bucketer.cs @@ -15,6 +15,7 @@ */ using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; using System; using System.Collections.Generic; using System.Text; @@ -102,8 +103,9 @@ private string FindBucket(string bucketingId, string userId, string parentId, IE /// Experiment Experiment in which user is to be bucketed /// A customer-assigned value used to create the key for the murmur hash. /// User identifier + /// Decision log messages. /// Variation which will be shown to the user - public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId) + public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId, IDecisionReasons reasons) { string message; Variation variation; @@ -122,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str if (string.IsNullOrEmpty(userExperimentId)) { message = $"User [{userId}] is in no experiment."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } if (userExperimentId != experiment.Id) { message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return new Variation(); } message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); } // Bucket user if not in whitelist and in group (if any). string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation); if (string.IsNullOrEmpty(variationId)) { - Logger.Log(LogLevel.INFO, $"User [{userId}] is in no variation."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation.")); return new Variation(); } @@ -149,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str // success! variation = config.GetVariationFromId(experiment.Key, variationId); message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}]."; - Logger.Log(LogLevel.INFO, message); + Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return variation; } } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index 3959dbbd..8b6963b4 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -19,6 +19,7 @@ using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; using OptimizelySDK.Utils; namespace OptimizelySDK.Bucketing @@ -83,20 +84,43 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil /// The userId of the user. /// The user's attributes. This should be filtered to just attributes in the Datafile. /// The Variation the user is allocated into. - public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes) + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes) + { + return GetVariation(experiment, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); + } + + /// + /// 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. + /// The Variation the user is allocated into. + public virtual Variation GetVariation(Experiment experiment, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + OptimizelyDecideOption[] options, + IDecisionReasons reasons) { if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null; // check if a forced variation is set - var forcedVariation = GetForcedVariation(experiment.Key, userId, config); + var forcedVariation = GetForcedVariation(experiment.Key, userId, config, reasons); if (forcedVariation != null) return forcedVariation; - var variation = GetWhitelistedVariation(experiment, userId); + var variation = GetWhitelistedVariation(experiment, userId, reasons); if (variation != null) return variation; + // fetch the user profile map from the user profile service + var ignoreUPS = Array.Exists(options, option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); + UserProfile userProfile = null; - if (UserProfileService != null) + if (!ignoreUPS && UserProfileService != null) { try { @@ -104,21 +128,21 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap); - variation = GetStoredVariation(experiment, userProfile, config); + variation = GetStoredVariation(experiment, userProfile, config, reasons); if (variation != null) return variation; } else if (userProfileMap == null) { - Logger.Log(LogLevel.INFO, "We were unable to get a user profile map from the UserProfileService."); + Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService.")); } else { - Logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map.")); } } catch (Exception exception) { - Logger.Log(LogLevel.ERROR, exception.Message); + Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } @@ -126,16 +150,16 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj if (ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger)) { // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes); + string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); - variation = Bucketer.Bucket(config, experiment, bucketingId, userId); + variation = Bucketer.Bucket(config, experiment, bucketingId, userId, reasons); if (variation != null && variation.Key != null) { - if (UserProfileService != null) + if (UserProfileService != null && !ignoreUPS) { var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary()); - SaveVariation(experiment, variation, bucketerUserProfile); + SaveVariation(experiment, variation, bucketerUserProfile, reasons); } else @@ -144,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj return variation; } - Logger.Log(LogLevel.INFO, $"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); return null; } @@ -157,6 +181,18 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj /// Project Config /// Variation entity which the given user and experiment should be forced into. public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config) + { + return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Gets the forced variation for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// Project Config + /// Variation entity which the given user and experiment should be forced into. + public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config, IDecisionReasons reasons) { if (ForcedVariationMap.ContainsKey(userId) == false) { @@ -191,8 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project // this case is logged in getVariationFromKey if (string.IsNullOrEmpty(variationKey)) return null; - - Logger.Log(LogLevel.DEBUG, $@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"); + Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map")); Variation variation = config.GetVariationFromKey(experimentKey, variationKey); @@ -248,8 +283,6 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia Logger.Log(LogLevel.DEBUG, $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map."); return true; } - - /// /// Get the variation the user has been whitelisted into. /// @@ -258,6 +291,19 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia /// if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation. public Variation GetWhitelistedVariation(Experiment experiment, string userId) + { + return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get the variation the user has been whitelisted into. + /// + /// in which user is to be bucketed. + /// User Identifier + /// Decision log messages. + /// if the user is not whitelisted into any variation {@link Variation} + /// the user is bucketed into if the user has a specified whitelisted variation. + public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons) { //if a user has a forced variation mapping, return the respective variation Dictionary userIdToVariationKeyMap = experiment.UserIdToKeyVariations; @@ -271,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) : null; if (forcedVariation != null) - Logger.Log(LogLevel.INFO, $"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); else - Logger.Log(LogLevel.ERROR, $"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); return forcedVariation; } @@ -284,7 +330,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) /// which the user was bucketed /// User profile of the user /// The user was previously bucketed into. - public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config) + public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config, IDecisionReasons reasons) { // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation @@ -296,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (decision == null) { - Logger.Log(LogLevel.INFO, $"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); return null; } @@ -310,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi if (savedVariation == null) { - Logger.Log(LogLevel.INFO, $"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.")); return null; } - Logger.Log(LogLevel.INFO, $"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); return savedVariation; } catch (Exception) @@ -322,7 +368,6 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi return null; } } - /// /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. /// @@ -330,6 +375,17 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi /// The Variation to save. /// instance of the user information. public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile) + { + SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}. + /// + /// The experiment the user was buck + /// The Variation to save. + /// instance of the user information. + public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile, IDecisionReasons reasons) { //only save if the user has implemented a user profile service if (UserProfileService == null) @@ -359,7 +415,7 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); } } - + /// /// Try to bucket the user into a rollout rule. /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience. @@ -368,9 +424,14 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil /// The feature flag the user wants to access. /// User Identifier /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// Decision log messages. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) + public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, + string userId, + UserAttributes filteredAttributes, + ProjectConfig config, + IDecisionReasons reasons) { if (featureFlag == null) { @@ -380,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(featureFlag.RolloutId)) { - Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in a rollout."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return null; } @@ -388,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature if (string.IsNullOrEmpty(rollout.Id)) { - Logger.Log(LogLevel.ERROR, $"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""); + Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return null; } @@ -400,16 +461,16 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature var rolloutRulesLength = rollout.Experiments.Count; // Get Bucketing ID from user attributes. - string bucketingId = GetBucketingId(userId, filteredAttributes); + string bucketingId = GetBucketingId(userId, filteredAttributes, reasons); // For all rules before the everyone else rule for (int i = 0; i < rolloutRulesLength - 1; i++) { string loggingKey = (i + 1).ToString(); var rolloutRule = rollout.Experiments[i]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, reasons, Logger)) { - variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId, reasons); if (variation == null || string.IsNullOrEmpty(variation.Id)) break; @@ -424,9 +485,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature // Get the last rule which is everyone else rule. var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1]; - if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger)) + if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", reasons, Logger)) { - variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId, reasons); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\"."); @@ -450,7 +511,12 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature /// The user's attributes. This should be filtered to just attributes in the Datafile. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// Otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) + public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, + string userId, + UserAttributes filteredAttributes, + ProjectConfig config, + OptimizelyDecideOption[] options, + IDecisionReasons reasons) { if (featureFlag == null) { @@ -460,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0) { - Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in any experiments."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); return null; } @@ -471,16 +537,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat if (string.IsNullOrEmpty(experiment.Key)) continue; - var variation = GetVariation(experiment, userId, config, filteredAttributes); + var variation = GetVariation(experiment, userId, config, filteredAttributes, options, reasons); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); } } - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".")); return null; } @@ -493,23 +559,44 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed. public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) + { + return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance()); + } + + /// + /// Get the variation the user is bucketed into for the FeatureFlag + /// + /// The feature flag the user wants to access. + /// User Identifier + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// The user's attributes. This should be filtered to just attributes in the Datafile. + /// An array of decision options. + /// Decision log messages. + /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is + /// successfully bucketed. + public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, + string userId, + ProjectConfig config, + UserAttributes filteredAttributes, + OptimizelyDecideOption[] options, + IDecisionReasons reasons) { // Check if the feature flag has an experiment and the user is bucketed into that experiment. - var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config); + var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config, options, reasons); if (decision != null) return decision; // Check if the feature flag has rollout and the the user is bucketed into one of its rules. - decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config); + decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config, reasons); if (decision != null) { - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return decision; } - Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."); + Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); } @@ -519,7 +606,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, s /// User Identifier /// The user's attributes. /// Bucketing Id if it is a string type in attributes, user Id otherwise. - private string GetBucketingId(string userId, UserAttributes filteredAttributes) + private string GetBucketingId(string userId, UserAttributes filteredAttributes, IDecisionReasons reasons) { string bucketingId = userId; @@ -533,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes) } else { - Logger.Log(LogLevel.WARN, "BucketingID attribute is not a string. Defaulted to userId"); + Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 59c37bb5..e36a6ec4 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -30,6 +30,7 @@ using OptimizelySDK.OptlyConfig; using System.Net; using OptimizelySDK.OptimizelyDecisions; +using System.Linq; namespace OptimizelySDK { @@ -438,7 +439,7 @@ public Variation GetForcedVariation(string experimentKey, string userId) return DecisionService.GetForcedVariation(experimentKey, userId, config); } -#region FeatureFlag APIs + #region FeatureFlag APIs /// /// Determine whether a feature is enabled. @@ -452,7 +453,8 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri { var config = ProjectConfigManager?.GetConfig(); - if (config == null) { + if (config == null) + { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IsFeatureEnabled'."); @@ -711,6 +713,140 @@ public OptimizelyUserContext CreateUserContext(string userId, return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger); } + /// + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + ///
    + ///
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. + ///
+ ///
+ /// A flag key for which a decision will be made. + /// A list of options for decision-making. + /// A decision result. + internal OptimizelyDecision Decide(OptimizelyUserContext user, + string key, + OptimizelyDecideOption[] options) + { + + var config = ProjectConfigManager?.GetConfig(); + if (config == null) + { + return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); + } + var userId = user.UserId; + + var flag = config.GetFeatureFlagFromKey(key); + if (flag.Key == null) + { + return OptimizelyDecision.NewErrorDecision(key, + user, + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), + ErrorHandler, Logger); + } + + var userAttributes = user.UserAttributes; + var decisionEventDispatched = false; + var allOptions = GetAllOptions(options); + var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions); + + var flagDecision = DecisionService.GetVariationForFeature( + flag, + userId, + config, + userAttributes, + allOptions, + decisionReasons); + + var featureEnabled = false; + + var variation = flagDecision.Variation; + + if (variation != null) + { + featureEnabled = variation.FeatureEnabled.GetValueOrDefault(); + } + + if (featureEnabled) + { + Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); + } + else + { + Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); + } + var variableMap = new Dictionary(); + if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) + { + + foreach (var featureVariable in flag?.Variables) + { + string variableValue = featureVariable.DefaultValue; + if (featureEnabled) + { + var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id); + if (featureVariableUsageInstance != null) + { + variableValue = featureVariableUsageInstance.Value; + } + } + + var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type); + + if (typeCastedValue is OptimizelyJSON) + typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); + + variableMap.Add(featureVariable.Key, typeCastedValue); + } + } + + var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger); + + var decisionSource = flagDecision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; + if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) + { + SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource, featureEnabled); + decisionEventDispatched = true; + } + var reasonsToReport = decisionReasons.ToReport(); + var variationKey = flagDecision.Variation?.Key; + + // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + var ruleKey = flagDecision.Experiment?.Key; + + var decisionInfo = new Dictionary + { + { "flagKey", key }, + { "enabled", featureEnabled }, + { "variables", variableMap }, + { "variationKey", variationKey }, + { "ruleKey", ruleKey }, + { "reasons", decisionReasons }, + { "decisionEventDispatched", decisionEventDispatched } + }; + + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FLAG, userId, + userAttributes ?? new UserAttributes(), decisionInfo); + + return new OptimizelyDecision( + variationKey, + featureEnabled, + optimizelyJSON, + ruleKey, + key, + user, + reasonsToReport.ToArray()); + } + + private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) + { + OptimizelyDecideOption[] copiedOptions = new OptimizelyDecideOption[DefaultDecideOptions.Length]; + Array.Copy(DefaultDecideOptions, copiedOptions, DefaultDecideOptions.Length); + if (options != null) + { + copiedOptions.Concat(options); + } + return copiedOptions; + } + /// /// Sends impression event. /// diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs index 2bce30c2..519fe73f 100644 --- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs +++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using System; using System.Collections.Generic; namespace OptimizelySDK.OptimizelyDecisions @@ -23,9 +24,9 @@ public class DefaultDecisionReasons : IDecisionReasons protected List Errors = new List(); private List Infos = new List(); - public static IDecisionReasons NewInstance(List options) + public static IDecisionReasons NewInstance(OptimizelyDecideOption[] options) { - if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) + if (options != null && Array.Exists(options, option => option == OptimizelyDecideOption.INCLUDE_REASONS)) { return new DefaultDecisionReasons(); } diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index ba53f747..88d9c5ee 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -77,7 +77,7 @@ public void SetAttribute(string key, object value) /// A decision result. public OptimizelyDecision Decide(string key) { - return Decide(key, new List()); + return Decide(key, new OptimizelyDecideOption[] { }); } /// @@ -90,9 +90,9 @@ public OptimizelyDecision Decide(string key) /// A list of options for decision-making. /// A decision result. public OptimizelyDecision Decide(string key, - List options) + OptimizelyDecideOption[] options) { - throw new NotImplementedException(); + return Optimizely.Decide(this, key, options); } /// @@ -111,7 +111,7 @@ public Dictionary DecideForKeys(List keys) /// A dictionary of all decision results, mapped by flag keys. public Dictionary DecideAll() { - return DecideAll(new List()); + return DecideAll(new OptimizelyDecideOption[] { }); } @@ -120,7 +120,7 @@ public Dictionary DecideAll() /// /// A list of options for decision-making. /// All decision results mapped by flag keys. - public Dictionary DecideAll(List options) + public Dictionary DecideAll(OptimizelyDecideOption[] options) { throw new NotImplementedException(); } diff --git a/OptimizelySDK/Utils/DecisionInfoTypes.cs b/OptimizelySDK/Utils/DecisionInfoTypes.cs index 188522e8..10cac1f7 100644 --- a/OptimizelySDK/Utils/DecisionInfoTypes.cs +++ b/OptimizelySDK/Utils/DecisionInfoTypes.cs @@ -20,6 +20,7 @@ public static class DecisionNotificationTypes { public const string AB_TEST = "ab-test"; public const string FEATURE = "feature"; + public const string FLAG = "flag"; public const string FEATURE_TEST = "feature-test"; public const string FEATURE_VARIABLE = "feature-variable"; public const string ALL_FEATURE_VARIABLE = "all-feature-variables"; diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs index 470b014c..b6023480 100644 --- a/OptimizelySDK/Utils/ExperimentUtils.cs +++ b/OptimizelySDK/Utils/ExperimentUtils.cs @@ -17,6 +17,7 @@ using OptimizelySDK.AudienceConditions; using OptimizelySDK.Entity; using OptimizelySDK.Logger; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Utils { @@ -35,6 +36,24 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger) return true; } + /// + /// Check if the user meets audience conditions to be in experiment or not + /// + /// ProjectConfig Configuration for the project + /// Experiment Entity representing the experiment + /// Attributes of the user. Defaults to empty attributes array if not provided + /// It can be either experiment or rule. + /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. + /// true if the user meets audience conditions to be in experiment, false otherwise. + public static bool DoesUserMeetAudienceConditions(ProjectConfig config, + Experiment experiment, + UserAttributes userAttributes, + string loggingKeyType, + string loggingKey, + ILogger logger) + { + return DoesUserMeetAudienceConditions(config, experiment, userAttributes, loggingKeyType, loggingKey, DefaultDecisionReasons.NewInstance(), logger); + } /// /// Check if the user meets audience conditions to be in experiment or not @@ -44,12 +63,14 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger) /// Attributes of the user. Defaults to empty attributes array if not provided /// It can be either experiment or rule. /// In case loggingKeyType is experiment it will be experiment key or else it will be rule number. + /// Decision log messages. /// true if the user meets audience conditions to be in experiment, false otherwise. public static bool DoesUserMeetAudienceConditions(ProjectConfig config, Experiment experiment, UserAttributes userAttributes, string loggingKeyType, string loggingKey, + IDecisionReasons reasons, ILogger logger) { if (userAttributes == null) @@ -73,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config, var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault(); var resultText = result.ToString().ToUpper(); - logger.Log(LogLevel.INFO, $@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"); + logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); return result; } }