Example #1
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));;
        }
Example #2
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));
        }
        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);
        }
        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);
        }
Example #7
0
        /// <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));
        }
Example #8
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));
        }
Example #9
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));
        }
Example #10
0
 public static void AreEqual(OptimizelyUserContext expected, OptimizelyUserContext actual)
 {
     Assert.AreEqual(expected.GetUserId(), actual.GetUserId());
     AreEquivalent(expected.GetAttributes(), actual.GetAttributes());
 }