示例#1
0
        /// <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));
        }
示例#2
0
        /// <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));;
        }
示例#3
0
        /// <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));
        }
示例#4
0
        /// <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));
        }
        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);
        }
        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.");
        }
        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);
        }
示例#8
0
        /// <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 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));
        }
示例#10
0
        /// <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));
        }
示例#11
0
        /// <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));
            }
        }
示例#12
0
        /// <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>
        /// 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));
        }
示例#14
0
 public static Result <T> NewResult(T resultObject, DecisionReasons decisionReasons)
 {
     return(new Result <T> {
         DecisionReasons = decisionReasons, ResultObject = resultObject
     });
 }
示例#15
0
        /// <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));
        }
示例#16
0
 public void Initialize()
 {
     LoggerMock         = new Mock <ILogger>();
     DecisionReasonsObj = new DecisionReasons();
     Config             = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, new ErrorHandler.NoOpErrorHandler());
 }
示例#17
0
 public static void AreEqual(DecisionReasons expected, DecisionReasons actual)
 {
     AreEquivalent(expected.ToReport(), actual.ToReport());
 }
示例#18
0
        /// <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));
        }
示例#19
0
        public Result <T> SetReasons(DecisionReasons decisionReasons)
        {
            DecisionReasons = decisionReasons;

            return(this);
        }
示例#20
0
 public static Result <T> NullResult(DecisionReasons decisionReasons)
 {
     return(NewResult(default(T), decisionReasons));
 }
示例#21
0
        /// <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));
        }