/// <summary> /// Get the variation the user has been whitelisted into. /// </summary> /// <param name = "experiment" >in which user is to be bucketed.</param> /// <param name = "userId" > User Identifier</param> /// <param name = "reasons" > Decision log messages.</param> /// <returns>if the user is not whitelisted into any variation {@link Variation} /// the user is bucketed into if the user has a specified whitelisted variation.</returns> public Result <Variation> GetWhitelistedVariation(Experiment experiment, string userId) { var reasons = new DecisionReasons(); //if a user has a forced variation mapping, return the respective variation Dictionary <string, string> userIdToVariationKeyMap = experiment.UserIdToKeyVariations; if (!userIdToVariationKeyMap.ContainsKey(userId)) { return(Result <Variation> .NullResult(reasons)); } string forcedVariationKey = userIdToVariationKeyMap[userId]; Variation forcedVariation = experiment.VariationKeyToVariationMap.ContainsKey(forcedVariationKey) ? experiment.VariationKeyToVariationMap[forcedVariationKey] : null; if (forcedVariation != null) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".")); } else { Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".")); } return(Result <Variation> .NewResult(forcedVariation, 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));; }
/// <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> /// Determine variation the user should be put in. /// </summary> /// <param name="config">ProjectConfig Configuration for the project</param> /// <param name="experiment">Experiment Experiment in which user is to be bucketed</param> /// <param name="bucketingId">A customer-assigned value used to create the key for the murmur hash.</param> /// <param name="userId">User identifier</param> /// <returns>Variation which will be shown to the user</returns> public virtual Result <Variation> Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId) { string message; Variation variation; var reasons = new DecisionReasons(); if (string.IsNullOrEmpty(experiment.Key)) { return(Result <Variation> .NewResult(new Variation(), reasons)); } // Determine if experiment is in a mutually exclusive group. if (experiment.IsInMutexGroup) { Group group = config.GetGroup(experiment.GroupId); if (string.IsNullOrEmpty(group.Id)) { return(Result <Variation> .NewResult(new Variation(), reasons)); } string userExperimentId = FindBucket(bucketingId, userId, group.Id, group.TrafficAllocation); if (string.IsNullOrEmpty(userExperimentId)) { message = $"User [{userId}] is in no experiment."; Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return(Result <Variation> .NewResult(new Variation(), reasons)); } if (userExperimentId != experiment.Id) { message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return(Result <Variation> .NewResult(new Variation(), reasons)); } message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}]."; 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, reasons.AddInfo($"User [{userId}] is in no variation.")); return(Result <Variation> .NewResult(new Variation(), reasons)); } // success! variation = config.GetVariationFromId(experiment.Key, variationId); message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}]."; Logger.Log(LogLevel.INFO, reasons.AddInfo(message)); return(Result <Variation> .NewResult(variation, reasons)); }
/// <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, string userId, UserAttributes filteredAttributes, ProjectConfig config, OptimizelyDecideOption[] options) { var reasons = new DecisionReasons(); 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); if (string.IsNullOrEmpty(experiment.Key)) { continue; } var variationResult = GetVariation(experiment, userId, config, filteredAttributes, options); reasons += variationResult.DecisionReasons; if (!string.IsNullOrEmpty(variationResult.ResultObject?.Id)) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".")); return(Result <FeatureDecision> .NewResult(new FeatureDecision(experiment, variationResult.ResultObject, FeatureDecision.DECISION_SOURCE_FEATURE_TEST), 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> .NewResult(null, reasons)); }
public void TestNewDecisionReasonWithoutIncludeReasons() { var decisionReasons = new DecisionReasons(); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\"."); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key")); Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type."); decisionReasons.AddInfo("Some info message."); Assert.AreEqual(decisionReasons.ToReport().Count, 2); }
/// <summary> /// Get the { @link Variation } that has been stored for the user in the { @link UserProfileService } implementation. /// </summary> /// <param name = "experiment" > which the user was bucketed</param> /// <param name = "userProfile" > User profile of the user</param> /// <returns>The user was previously bucketed into.</returns> public Result <Variation> GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config) { // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation string experimentId = experiment.Id; string experimentKey = experiment.Key; var reasons = new DecisionReasons(); Decision decision = userProfile.ExperimentBucketMap.ContainsKey(experimentId) ? userProfile.ExperimentBucketMap[experimentId] : null; if (decision == null) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.")); return(Result <Variation> .NullResult(reasons)); } try { string variationId = decision.VariationId; Variation savedVariation = config.ExperimentIdMap[experimentId].VariationIdToVariationMap.ContainsKey(variationId) ? config.ExperimentIdMap[experimentId].VariationIdToVariationMap[variationId] : null; if (savedVariation == null) { 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(Result <Variation> .NullResult(reasons)); } Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.")); return(Result <Variation> .NewResult(savedVariation, reasons)); } catch (Exception) { return(Result <Variation> .NullResult(reasons)); } }
public void TestNewDecisionReasonWithIncludeReasons() { var decisionReasons = new DecisionReasons(); var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS }; decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key")); Assert.AreEqual(decisionReasons.ToReport(decideOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS))[0], "No flag was found for key \"invalid_key\"."); decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key")); Assert.AreEqual(decisionReasons.ToReport(decideOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS))[1], "Variable value for key \"invalid_key\" is invalid or wrong type."); decisionReasons.AddInfo("Some info message."); Assert.AreEqual(decisionReasons.ToReport(decideOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS))[2], "Some info message."); }
/// <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> /// <returns>A result with the variation</returns> public Result <Variation> FindValidatedForcedDecision(OptimizelyDecisionContext context, ProjectConfig config) { DecisionReasons reasons = new DecisionReasons(); var forcedDecision = 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)); }
public void TestFindValidatedForcedDecisionReturnsCorrectDecisionWithNullVariation() { var decisionReasons = new DecisionReasons(); decisionReasons.AddInfo("{0}", "Invalid variation is mapped to flag: flagKey and rule: rule forced decision map."); var expectedResult = Result <Variation> .NullResult(decisionReasons); var user = Optimizely.CreateUserContext(UserID); var context = new OptimizelyDecisionContext("flagKey", "ruleKey"); var result = user.FindValidatedForcedDecision(context, null); Assertions.AreEqual(expectedResult, result); }
/// <summary> /// Gets the forced variation for the given user and experiment. /// </summary> /// <param name="experimentKey">The experiment key</param> /// <param name="userId">The user ID</param> /// <param name="config">Project Config</param> /// <returns>Variation entity which the given user and experiment should be forced into.</returns> public Result <Variation> GetForcedVariation(string experimentKey, string userId, ProjectConfig config) { var reasons = new DecisionReasons(); if (ForcedVariationMap.ContainsKey(userId) == false) { Logger.Log(LogLevel.DEBUG, $@"User ""{userId}"" is not in the forced variation map."); return(Result <Variation> .NullResult(reasons)); } Dictionary <string, string> experimentToVariationMap = ForcedVariationMap[userId]; string experimentId = config.GetExperimentFromKey(experimentKey).Id; // this case is logged in getExperimentFromKey if (string.IsNullOrEmpty(experimentId)) { return(Result <Variation> .NullResult(reasons)); } if (experimentToVariationMap.ContainsKey(experimentId) == false) { Logger.Log(LogLevel.DEBUG, $@"No experiment ""{experimentKey}"" mapped to user ""{userId}"" in the forced variation map."); return(Result <Variation> .NullResult(reasons)); } string variationId = experimentToVariationMap[experimentId]; if (string.IsNullOrEmpty(variationId)) { Logger.Log(LogLevel.DEBUG, $@"No variation mapped to experiment ""{experimentKey}"" in the forced variation map."); return(Result <Variation> .NullResult(reasons)); } string variationKey = config.GetVariationFromId(experimentKey, variationId).Key; // this case is logged in getVariationFromKey if (string.IsNullOrEmpty(variationKey)) { return(Result <Variation> .NullResult(reasons)); } 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); return(Result <Variation> .NewResult(variation, reasons)); }
/// <summary> /// Get Bucketing ID from user attributes. /// </summary> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes.</param> /// <returns>Bucketing Id if it is a string type in attributes, user Id otherwise.</returns> private Result <string> GetBucketingId(string userId, UserAttributes filteredAttributes) { var reasons = new DecisionReasons(); string bucketingId = userId; // If the bucketing ID key is defined in attributes, then use that in place of the userID for the murmur hash key if (filteredAttributes != null && filteredAttributes.ContainsKey(ControlAttributes.BUCKETING_ID_ATTRIBUTE)) { if (filteredAttributes[ControlAttributes.BUCKETING_ID_ATTRIBUTE] is string) { bucketingId = (string)filteredAttributes[ControlAttributes.BUCKETING_ID_ATTRIBUTE]; Logger.Log(LogLevel.DEBUG, $"BucketingId is valid: \"{bucketingId}\""); } else { Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId")); } } return(Result <string> .NewResult(bucketingId, reasons)); }
/// <summary> /// Check if the user meets audience conditions to be in experiment or not /// </summary> /// <param name="config">ProjectConfig Configuration for the project</param> /// <param name="experiment">Experiment Entity representing the experiment</param> /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param> /// <param name="loggingKeyType">It can be either experiment or rule.</param> /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param> /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns> public static Result <bool> DoesUserMeetAudienceConditions(ProjectConfig config, Experiment experiment, UserAttributes userAttributes, string loggingKeyType, string loggingKey, ILogger logger) { var reasons = new DecisionReasons(); if (userAttributes == null) { userAttributes = new UserAttributes(); } ICondition expConditions = null; if (experiment.AudienceConditionsList != null) { expConditions = experiment.AudienceConditionsList; logger.Log(LogLevel.DEBUG, $@"Evaluating audiences for {loggingKeyType} ""{loggingKey}"": {experiment.AudienceConditionsString}."); } else { expConditions = experiment.AudienceIdsList; logger.Log(LogLevel.DEBUG, $@"Evaluating audiences for {loggingKeyType} ""{loggingKey}"": {experiment.AudienceIdsString}."); } // If there are no audiences, return true because that means ALL users are included in the experiment. if (expConditions == null) { return(Result <bool> .NewResult(true, reasons)); } var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault(); var resultText = result.ToString().ToUpper(); logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}")); return(Result <bool> .NewResult(result, reasons)); }
/// <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)); }
/// <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, string userId, UserAttributes filteredAttributes, 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)); } Result <Variation> variationResult = null; var rolloutRulesLength = rollout.Experiments.Count; // Get Bucketing ID from user attributes. var bucketingIdResult = GetBucketingId(userId, filteredAttributes); reasons += bucketingIdResult.DecisionReasons; // 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]; var userMeetConditionsResult = ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger); reasons += userMeetConditionsResult.DecisionReasons; if (userMeetConditionsResult.ResultObject) { variationResult = Bucketer.Bucket(config, rolloutRule, bucketingIdResult.ResultObject, userId); reasons += variationResult?.DecisionReasons; if (string.IsNullOrEmpty(variationResult.ResultObject?.Id)) { break; } return(Result <FeatureDecision> .NewResult(new FeatureDecision(rolloutRule, variationResult.ResultObject, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons)); } else { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" does not meet the conditions for targeting rule \"{loggingKey}\"."); } } // Get the last rule which is everyone else rule. var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1]; var userMeetConditionsResultEveryoneElse = ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger); reasons += userMeetConditionsResultEveryoneElse.DecisionReasons; if (userMeetConditionsResultEveryoneElse.ResultObject) { variationResult = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingIdResult.ResultObject, userId); reasons += variationResult?.DecisionReasons; if (!string.IsNullOrEmpty(variationResult?.ResultObject?.Id)) { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\"."); return(Result <FeatureDecision> .NewResult(new FeatureDecision(everyoneElseRolloutRule, variationResult.ResultObject, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons)); } } else { var audience = config.GetAudience(everyoneElseRolloutRule.AudienceIds[0]); Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" does not meet the conditions to be in rollout rule for audience \"{audience.Name}\"."); } return(Result <FeatureDecision> .NewResult(null, reasons)); }