/// <summary> /// Get the variation if the user is bucketed for one of the experiments on this feature flag. /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>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</returns> public virtual Result <FeatureDecision> GetVariationForFeatureExperiment(FeatureFlag featureFlag, OptimizelyUserContext user, UserAttributes filteredAttributes, ProjectConfig config, OptimizelyDecideOption[] options) { var reasons = new DecisionReasons(); var userId = user.GetUserId(); if (featureFlag == null) { Logger.Log(LogLevel.ERROR, "Invalid feature flag provided."); return(Result <FeatureDecision> .NullResult(reasons)); } if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments.")); return(Result <FeatureDecision> .NullResult(reasons)); } foreach (var experimentId in featureFlag.ExperimentIds) { var experiment = config.GetExperimentFromId(experimentId); Variation decisionVariation = null; if (string.IsNullOrEmpty(experiment.Key)) { continue; } var forcedDecisionResponse = user.FindValidatedForcedDecision( new OptimizelyDecisionContext(featureFlag.Key, experiment?.Key), config); reasons += forcedDecisionResponse.DecisionReasons; if (forcedDecisionResponse?.ResultObject != null) { decisionVariation = forcedDecisionResponse.ResultObject; } else { var decisionResponse = GetVariation(experiment, user, config, options); reasons += decisionResponse?.DecisionReasons; decisionVariation = decisionResponse.ResultObject; } if (!string.IsNullOrEmpty(decisionVariation?.Id)) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); var featureDecision = new FeatureDecision(experiment, decisionVariation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); return(Result <FeatureDecision> .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 <FeatureDecision> .NullResult(reasons)); }
/// <summary> /// Get the variation the user is bucketed into for the FeatureFlag /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <param name = "options" >An array of decision options.</param> /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed.</returns> public virtual Result <FeatureDecision> GetVariationForFeature(FeatureFlag featureFlag, OptimizelyUserContext user, ProjectConfig config, UserAttributes filteredAttributes, OptimizelyDecideOption[] options) { var reasons = new DecisionReasons(); var userId = user.GetUserId(); // Check if the feature flag has an experiment and the user is bucketed into that experiment. var decisionResult = GetVariationForFeatureExperiment(featureFlag, user, filteredAttributes, config, options); reasons += decisionResult.DecisionReasons; if (decisionResult.ResultObject != null) { return(Result <FeatureDecision> .NewResult(decisionResult.ResultObject, reasons)); } // Check if the feature flag has rollout and the the user is bucketed into one of its rules. decisionResult = GetVariationForFeatureRollout(featureFlag, user, config); reasons += decisionResult.DecisionReasons; if (decisionResult.ResultObject != null) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return(Result <FeatureDecision> .NewResult(decisionResult.ResultObject, reasons)); } Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); return(Result <FeatureDecision> .NewResult(new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons));; }
public void OptimizelyUserContextNoAttributes() { OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); Assert.True(user.GetAttributes().Count == 0); }
public void OptimizelyUserContextWithAttributes() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); Assert.AreEqual(user.GetAttributes(), attributes); }
public void SetAttributeToOverrideAttribute() { OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); user.SetAttribute("k1", "v1"); Assert.AreEqual(user.GetAttributes()["k1"], "v1"); user.SetAttribute("k1", true); Assert.AreEqual(user.GetAttributes()["k1"], true); }
/// <summary> /// Static function to return OptimizelyDecision /// when there are errors for example like OptimizelyConfig is not valid, etc. /// OptimizelyDecision will have null variation key, false enabled, empty variables, null rule key /// and error reason array /// </summary> public static OptimizelyDecision NewErrorDecision(string key, OptimizelyUserContext optimizelyUserContext, string error, IErrorHandler errorHandler, ILogger logger) { return(new OptimizelyDecision( null, false, new OptimizelyJSON(new Dictionary <string, object>(), errorHandler, logger), null, key, optimizelyUserContext, new string[] { error })); }
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.GetAttributes(); Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["house"], "v2"); }
public OptimizelyDecision(string variationKey, bool enabled, OptimizelyJSON variables, string ruleKey, string flagKey, OptimizelyUserContext userContext, string[] reasons) { VariationKey = variationKey; Enabled = enabled; Variables = variables; RuleKey = ruleKey; FlagKey = flagKey; UserContext = userContext; Reasons = reasons; }
public void SetAttributeNullValue() { var attributes = new UserAttributes() { { "k1", null } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); var newAttributes = user.GetAttributes(); Assert.AreEqual(newAttributes["k1"], null); user.SetAttribute("k1", true); newAttributes = user.GetAttributes(); Assert.AreEqual(newAttributes["k1"], true); user.SetAttribute("k1", null); newAttributes = user.GetAttributes(); Assert.AreEqual(newAttributes["k1"], null); }
public void SetAttribute() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); user.SetAttribute("k1", "v1"); user.SetAttribute("k2", true); user.SetAttribute("k3", 100); user.SetAttribute("k4", 3.5); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); var newAttributes = user.GetAttributes(); Assert.AreEqual(newAttributes["house"], "GRYFFINDOR"); Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["k2"], true); Assert.AreEqual(newAttributes["k3"], 100); Assert.AreEqual(newAttributes["k4"], 3.5); }
/// <summary> /// Get a Variation of an Experiment for a user to be allocated into. /// </summary> /// <param name = "experiment" > The Experiment the user will be bucketed into.</param> /// <param name = "user" > Optimizely user context. /// <param name = "config" > Project config.</param> /// <returns>The Variation the user is allocated into.</returns> public virtual Result <Variation> GetVariation(Experiment experiment, OptimizelyUserContext user, ProjectConfig config) { return(GetVariation(experiment, user, config, new OptimizelyDecideOption[] { })); }
/// <summary> /// Finds a validated forced decision. /// </summary> /// <param name="context">Object containing flag and rule key of which forced decision is set.</param> /// <param name="config">The Project config.</param> /// <param name="user">Optimizely user context.</param> /// <returns>A result with the variation</returns> public Result <Variation> ValidatedForcedDecision(OptimizelyDecisionContext context, ProjectConfig config, OptimizelyUserContext user) { DecisionReasons reasons = new DecisionReasons(); var userId = user.GetUserId(); var forcedDecision = user.GetForcedDecision(context); 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 = config.GetFlagVariationByKey(context.FlagKey, variationKey); if (variation != null) { reasons.AddInfo("Decided by forced decision."); reasons.AddInfo("Variation ({0}) is mapped to {1} and user ({2}) in the forced decision map.", variationKey, loggingKey, userId); return(Result <Variation> .NewResult(variation, reasons)); } else { reasons.AddInfo("Invalid variation is mapped to {0} and user ({1}) in the forced decision map.", loggingKey, userId); } } return(Result <Variation> .NullResult(reasons)); }
/// <summary> /// Get the variation the user is bucketed into for the FeatureFlag /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed.</returns> public virtual Result <FeatureDecision> GetVariationForFeature(FeatureFlag featureFlag, OptimizelyUserContext user, ProjectConfig config) { return(GetVariationForFeature(featureFlag, user, config, user.GetAttributes(), new OptimizelyDecideOption[] { })); }
/// <summary> /// 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. /// Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation. /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <param name = "reasons" >Decision log messages.</param> /// <returns>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</returns> public virtual Result <FeatureDecision> GetVariationForFeatureRollout(FeatureFlag featureFlag, OptimizelyUserContext user, ProjectConfig config) { var reasons = new DecisionReasons(); if (featureFlag == null) { Logger.Log(LogLevel.ERROR, "Invalid feature flag provided."); return(Result <FeatureDecision> .NullResult(reasons)); } if (string.IsNullOrEmpty(featureFlag.RolloutId)) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return(Result <FeatureDecision> .NullResult(reasons)); } Rollout rollout = config.GetRolloutFromId(featureFlag.RolloutId); if (string.IsNullOrEmpty(rollout.Id)) { Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return(Result <FeatureDecision> .NullResult(reasons)); } if (rollout.Experiments == null || rollout.Experiments.Count == 0) { return(Result <FeatureDecision> .NullResult(reasons)); } var rolloutRulesLength = rollout.Experiments.Count; var rolloutRules = rollout.Experiments; var userId = user.GetUserId(); var attributes = user.GetAttributes(); var index = 0; while (index < rolloutRulesLength) { // To skip rules var skipToEveryoneElse = false; //Check forced decision first var rule = rolloutRules[index]; var decisionContext = new OptimizelyDecisionContext(featureFlag.Key, rule.Key); var forcedDecisionResponse = ValidatedForcedDecision(decisionContext, config, user); reasons += forcedDecisionResponse.DecisionReasons; if (forcedDecisionResponse.ResultObject != null) { return(Result <FeatureDecision> .NewResult(new FeatureDecision(rule, forcedDecisionResponse.ResultObject, null), reasons)); } // Regular decision // 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 <FeatureDecision> .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 { 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 = skipToEveryoneElse ? (rolloutRulesLength - 1) : (index + 1); } return(Result <FeatureDecision> .NullResult(reasons)); }
/// <summary> /// Get a Variation of an Experiment for a user to be allocated into. /// </summary> /// <param name = "experiment" > The Experiment the user will be bucketed into.</param> /// <param name = "user" > optimizely user context. /// <param name = "config" > Project Config.</param> /// <param name = "options" >An array of decision options.</param> /// <returns>The Variation the user is allocated into.</returns> public virtual Result <Variation> GetVariation(Experiment experiment, OptimizelyUserContext user, ProjectConfig config, OptimizelyDecideOption[] options) { var reasons = new DecisionReasons(); var userId = user.GetUserId(); if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) { return(Result <Variation> .NullResult(reasons)); } // check if a forced variation is set var decisionVariationResult = GetForcedVariation(experiment.Key, userId, config); reasons += decisionVariationResult.DecisionReasons; var variation = decisionVariationResult.ResultObject; if (variation == null) { decisionVariationResult = GetWhitelistedVariation(experiment, user.GetUserId()); reasons += decisionVariationResult.DecisionReasons; variation = decisionVariationResult.ResultObject; } if (variation != null) { decisionVariationResult.SetReasons(reasons); return(decisionVariationResult); } // 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 (!ignoreUPS && UserProfileService != null) { try { Dictionary <string, object> userProfileMap = UserProfileService.Lookup(user.GetUserId()); if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap); decisionVariationResult = GetStoredVariation(experiment, userProfile, config); reasons += decisionVariationResult.DecisionReasons; if (decisionVariationResult.ResultObject != null) { return(decisionVariationResult.SetReasons(reasons)); } } else if (userProfileMap == null) { Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService.")); } else { Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map.")); } } catch (Exception exception) { Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); 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) { // Get Bucketing ID from user attributes. var bucketingIdResult = GetBucketingId(userId, filteredAttributes); reasons += bucketingIdResult.DecisionReasons; decisionVariationResult = Bucketer.Bucket(config, experiment, bucketingIdResult.ResultObject, userId); reasons += decisionVariationResult.DecisionReasons; if (decisionVariationResult.ResultObject?.Key != null) { if (UserProfileService != null && !ignoreUPS) { var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary <string, Decision>()); SaveVariation(experiment, decisionVariationResult.ResultObject, bucketerUserProfile); } else { Logger.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."); } } return(decisionVariationResult.SetReasons(reasons)); } Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{user.GetUserId()}\" does not meet conditions to be in experiment \"{experiment.Key}\".")); return(Result <Variation> .NullResult(reasons)); }
public static void AreEqual(OptimizelyUserContext expected, OptimizelyUserContext actual) { Assert.AreEqual(expected.GetUserId(), actual.GetUserId()); AreEquivalent(expected.GetAttributes(), actual.GetAttributes()); }