diff --git a/src/Optimizely/Config/DatafileProjectConfig.php b/src/Optimizely/Config/DatafileProjectConfig.php index 9663a924..9283f79c 100644 --- a/src/Optimizely/Config/DatafileProjectConfig.php +++ b/src/Optimizely/Config/DatafileProjectConfig.php @@ -245,6 +245,13 @@ class DatafileProjectConfig implements ProjectConfigInterface */ private $_sendFlagDecisions; + /** + * Map indicating variations of flag decisions + * + * @return map + */ + private $_flagVariationsMap; + /** * DatafileProjectConfig constructor to load and set project configuration data. * @@ -376,7 +383,30 @@ public function __construct($datafile, $logger, $errorHandler) } } } + $this->_flagVariationsMap = array(); + foreach ($this->_featureFlags as $flag) { + $flagVariations = array(); + $flagRules = $this->getAllRulesForFlag($flag); + + foreach ($flagRules as $rule) { + $filtered_variations = []; + foreach (array_values($rule->getVariations()) as $variation) { + $exist = false; + foreach ($flagVariations as $flagVariation) { + if ($flagVariation->getId() == $variation->getId()) { + $exist = true; + break; + } + } + if (!$exist) { + array_push($filtered_variations, $variation); + } + } + $flagVariations = array_merge($flagVariations, $filtered_variations); + } + $this->_flagVariationsMap[$flag->getKey()] = $flagVariations; + } // Add variations for rollout experiments to variationIdMap and variationKeyMap $this->_variationIdMap = $this->_variationIdMap + $rolloutVariationIdMap; $this->_variationKeyMap = $this->_variationKeyMap + $rolloutVariationKeyMap; @@ -404,6 +434,18 @@ public function __construct($datafile, $logger, $errorHandler) } } + private function getAllRulesForFlag(FeatureFlag $flag) + { + $rules = array(); + foreach ($flag->getExperimentIds() as $experimentId) { + array_push($rules, $this->_experimentIdMap[$experimentId]); + } + if ($this->_rolloutIdMap && key_exists($flag->getRolloutId(), $this->_rolloutIdMap)) { + $rollout = $this->_rolloutIdMap[$flag->getRolloutId()]; + $rules = array_merge($rules, $rollout->getExperiments()); + } + return $rules; + } /** * Create ProjectConfig based on datafile string. * @@ -614,6 +656,26 @@ public function getExperimentFromId($experimentId) return new Experiment(); } + /** + * Gets the variation associated with experiment or rollout in instance of given feature flag key + * + * @param string Feature flag key + * @param string variation key + * + * @return Variation / null + */ + public function getFlagVariationByKey($flagKey, $variationKey) + { + if (array_key_exists($flagKey, $this->_flagVariationsMap)) { + foreach ($this->_flagVariationsMap[$flagKey] as $variation) { + if ($variation->getKey() == $variationKey) { + return $variation; + } + } + } + return null; + } + /** * @param String $featureKey Key of the feature flag * @@ -868,6 +930,16 @@ public function isFeatureExperiment($experimentId) return array_key_exists($experimentId, $this->_experimentFeatureMap); } + /** + * Returns map array of Flag key as key and Variations as value + * + * @return array + */ + public function getFlagVariationsMap() + { + return $this->_flagVariationsMap; + } + /** * Returns if flag decisions should be sent to server or not * diff --git a/src/Optimizely/Config/ProjectConfigInterface.php b/src/Optimizely/Config/ProjectConfigInterface.php index 887e706c..bbd02e94 100644 --- a/src/Optimizely/Config/ProjectConfigInterface.php +++ b/src/Optimizely/Config/ProjectConfigInterface.php @@ -199,7 +199,23 @@ public function getVariationFromKeyByExperimentId($experimentId, $variationKey); * @return FeatureVariable / null */ public function getFeatureVariableFromKey($featureFlagKey, $variableKey); - + + /** + * Gets the variation associated with experiment or rollout in instance of given feature flag key + * + * @param string Feature flag key + * @param string variation key + * + * @return Variation / null + */ + public function getFlagVariationByKey($flagKey, $variationKey); + + /** + * Returns map array of Flag key as key and Variations as value + * + * @return array + */ + public function getFlagVariationsMap(); /** * Determines if given experiment is a feature test. * diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 9ab91c0f..c8c8a3e0 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -19,6 +19,7 @@ use Exception; use Monolog\Logger; use Optimizely\Bucketer; +use Optimizely\OptimizelyDecisionContext; use Optimizely\Config\ProjectConfigInterface; use Optimizely\Decide\OptimizelyDecideOption; use Optimizely\Entity\Experiment; @@ -26,8 +27,10 @@ use Optimizely\Entity\Rollout; use Optimizely\Entity\Variation; use Optimizely\Enums\ControlAttributes; +use Optimizely\ForcedDecision; use Optimizely\Logger\LoggerInterface; use Optimizely\Optimizely; +use Optimizely\OptimizelyUserContext; use Optimizely\UserProfile\Decision; use Optimizely\UserProfile\UserProfileServiceInterface; use Optimizely\UserProfile\UserProfile; @@ -118,16 +121,17 @@ protected function getBucketingId($userId, $userAttributes) * Determine which variation to show the user. * * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. - * @param $experiment Experiment Experiment to get the variation for. - * @param $userId string User identifier. - * @param $attributes array Attributes of the user. - * @param $decideOptions array Options to customize evaluation. + * @param $experiment Experiment Experiment to get the variation for. + * @param $user OptimizelyUserContext User identifier. + * @param $decideOptions array Options to customize evaluation. * * @return [ Variation, array ] Variation which the user is bucketed into and array of log messages representing decision making. */ - public function getVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId, $attributes = null, $decideOptions = []) + public function getVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, OptimizelyUserContext $user, $decideOptions = []) { $decideReasons = []; + $userId = $user->getUserId(); + $attributes = $user->getAttributes(); list($bucketingId, $reasons) = $this->getBucketingId($userId, $attributes); $decideReasons = array_merge($decideReasons, $reasons); @@ -212,14 +216,13 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ * Get the variation the user is bucketed into for the given FeatureFlag * * @param ProjectConfigInterface $projectConfig ProjectConfigInterface instance. - * @param FeatureFlag $featureFlag The feature flag the user wants to access - * @param string $userId user ID - * @param array $userAttributes user attributes - * @param array $decideOptions Options to customize evaluation. + * @param FeatureFlag $featureFlag The feature flag the user wants to access + * @param OptimizelyUserContext $user Optimizely User context containing user id and attribute + * @param array $decideOptions Options to customize evaluation. * * @return FeatureDecision representing decision. */ - public function getVariationForFeature(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = []) + public function getVariationForFeature(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, OptimizelyUserContext $user, $decideOptions = []) { $decideReasons = []; @@ -228,7 +231,7 @@ public function getVariationForFeature(ProjectConfigInterface $projectConfig, Fe //2. Attempt to bucket user into rollout using the feature flag. // Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments - $decision = $this->getVariationForFeatureExperiment($projectConfig, $featureFlag, $userId, $userAttributes, $decideOptions); + $decision = $this->getVariationForFeatureExperiment($projectConfig, $featureFlag, $user, $decideOptions); if ($decision->getVariation()) { return $decision; } @@ -236,9 +239,9 @@ public function getVariationForFeature(ProjectConfigInterface $projectConfig, Fe $decideReasons = array_merge($decideReasons, $decision->getReasons()); // Check if the feature flag has rollout and the user is bucketed into one of it's rules - $decision = $this->getVariationForFeatureRollout($projectConfig, $featureFlag, $userId, $userAttributes); + $decision = $this->getVariationForFeatureRollout($projectConfig, $featureFlag, $user); $decideReasons = array_merge($decideReasons, $decision->getReasons()); - + $userId = $user->getUserId(); if ($decision->getVariation()) { $message = "User '{$userId}' is bucketed into rollout for feature flag '{$featureFlag->getKey()}'."; $this->_logger->log( @@ -266,13 +269,12 @@ public function getVariationForFeature(ProjectConfigInterface $projectConfig, Fe * * @param ProjectConfigInterface $projectConfig ProjectConfigInterface instance. * @param FeatureFlag $featureFlag The feature flag the user wants to access - * @param string $userId user id - * @param array $userAttributes user userAttributes + * @param OptimizelyUserContext $user Optimizely User context containing user id and attribute * @param array $decideOptions Options to customize evaluation. * * @return FeatureDecision representing decision. */ - public function getVariationForFeatureExperiment(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = []) + public function getVariationForFeatureExperiment(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, OptimizelyUserContext $user, $decideOptions = []) { $decideReasons = []; $featureFlagKey = $featureFlag->getKey(); @@ -288,7 +290,7 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project $decideReasons[] = $message; return new FeatureDecision(null, null, null, $decideReasons); } - + $userId = $user->getUserId(); // Evaluate each experiment ID and return the first bucketed experiment variation foreach ($experimentIds as $experiment_id) { $experiment = $projectConfig->getExperimentFromId($experiment_id); @@ -297,7 +299,7 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project continue; } - list($variation, $reasons) = $this->getVariation($projectConfig, $experiment, $userId, $userAttributes, $decideOptions); + list($variation, $reasons) = $this->getVariationFromExperimentRule($projectConfig, $featureFlagKey, $experiment, $user, $decideOptions); $decideReasons = array_merge($decideReasons, $reasons); if ($variation && $variation->getKey()) { $message = "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$featureFlagKey}'."; @@ -328,14 +330,13 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project * * @param ProjectConfigInterface $projectConfig ProjectConfigInterface instance. * @param FeatureFlag $featureFlag The feature flag the user wants to access - * @param string $userId user id - * @param array $userAttributes user userAttributes + * @param OptimizelyUserContext $user Optimizely User context containing user id and attribute + * @param array $decideOptions Options to customize evaluation. * @return FeatureDecision representing decision. */ - public function getVariationForFeatureRollout(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes) + public function getVariationForFeatureRollout(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, OptimizelyUserContext $user, $decideOptions = []) { $decideReasons = []; - list($bucketing_id, $reasons) = $this->getBucketingId($userId, $userAttributes); $featureFlagKey = $featureFlag->getKey(); $rollout_id = $featureFlag->getRolloutId(); if (empty($rollout_id)) { @@ -357,58 +358,108 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon if (sizeof($rolloutRules) == 0) { return new FeatureDecision(null, null, null, $decideReasons); } + $index = 0; + while ($index < sizeof($rolloutRules)) { + list($decisionResponses, $skipToEveryoneElse) = $this->getVariationFromDeliveryRule($projectConfig, $featureFlagKey, $rolloutRules, $index, $user, $decideOptions); + $decideReasons = array_merge($decideReasons, $decisionResponses->getReasons()); + $variation = $decisionResponses->getVariation(); + if ($variation) { + return new FeatureDecision($rolloutRules[$index], $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT, $decideReasons); + } + // the last rule is special for "Everyone Else" + $index = $skipToEveryoneElse ? (sizeof($rolloutRules) - 1) : ($index + 1); + } + return new FeatureDecision(null, null, null, $decideReasons); + } - // Evaluate all rollout rules except for last one - for ($i = 0; $i < sizeof($rolloutRules) - 1; $i++) { - $rolloutRule = $rolloutRules[$i]; + private function getVariationFromExperimentRule(ProjectConfigInterface $projectConfig, $flagKey, Experiment $rule, OptimizelyUserContext $user, $decideOptions = []) + { + $decideReasons = []; + // check forced-decision first + $context = new OptimizelyDecisionContext($flagKey, $rule->getKey()); + list($decisionResponse, $reasons) = $user->findValidatedForcedDecision($context); + $decideReasons = array_merge($decideReasons, $reasons); + if ($decisionResponse) { + return [$decisionResponse, $decideReasons]; + } - // Evaluate if user meets the audience condition of this rollout rule - list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', $i + 1); - $decideReasons = array_merge($decideReasons, $reasons); - if (!$evalResult) { - $message = sprintf("User '%s' does not meet conditions for targeting rule %s.", $userId, $i+1); - $this->_logger->log( - Logger::DEBUG, - $message - ); - $decideReasons[] = $message; - // Evaluate this user for the next rule - continue; - } + // regular decision + list($variation, $reasons) = $this->getVariation($projectConfig, $rule, $user, $decideOptions); + $decideReasons = array_merge($decideReasons, $reasons); - // Evaluate if user satisfies the traffic allocation for this rollout rule - list($variation, $reasons) = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId); - $decideReasons = array_merge($decideReasons, $reasons); - if ($variation && $variation->getKey()) { - return new FeatureDecision($rolloutRule, $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT, $decideReasons); - } - break; + return [$variation, $decideReasons]; + } + + /** + * Gets the forced variation key for the given user and experiment. + * + * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. + * @param $flagKey string Key of feature flag. + * @param $rules array Array of delivery rules. + * @param $ruleIndex integer Index of delivery rule of which validation of forced decision is needed. + * @param $user OptimizelyUserContext Optimizely User context containing user id and attribute + * @param $options array Options to customize evaluation. + * + * @return [ FeatureDecision, Boolean ] The variation which the given user and experiment should be forced into and + * skipToEveryone boolean to decision making. + */ + public function getVariationFromDeliveryRule(ProjectConfigInterface $projectConfig, $flagKey, array $rules, $ruleIndex, OptimizelyUserContext $user, array $options = []) + { + $decideReasons = []; + $skipToEveryoneElse = false; + // check forced-decision first + $rule = $rules[$ruleIndex]; + $context = new OptimizelyDecisionContext($flagKey, $rule->getKey()); + list($forcedDecisionResponse, $reasons) = $user->findValidatedForcedDecision($context); + + $decideReasons = array_merge($decideReasons, $reasons); + if ($forcedDecisionResponse) { + return [new FeatureDecision($rule, $forcedDecisionResponse, null, $decideReasons), $skipToEveryoneElse]; } - // Evaluate Everyone Else Rule / Last Rule now - $rolloutRule = $rolloutRules[sizeof($rolloutRules) - 1]; - // Evaluate if user meets the audience condition of Everyone Else Rule / Last Rule now - list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', 'Everyone Else'); + // regular decision + $userId = $user->getUserId(); + $attributes = $user->getAttributes(); + list($bucketingId, $reasons) = $this->getBucketingId($userId, $attributes); $decideReasons = array_merge($decideReasons, $reasons); - if (!$evalResult) { - $message = sprintf("User '%s' does not meet conditions for targeting rule 'Everyone Else'.", $userId); + + $everyoneElse = $ruleIndex == sizeof($rules) - 1; + $loggingKey = $everyoneElse ? "Everyone Else" : $ruleIndex + 1; + $bucketedVariation = null; + + // Evaluate if user meets the audience condition of this rollout rule + list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($projectConfig, $rule, $attributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', $loggingKey); + $decideReasons = array_merge($decideReasons, $reasons); + if ($evalResult) { + $message = sprintf('User "%s" meets condition for targeting rule "%s".', $userId, $loggingKey); $this->_logger->log( - Logger::DEBUG, + Logger::INFO, $message ); $decideReasons[] = $message; - return new FeatureDecision(null, null, null, $decideReasons); + list($bucketedVariation, $reasons) = $this->_bucketer->bucket($projectConfig, $rule, $bucketingId, $userId); + $decideReasons = array_merge($decideReasons, $reasons); + if ($bucketedVariation) { + $message = sprintf('User "%s" is in the traffic group of targeting rule "%s".', $userId, $loggingKey); + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; + } elseif (!$everyoneElse) { + // skip this logging for EveryoneElse since this has a message not for EveryoneElse + $message = sprintf('User "%s" is not in the traffic group for targeting rule "%s". Checking Everyone Else rule now.', $userId, $loggingKey); + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; + // skip the rest of rollout rules to the everyone-else rule if audience matches but not bucketed. + $skipToEveryoneElse = true; + } + } else { + $message = sprintf('User "%s" does not meet conditions for targeting rule "%s".', $userId, $loggingKey); + $this->_logger->log(Logger::DEBUG, $message); + $decideReasons[] = $message; } - list($variation, $reasons) = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId); - $decideReasons = array_merge($decideReasons, $reasons); - if ($variation && $variation->getKey()) { - return new FeatureDecision($rolloutRule, $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT); - } - return new FeatureDecision(null, null, null, $decideReasons); + return [new FeatureDecision($rule, $bucketedVariation, null, $decideReasons), $skipToEveryoneElse]; } - /** * Gets the forced variation key for the given user and experiment. * diff --git a/src/Optimizely/Event/Builder/EventBuilder.php b/src/Optimizely/Event/Builder/EventBuilder.php index 8f2144cb..744d2cf1 100644 --- a/src/Optimizely/Event/Builder/EventBuilder.php +++ b/src/Optimizely/Event/Builder/EventBuilder.php @@ -27,6 +27,7 @@ use Optimizely\Utils\EventTagUtils; use Optimizely\Utils\GeneratorUtils; use Optimizely\Utils\Validator; +use phpDocumentor\Reflection\Types\This; class EventBuilder { @@ -139,19 +140,36 @@ private function getCommonParams($config, $userId, $attributes) * Helper function to get parameters specific to impression event. * * @param $experiment Experiment Experiment being activated. - * @param $variationId String ID representing the variation for the user. + * @param $variation Variation representing the variation for the user is allocated. + * @param $flagKey string feature flag key. + * @param $ruleKey string feature or rollout experiment key. + * @param $ruleType string feature or rollout experiment source type. + * @param $enabled Boolean feature enabled. * * @return array Hash representing parameters particular to impression event. */ private function getImpressionParams(Experiment $experiment, $variation, $flagKey, $ruleKey, $ruleType, $enabled) { - $variationKey = $variation->getKey() ? $variation->getKey() : ''; + $variationKey = ''; + $variationId = null; + if ($variation) { + $variationKey = $variation->getKey() ? $variation->getKey() : ''; + $variationId = $variation->getId(); + } + $experimentID = ''; + $campaignID = ''; + if ($experiment) { + if ($experiment->getId()) { + $experimentID = $experiment->getId(); + $campaignID = $experiment->getLayerId(); + } + } $impressionParams = [ DECISIONS => [ [ - CAMPAIGN_ID => $experiment->getLayerId(), - EXPERIMENT_ID => $experiment->getId(), - VARIATION_ID => $variation->getId(), + CAMPAIGN_ID => $campaignID, + EXPERIMENT_ID => $experimentID, + VARIATION_ID => $variationId, METADATA => [ FLAG_KEY => $flagKey, RULE_KEY => $ruleKey, @@ -232,9 +250,14 @@ private function getConversionParams($eventEntity, $eventTags) public function createImpressionEvent($config, $experimentId, $variationKey, $flagKey, $ruleKey, $ruleType, $enabled, $userId, $attributes) { $eventParams = $this->getCommonParams($config, $userId, $attributes); - $experiment = $config->getExperimentFromId($experimentId); - $variation = $config->getVariationFromKeyByExperimentId($experimentId, $variationKey); + + if (empty($experimentId)) { + $variation = $config->getFlagVariationByKey($flagKey, $variationKey); + } else { + $variation = $config->getVariationFromKeyByExperimentId($experimentId, $variationKey); + } + $impressionParams = $this->getImpressionParams($experiment, $variation, $flagKey, $ruleKey, $ruleType, $enabled); $eventParams[VISITORS][0][SNAPSHOTS][] = $impressionParams; diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 5dcacbe6..179360de 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -28,6 +28,7 @@ use Optimizely\Decide\OptimizelyDecisionMessage; use Optimizely\DecisionService\DecisionService; use Optimizely\DecisionService\FeatureDecision; +use Optimizely\OptimizelyDecisionContext; use Optimizely\Entity\Experiment; use Optimizely\Entity\FeatureVariable; use Optimizely\Enums\DecisionNotificationTypes; @@ -201,11 +202,15 @@ private function validateUserInputs($attributes, $eventTags = null) } /** + * @param DatafileProjectConfig DatafileProjectConfig instance * @param string Experiment ID * @param string Variation key + * @param string Flag key + * @param string Rule key + * @param string Rule type + * @param boolean Feature enabled * @param string User ID * @param array Associative array of user attributes - * @param DatafileProjectConfig DatafileProjectConfig instance */ protected function sendImpressionEvent($config, $experimentId, $variationKey, $flagKey, $ruleKey, $ruleType, $enabled, $userId, $attributes) { @@ -347,22 +352,32 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp $decisionEventDispatched = false; // get decision - $decision = $this->_decisionService->getVariationForFeature( - $config, - $featureFlag, - $userId, - $userAttributes, - $decideOptions - ); - - $decideReasons = $decision->getReasons(); + $decision = null; + // check forced-decisions first + $context = new OptimizelyDecisionContext($flagKey, $ruleKey); + list($forcedDecisionResponse, $reasons) = $userContext->findValidatedForcedDecision($context); + if ($forcedDecisionResponse) { + $decision = new FeatureDecision(null, $forcedDecisionResponse, FeatureDecision::DECISION_SOURCE_FEATURE_TEST, $decideReasons); + } else { + // regular decision + $decision = $this->_decisionService->getVariationForFeature( + $config, + $featureFlag, + $userContext, + $decideOptions + ); + } + $decideReasons = array_merge($decideReasons, $reasons); + $decideReasons = array_merge($decideReasons, $decision->getReasons()); $variation = $decision->getVariation(); if ($variation) { $variationKey = $variation->getKey(); $featureEnabled = $variation->getFeatureEnabled(); - $ruleKey = $decision->getExperiment()->getKey(); - $experimentId = $decision->getExperiment()->getId(); + if ($decision->getExperiment()) { + $ruleKey = $decision->getExperiment()->getKey(); + $experimentId = $decision->getExperiment()->getId(); + } } else { $variationKey = null; $ruleKey = null; @@ -687,7 +702,8 @@ public function getVariation($experimentKey, $userId, $attributes = null) return null; } - list($variation, $reasons) = $this->_decisionService->getVariation($config, $experiment, $userId, $attributes); + $userContext = $this->createUserContext($userId, $attributes ? $attributes : []); + list($variation, $reasons) = $this->_decisionService->getVariation($config, $experiment, $userContext); $variationKey = ($variation === null) ? null : $variation->getKey(); if ($config->isFeatureExperiment($experiment->getId())) { @@ -815,7 +831,8 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) } $featureEnabled = false; - $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes); + $userContext = $this->createUserContext($userId, $attributes?: []); + $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userContext); $variation = $decision->getVariation(); if ($config->getSendFlagDecisions() && ($decision->getSource() == FeatureDecision::DECISION_SOURCE_ROLLOUT || !$variation)) { @@ -948,8 +965,8 @@ public function getFeatureVariableValueForType( // Error logged in DatafileProjectConfig - getFeatureFlagFromKey return null; } - - $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes); + $userContext = $this->createUserContext($userId, $attributes? $attributes : []); + $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userContext); $variation = $decision->getVariation(); $experiment = $decision->getExperiment(); $featureEnabled = $variation !== null ? $variation->getFeatureEnabled() : false; @@ -1124,7 +1141,7 @@ public function getAllFeatureVariables($featureFlagKey, $userId, $attributes = n return null; } - $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes); + $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $this->createUserContext($userId, $attributes)); $variation = $decision->getVariation(); $experiment = $decision->getExperiment(); $featureEnabled = $variation !== null ? $variation->getFeatureEnabled() : false; @@ -1247,7 +1264,14 @@ private function getFeatureVariableValueFromVariation($featureFlagKey, $variable */ public function isValid() { - return $this->getConfig() !== null; + if (!$this->getConfig()) { + $this->_logger->log( + Logger::ERROR, + Errors::NO_CONFIG + ); + return false; + } + return true; } /** @@ -1281,4 +1305,17 @@ protected function validateInputs(array $values, $logLevel = Logger::ERROR) return $isValid; } + + /** + * Gets the variation associated with experiment or rollout in instance of given feature flag key + * + * @param string Feature flag key + * @param string variation key + * + * @return Variation / null + */ + public function getFlagVariationByKey($flagKey, $variationKey) + { + return $this->getConfig()->getFlagVariationByKey($flagKey, $variationKey); + } } diff --git a/src/Optimizely/OptimizelyUserContext.php b/src/Optimizely/OptimizelyUserContext.php index c1b50264..41cc7288 100644 --- a/src/Optimizely/OptimizelyUserContext.php +++ b/src/Optimizely/OptimizelyUserContext.php @@ -17,23 +17,129 @@ namespace Optimizely; +use Optimizely\Logger\LoggerInterface; +use Optimizely\Logger\NoOpLogger; + class OptimizelyUserContext implements \JsonSerializable { private $optimizelyClient; private $userId; private $attributes; - - - public function __construct(Optimizely $optimizelyClient, $userId, array $attributes = []) + private $forcedDecisions; + + public function __construct(Optimizely $optimizelyClient, $userId, array $attributes = [], $forcedDecisions = null) { $this->optimizelyClient = $optimizelyClient; $this->userId = $userId; $this->attributes = $attributes; + $this->forcedDecisions = $forcedDecisions; + } + + public function setForcedDecision($context, $decision) + { + // check if SDK is ready + if (!$this->optimizelyClient->isValid()) { + return false; + } + $flagKey = $context->getFlagKey(); + if (!isset($flagKey)) { + return false; + } + $index = $this->findExistingRuleAndFlagKey($context); + if ($index != -1) { + $this->forcedDecisions[$index]->setOptimizelyForcedDecision($decision); + } else { + if (!$this->forcedDecisions) { + $this->forcedDecisions = array(); + } + array_push($this->forcedDecisions, new ForcedDecision($context, $decision)); + } + return true; + } + + public function getForcedDecision($context) + { + // check if SDK is ready + if (!$this->optimizelyClient->isValid()) { + return null; + } + return $this->findForcedDecision($context); + } + + public function removeForcedDecision($context) + { + // check if SDK is ready + if (!$this->optimizelyClient->isValid()) { + return false; + } + $index = $this->findExistingRuleAndFlagKey($context); + if ($index != -1) { + array_splice($this->forcedDecisions, $index, 1); + return true; + } + return false; + } + + public function removeAllForcedDecisions() + { + // check if SDK is ready + if (!$this->optimizelyClient->isValid()) { + return false; + } + + $this->forcedDecisions = []; + return true; } + public function findValidatedForcedDecision($context) + { + $decideReasons = []; + $flagKey = $context->getFlagKey(); + $ruleKey = $context->getRuleKey(); + $variationKey = $this->findForcedDecision($context); + $variation = null; + if ($variationKey) { + $variation = $this->optimizelyClient->getFlagVariationByKey($flagKey, $variationKey); + if ($variation) { + array_push($decideReasons, 'Decided by forced decision.'); + array_push($decideReasons, sprintf('Variation (%s) is mapped to %s and user (%s) in the forced decision map.', $variationKey, $ruleKey? 'flag ('.$flagKey.'), rule ('.$ruleKey.')': 'flag ('.$flagKey.')', $this->userId)); + } else { + array_push($decideReasons, sprintf('Invalid variation is mapped to %s and user (%s) in the forced decision map.', $ruleKey? 'flag ('.$flagKey.'), rule ('.$ruleKey.')': 'flag ('.$flagKey.')', $this->userId)); + } + } + return [$variation, $decideReasons]; + } + + private function findExistingRuleAndFlagKey($context) + { + if ($this->forcedDecisions) { + for ($index = 0; $index < count($this->forcedDecisions); $index++) { + if ($this->forcedDecisions[$index]->getOptimizelyDecisionContext()->getFlagKey() == $context->getFlagKey() && $this->forcedDecisions[$index]->getOptimizelyDecisionContext()->getRuleKey() == $context->getRuleKey()) { + return $index; + } + } + } + return -1; + } + + public function findForcedDecision($context) + { + $foundVariationKey = null; + if (!isset($this->forcedDecisions)) { + return null; + } + if (count($this->forcedDecisions) == 0) { + return null; + } + $index = $this->findExistingRuleAndFlagKey($context); + if ($index != -1) { + $foundVariationKey = $this->forcedDecisions[$index]->getOptimizelyForcedDecision()->getVariationKey(); + } + return $foundVariationKey; + } protected function copy() { - return new OptimizelyUserContext($this->optimizelyClient, $this->userId, $this->attributes); + return new OptimizelyUserContext($this->optimizelyClient, $this->userId, $this->attributes, $this->forcedDecisions); } public function setAttribute($key, $value) @@ -85,3 +191,87 @@ public function jsonSerialize() ]; } } +class ForcedDecision +{ + private $optimizelyDecisionContext; + private $optimizelyForcedDecision; + + public function __construct($optimizelyDecisionContext, $optimizelyForcedDecision) + { + $this->optimizelyDecisionContext = $optimizelyDecisionContext; + $this->setOptimizelyForcedDecision($optimizelyForcedDecision); + } + + /** + * @return mixed + */ + public function getOptimizelyDecisionContext() + { + return $this->optimizelyDecisionContext; + } + + /** + * @return mixed + */ + public function getOptimizelyForcedDecision() + { + return $this->optimizelyForcedDecision; + } + + public function setOptimizelyForcedDecision($optimizelyForcedDecision) + { + $this->optimizelyForcedDecision = $optimizelyForcedDecision; + } +} + +class OptimizelyDecisionContext +{ + private $flagKey; + private $ruleKey; + + public function __construct($flagKey, $ruleKey) + { + $this->flagKey = $flagKey; + $this->ruleKey = $ruleKey; + } + + /** + * @return mixed + */ + public function getFlagKey() + { + return $this->flagKey; + } + + /** + * @return mixed + */ + public function getRuleKey() + { + return $this->ruleKey; + } +} +class OptimizelyForcedDecision +{ + private $variationKey; + + public function __construct($variationKey) + { + $this->setVariationKey($variationKey); + } + + public function setVariationKey($variationKey) + { + if (isset($variationKey) && trim($variationKey) !== '') { + $this->variationKey = $variationKey; + } + } + + /** + * @return mixed + */ + public function getVariationKey() + { + return $this->variationKey; + } +} diff --git a/src/Optimizely/Utils/Errors.php b/src/Optimizely/Utils/Errors.php index f883412e..196b6729 100644 --- a/src/Optimizely/Utils/Errors.php +++ b/src/Optimizely/Utils/Errors.php @@ -20,4 +20,5 @@ class Errors { const INVALID_FORMAT = 'Provided %s is in an invalid format.'; const INVALID_DATAFILE = 'Datafile has invalid format. Failing "%s".'; + const NO_CONFIG = 'Optimizely SDK not configured properly yet.'; } diff --git a/tests/ConfigTests/DatafileProjectConfigTest.php b/tests/ConfigTests/DatafileProjectConfigTest.php index f7bd7938..bfd3d513 100644 --- a/tests/ConfigTests/DatafileProjectConfigTest.php +++ b/tests/ConfigTests/DatafileProjectConfigTest.php @@ -1,6 +1,6 @@ $this->config->getExperimentFromKey('rollout_1_exp_3'), 'rollout_2_exp_1' => $this->config->getExperimentFromKey('rollout_2_exp_1'), 'rollout_2_exp_2' => $this->config->getExperimentFromKey('rollout_2_exp_2'), + 'flag1_targeted_delivery' => $this->config->getExperimentFromKey('flag1_targeted_delivery'), + 'flag1_targeted_delivery2' => $this->config->getExperimentFromKey('flag1_targeted_delivery2'), + 'flag1_targeted_delivery3' => $this->config->getExperimentFromKey('flag1_targeted_delivery3'), + 'default-rollout-5969-20778120250' => $this->config->getExperimentFromKey('default-rollout-5969-20778120250'), ], $experimentKeyMap->getValue($this->config) ); @@ -147,6 +151,10 @@ public function testInit() '177774' => $this->config->getExperimentFromId('177774'), '177779' => $this->config->getExperimentFromId('177779'), '7716830083' => $this->config->getExperimentFromId('7716830083'), + '9300000018548' => $this->config->getExperimentFromId('9300000018548'), + '9300000018549' => $this->config->getExperimentFromId('9300000018549'), + '9300000018550' => $this->config->getExperimentFromId('9300000018550'), + 'default-rollout-5969-20778120250' => $this->config->getExperimentFromId('default-rollout-5969-20778120250'), ], $experimentIdMap->getValue($this->config) ); @@ -184,11 +192,67 @@ public function testInit() $this->assertEquals( [ '7718080042' => $this->config->getAudience('7718080042'), - '11155' => $this->config->getAudience('11155') + '11155' => $this->config->getAudience('11155'), + '20803170009' => $this->config->getAudience('20803170009'), + '20787080332' => $this->config->getAudience('20787080332'), + '20778250294' => $this->config->getAudience('20778250294') ], $audienceIdMap->getValue($this->config) ); + // Check flag key variations map + $flagVariationsMap = new \ReflectionProperty(DatafileProjectConfig::class, '_flagVariationsMap'); + $flagVariationsMap->setAccessible(true); + + $this->assertEquals( + [ + 'boolean_feature' => [ + $this->config->getVariationFromKey('test_experiment_2', 'test_variation_1'), + $this->config->getVariationFromKey('test_experiment_2', 'test_variation_2') + ], + 'double_single_variable_feature' => [ + $this->config->getVariationFromKey('test_experiment_double_feature', 'control'), + $this->config->getVariationFromKey('test_experiment_double_feature', 'variation') + ], + 'integer_single_variable_feature' => [ + $this->config->getVariationFromKey('test_experiment_integer_feature', 'control'), + $this->config->getVariationFromKey('test_experiment_integer_feature', 'variation') + ], + 'boolean_single_variable_feature' => [ + $this->config->getVariationFromKey('rollout_1_exp_1', '177771'), + $this->config->getVariationFromKey('rollout_1_exp_2', '177773'), + $this->config->getVariationFromKey('rollout_1_exp_3', '177778') + ], + 'string_single_variable_feature' => [ + $this->config->getVariationFromKey('test_experiment_with_feature_rollout', 'control'), + $this->config->getVariationFromKey('test_experiment_with_feature_rollout', 'variation'), + $this->config->getVariationFromKey('rollout_2_exp_1', '177775'), + $this->config->getVariationFromKey('rollout_2_exp_2', '177780') + ], + 'multiple_variables_feature' => [ + $this->config->getVariationFromKey('test_experiment_json_feature', 'json_variation') + ], + 'multi_variate_feature' => [ + $this->config->getVariationFromKey('test_experiment_multivariate', 'Fred'), + $this->config->getVariationFromKey('test_experiment_multivariate', 'Feorge'), + $this->config->getVariationFromKey('test_experiment_multivariate', 'Gred'), + $this->config->getVariationFromKey('test_experiment_multivariate', 'George') + ], + 'mutex_group_feature' => [ + $this->config->getVariationFromKey('group_experiment_1', 'group_exp_1_var_1'), + $this->config->getVariationFromKey('group_experiment_1', 'group_exp_1_var_2'), + $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_1'), + $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_2') + ], + 'empty_feature' => [], + 'same_variation_flag' => [ + $this->config->getVariationFromKey('flag1_targeted_delivery', 'new_variation'), + $this->config->getVariationFromKey('default-rollout-5969-20778120250', 'off') + ] + ], + $flagVariationsMap->getValue($this->config) + ); + // Check variation key map $variationKeyMap = new \ReflectionProperty(DatafileProjectConfig::class, '_variationKeyMap'); $variationKeyMap->setAccessible(true); @@ -250,6 +314,18 @@ public function testInit() ], 'test_experiment_json_feature' => [ 'json_variation' => $this->config->getVariationFromKey('test_experiment_json_feature', 'json_variation') + ], + 'flag1_targeted_delivery' => [ + 'new_variation' => $this->config->getVariationFromKey('flag1_targeted_delivery', 'new_variation') + ], + 'flag1_targeted_delivery2' => [ + 'new_variation' => $this->config->getVariationFromKey('flag1_targeted_delivery2', 'new_variation') + ], + 'flag1_targeted_delivery3' => [ + 'new_variation' => $this->config->getVariationFromKey('flag1_targeted_delivery3', 'new_variation') + ], + 'default-rollout-5969-20778120250' => [ + 'off' => $this->config->getVariationFromKey('default-rollout-5969-20778120250', 'off') ] ], $variationKeyMap->getValue($this->config) @@ -315,6 +391,18 @@ public function testInit() ], 'test_experiment_json_feature' => [ '122246' => $this->config->getVariationFromId('test_experiment_json_feature', '122246') + ], + 'flag1_targeted_delivery' => [ + '16421' => $this->config->getVariationFromId('flag1_targeted_delivery', '16421') + ], + 'flag1_targeted_delivery2' => [ + '16421' => $this->config->getVariationFromId('flag1_targeted_delivery2', '16421') + ], + 'flag1_targeted_delivery3' => [ + '16421' => $this->config->getVariationFromId('flag1_targeted_delivery3', '16421') + ], + 'default-rollout-5969-20778120250' => [ + '16419' => $this->config->getVariationFromId('default-rollout-5969-20778120250', '16419') ] ], $variationIdMap->getValue($this->config) @@ -334,7 +422,8 @@ public function testInit() 'multiple_variables_feature' => $this->config->getFeatureFlagFromKey('multiple_variables_feature'), 'multi_variate_feature' => $this->config->getFeatureFlagFromKey('multi_variate_feature'), 'mutex_group_feature' => $this->config->getFeatureFlagFromKey('mutex_group_feature'), - 'empty_feature' => $this->config->getFeatureFlagFromKey('empty_feature') + 'empty_feature' => $this->config->getFeatureFlagFromKey('empty_feature'), + 'same_variation_flag' => $this->config->getFeatureFlagFromKey('same_variation_flag') ], $featureFlagKeyMap->getValue($this->config) ); @@ -346,7 +435,8 @@ public function testInit() $this->assertEquals( [ '166660' => $this->config->getRolloutFromId('166660'), - '166661' => $this->config->getRolloutFromId('166661') + '166661' => $this->config->getRolloutFromId('166661'), + 'rollout-5969-20778120250' => $this->config->getRolloutFromId('rollout-5969-20778120250') ], $rolloutIdMap->getValue($this->config) ); diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 6c4669f8..6f81a924 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -28,6 +28,7 @@ use Optimizely\Logger\DefaultLogger; use Optimizely\Logger\NoOpLogger; use Optimizely\Optimizely; +use Optimizely\OptimizelyUserContext; use Optimizely\UserProfile\UserProfileServiceInterface; use Optimizely\Utils\Validator; @@ -40,7 +41,7 @@ class DecisionServiceTest extends \PHPUnit_Framework_TestCase private $loggerMock; private $testUserId; private $userProvideServiceMock; - + private $optimizely; public function setUp() { $this->testUserId = 'testUserId'; @@ -84,6 +85,7 @@ public function setUp() ->setConstructorArgs(array($this->loggerMock)) ->setMethods(array('getVariation')) ->getMock(); + $this->optimizely = new Optimizely(DATAFILE, new ValidEventDispatcher(), $this->loggerMock); } public function compareFeatureDecisionsExceptReasons(FeatureDecision $expectedObj, FeatureDecision $actualObj) @@ -104,13 +106,15 @@ public function testGetVariationReturnsNullWhenExperimentIsNotRunning() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $pausedExperiment, $this->testUserId); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $pausedExperiment, $this->optimizely->createUserContext($this->testUserId)); $this->assertNull($variation); } public function testGetVariationBucketsUserWhenExperimentIsRunning() { + $optimizely = new Optimizely(DATAFILE, new ValidEventDispatcher(), $this->loggerMock); + $expectedVariation = new Variation('7722370027', 'control'); $this->bucketerMock->expects($this->once()) ->method('bucket') @@ -122,7 +126,7 @@ public function testGetVariationBucketsUserWhenExperimentIsRunning() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->testUserId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext($this->testUserId, $this->testUserAttributes)); $this->assertEquals( $expectedVariation, @@ -219,7 +223,7 @@ public function testGetVariationReturnsWhitelistedVariation() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1'); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext('user1')); $this->assertEquals( $expectedVariation, @@ -260,7 +264,7 @@ public function testGetVariationReturnsWhitelistedVariationForGroupedExperiment( $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1'); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext('user1')); $this->assertEquals( $expectedVariation, @@ -290,7 +294,7 @@ public function testGetVariationBucketsWhenForcedVariationsIsEmpty() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1', $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext('user1', $this->testUserAttributes)); $this->assertEquals( $expectedVariation, @@ -321,7 +325,7 @@ public function testGetVariationBucketsWhenWhitelistedVariationIsInvalid() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1', $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext('user1', $this->testUserAttributes)); $this->assertEquals( $expectedVariation, @@ -342,7 +346,7 @@ public function testGetVariationBucketsUserWhenUserIsNotWhitelisted() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'not_whitelisted_user', $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext('not_whitelisted_user', $this->testUserAttributes)); $this->assertEquals( $expectedVariation, @@ -352,6 +356,8 @@ public function testGetVariationBucketsUserWhenUserIsNotWhitelisted() public function testGetVariationReturnsNullIfUserDoesNotMeetAudienceConditions() { + $optimizely = new Optimizely(DATAFILE, new ValidEventDispatcher(), $this->loggerMock); + $this->bucketerMock->expects($this->never()) ->method('bucket'); @@ -361,7 +367,7 @@ public function testGetVariationReturnsNullIfUserDoesNotMeetAudienceConditions() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->testUserId); // no matching attributes + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext($this->testUserId)); // no matching attributes $this->assertNull($variation); } @@ -399,12 +405,14 @@ public function testGetVariationReturnsStoredVariationIfAvailable() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext($userId)); $this->assertEquals($expectedVariation, $variation); } public function testGetVariationBucketsIfNoStoredVariation() { + $optimizely = new Optimizely(DATAFILE, new ValidEventDispatcher(), $this->loggerMock); + $userId = $this->testUserId; $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); $expectedVariation = new Variation('7722370027', 'control'); @@ -443,7 +451,7 @@ public function testGetVariationBucketsIfNoStoredVariation() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext($userId, $this->testUserAttributes)); $this->assertEquals($expectedVariation, $variation); // Verify Logs @@ -496,7 +504,7 @@ public function testGetVariationBucketsIfStoredVariationIsInvalid() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext($userId, $this->testUserAttributes)); $this->assertEquals($expectedVariation, $variation); // Verify Logs @@ -511,6 +519,8 @@ public function testGetVariationBucketsIfStoredVariationIsInvalid() public function testGetVariationBucketsIfUserProfileServiceLookupThrows() { + $optimizely = new Optimizely(DATAFILE, new ValidEventDispatcher(), $this->loggerMock); + $userId = $this->testUserId; $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); $expectedVariation = new Variation('7722370027', 'control'); @@ -553,7 +563,7 @@ public function testGetVariationBucketsIfUserProfileServiceLookupThrows() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext($userId, $this->testUserAttributes)); $this->assertEquals($expectedVariation, $variation); // Verify Logs @@ -601,7 +611,7 @@ public function testGetVariationBucketsIfUserProfileServiceSaveThrows() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->optimizely->createUserContext($userId, $this->testUserAttributes)); $this->assertEquals($expectedVariation, $variation); // Verify Logs @@ -724,7 +734,7 @@ public function testGetVariationForFeatureExperimentGivenNullExperimentIds() ->method('log') ->with(Logger::DEBUG, "The feature flag 'empty_feature' is not used in any experiments."); - $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', []); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, $this->optimizely->createUserContext('user1', [])); $this->assertNull($actualDecision->getVariation()); } @@ -747,7 +757,7 @@ public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile "The user 'user1' is not bucketed into any of the experiments using the feature 'boolean_feature'." ); - $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', []); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, $this->optimizely->createUserContext('user1', [])); $this->assertNull($actualDecision->getVariation()); } @@ -769,7 +779,7 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNot ); $featureFlag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); - $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', []); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, $this->optimizely->createUserContext('user1', [])); $this->assertNull($actualDecision->getVariation()); } @@ -793,7 +803,7 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsB "The user 'user_1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'." ); - $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user_1', []); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', [])); $this->compareFeatureDecisionsExceptReasons($expected_decision, $actualDecision); } @@ -818,7 +828,7 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBuck "The user 'user_1' is bucketed into experiment 'group_experiment_1' of feature 'mutex_group_feature'." ); - $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user_1', []); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', [])); $this->compareFeatureDecisionsExceptReasons($expected_decision, $actualDecision); } @@ -844,8 +854,7 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuc $actualFeatureDecision = $this->decisionServiceMock->getVariationForFeatureExperiment( $this->config, $featureFlag, - 'user_1', - [] + $this->optimizely->createUserContext('user_1', []) ); $this->assertNull($actualFeatureDecision->getVariation()); } @@ -874,7 +883,7 @@ public function testGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperi $this->assertEquals( $expected_decision, - $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, 'user_1', []) + $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', [])) ); } @@ -912,7 +921,7 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout() "User 'user_1' is bucketed into rollout for feature flag 'string_single_variable_feature'." ); - $actualFeatureDecision = $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, 'user_1', []); + $actualFeatureDecision = $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', [])); $this->compareFeatureDecisionsExceptReasons($expected_decision, $actualFeatureDecision); } @@ -948,7 +957,7 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur FeatureDecision::DECISION_SOURCE_ROLLOUT ); - $actualFeatureDecision = $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, 'user_1', []); + $actualFeatureDecision = $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', [])); $this->compareFeatureDecisionsExceptReasons($expectedDecision, $actualFeatureDecision); } @@ -969,8 +978,7 @@ public function testGetVariationForFeatureRolloutWhenNoRolloutIsAssociatedToFeat $actualFeatureDecision = $this->decisionServiceMock->getVariationForFeatureRollout( $this->config, $featureFlag, - 'user_1', - [] + $this->optimizely->createUserContext('user_1', []) ); $this->assertNull($actualFeatureDecision->getVariation()); } @@ -993,8 +1001,7 @@ public function testGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() $actualFeatureDecision = $this->decisionServiceMock->getVariationForFeatureRollout( $this->config, $featureFlag, - 'user_1', - [] + $this->optimizely->createUserContext('user_1', []) ); $this->assertNull($actualFeatureDecision->getVariation()); } @@ -1020,7 +1027,7 @@ public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperimen ->method('getRolloutFromId') ->will($this->returnValue($experiment_less_rollout)); - $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout($configMock, $featureFlag, 'user_1', []); + $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout($configMock, $featureFlag, $this->optimizely->createUserContext('user_1', [])); $this->assertNull($actualFeatureDecision->getVariation()); } @@ -1053,7 +1060,7 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $this->compareFeatureDecisionsExceptReasons( $expected_decision, - $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes) + $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', $user_attributes)) ); } @@ -1071,7 +1078,15 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $expected_decision = new FeatureDecision( $experiment2, $expected_variation, - FeatureDecision::DECISION_SOURCE_ROLLOUT + FeatureDecision::DECISION_SOURCE_ROLLOUT, + [ + 'Audiences for rule 1 collectively evaluated to TRUE.', + 'User "user_1" meets condition for targeting rule "1".', + 'User "user_1" is not in the traffic group for targeting rule "1". Checking Everyone Else rule now.', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "user_1" meets condition for targeting rule "Everyone Else".', + 'User "user_1" is in the traffic group of targeting rule "Everyone Else".' + ] ); // Provide attributes such that user qualifies for audience @@ -1091,7 +1106,7 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $this->assertEquals( $expected_decision, - $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes) + $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', $user_attributes)) ); } @@ -1124,8 +1139,7 @@ public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheT $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout( $this->config, $featureFlag, - 'user_1', - $user_attributes + $this->optimizely->createUserContext('user_1', $user_attributes) ); $this->assertNull($actualFeatureDecision->getVariation()); @@ -1150,7 +1164,16 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $expected_decision = new FeatureDecision( $experiment2, $expected_variation, - FeatureDecision::DECISION_SOURCE_ROLLOUT + FeatureDecision::DECISION_SOURCE_ROLLOUT, + [ + 'Audiences for rule 1 collectively evaluated to FALSE.' , + 'User "user_1" does not meet conditions for targeting rule "1".', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "user_1" does not meet conditions for targeting rule "2".', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "user_1" meets condition for targeting rule "Everyone Else".', + 'User "user_1" is in the traffic group of targeting rule "Everyone Else".' + ] ); // Provide null attributes so that user does not qualify for audience @@ -1171,12 +1194,12 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $this->assertEquals( $expected_decision, - $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes) + $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', $user_attributes)) ); // Verify Logs - $this->assertContains([Logger::DEBUG, "User 'user_1' does not meet conditions for targeting rule 1."], $this->collectedLogs); - $this->assertContains([Logger::DEBUG, "User 'user_1' does not meet conditions for targeting rule 2."], $this->collectedLogs); + $this->assertContains([Logger::DEBUG, 'User "user_1" does not meet conditions for targeting rule "1".'], $this->collectedLogs); + $this->assertContains([Logger::DEBUG, 'User "user_1" does not meet conditions for targeting rule "2".'], $this->collectedLogs); } public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargetingRuleOrEveryoneElseRule() @@ -1208,14 +1231,14 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar ->method('log') ->will($this->returnCallback($this->collectLogsForAssertion)); - $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes); + $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', $user_attributes)); $this->assertNull($actualFeatureDecision->getVariation()); // Verify Logs - $this->assertContains([Logger::DEBUG, "User 'user_1' does not meet conditions for targeting rule 1."], $this->collectedLogs); - $this->assertContains([Logger::DEBUG, "User 'user_1' does not meet conditions for targeting rule 2."], $this->collectedLogs); - $this->assertContains([Logger::DEBUG, "User 'user_1' does not meet conditions for targeting rule 'Everyone Else'."], $this->collectedLogs); + $this->assertContains([Logger::DEBUG, 'User "user_1" does not meet conditions for targeting rule "1".'], $this->collectedLogs); + $this->assertContains([Logger::DEBUG, 'User "user_1" does not meet conditions for targeting rule "2".'], $this->collectedLogs); + $this->assertContains([Logger::DEBUG, 'User "user_1" does not meet conditions for targeting rule "Everyone Else".'], $this->collectedLogs); } @@ -1232,7 +1255,16 @@ public function testGetVariationForFeatureRolloutLogging() $expected_decision = new FeatureDecision( $experiment2, $expected_variation, - FeatureDecision::DECISION_SOURCE_ROLLOUT + FeatureDecision::DECISION_SOURCE_ROLLOUT, + [ + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "user_1" does not meet conditions for targeting rule "1".', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "user_1" does not meet conditions for targeting rule "2".', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "user_1" meets condition for targeting rule "Everyone Else".', + 'User "user_1" is in the traffic group of targeting rule "Everyone Else".' + ] ); // Provide null attributes so that user does not qualify for audience @@ -1253,7 +1285,7 @@ public function testGetVariationForFeatureRolloutLogging() $this->assertEquals( $expected_decision, - $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes) + $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, $this->optimizely->createUserContext('user_1', $user_attributes)) ); // Verify Logs diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 51504308..c6b450c7 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -46,6 +46,8 @@ use Optimizely\Event\Builder\EventBuilder; use Optimizely\Logger\DefaultLogger; use Optimizely\Optimizely; +use Optimizely\OptimizelyDecisionContext; +use Optimizely\OptimizelyForcedDecision; use Optimizely\OptimizelyUserContext; use Optimizely\UserProfile\UserProfileServiceInterface; @@ -475,6 +477,273 @@ public function testDecide() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } + public function testSetForcedDecisionExperimentRuleToDecisionSendImpressionAndNotification() + { + $optimizely = new Optimizely($this->datafile); + $userContext = $optimizely->createUserContext('test_user', ['device_type' => 'iPhone']); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'variation'); + + // assert that featureEnabled for $variation is true + $this->assertFalse($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> false, + 'variables'=> ["double_variable" => 14.99], + 'variationKey' => 'variation', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + '122238', + 'variation', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + false, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + $context = new OptimizelyDecisionContext('double_single_variable_feature', 'test_experiment_double_feature'); + $decision = new OptimizelyForcedDecision('variation'); + $this->assertTrue($userContext->setForcedDecision($context, $decision)); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'variation', + false, + ['double_variable' => 14.99], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testSetForcedDecisionDeliveryRuleToDecisionSendImpressionAndNotification() + { + $optimizely = new Optimizely($this->datafile); + $userContext = $optimizely->createUserContext('test_user', ['device_type' => 'iPhone']); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('rollout_1_exp_2'); + $variation = $this->projectConfig->getVariationFromKey('rollout_1_exp_2', '177773'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'boolean_single_variable_feature', + 'enabled'=> true, + 'variables'=> ['boolean_variable' => false], + 'variationKey' => '177773', + 'ruleKey' => 'rollout_1_exp_2', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + '177772', + '177773', + 'boolean_single_variable_feature', + 'rollout_1_exp_2', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $context = new OptimizelyDecisionContext('boolean_single_variable_feature', 'rollout_1_exp_2'); + $decision = new OptimizelyForcedDecision('177773'); + $this->assertTrue($userContext->setForcedDecision($context, $decision)); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'boolean_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + '177773', + true, + ['boolean_variable' => false], + 'rollout_1_exp_2', + 'boolean_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testSetForcedDecisionFlagToDecisionSendImpressionAndNotification() + { + $optimizely = new Optimizely($this->datafile); + $userContext = $optimizely->createUserContext('test_user', ['device_type' => 'iPhone']); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'variation'); + + // assert that featureEnabled for $variation is true + $this->assertFalse($variation->getFeatureEnabled()); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> false, + 'variables'=> ["double_variable" => 14.99], + 'variationKey' => 'variation', + 'ruleKey' => null, + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + '', + 'variation', + 'double_single_variable_feature', + '', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + false, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $context = new OptimizelyDecisionContext('double_single_variable_feature', null); + $decision = new OptimizelyForcedDecision('variation'); + $this->assertTrue($userContext->setForcedDecision($context, $decision)); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'variation', + false, + ['double_variable' => 14.99], + null, + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + public function testDecideWhenSdkNotReady() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) @@ -1994,7 +2263,7 @@ public function testDecideAll() $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - $this->notificationCenterMock->expects($this->exactly(9)) + $this->notificationCenterMock->expects($this->exactly(10)) ->method('sendNotifications'); //assert that sendImpressionEvent is called with expected params @@ -2025,7 +2294,7 @@ public function testDecideAll() $optimizelyDecisions = $optimizelyMock->decideAll($userContext); - $this->assertEquals(count($optimizelyDecisions), 9); + $this->assertEquals(count($optimizelyDecisions), 10); $this->compareOptimizelyDecisions($expectedOptimizelyDecision1, $optimizelyDecisions['double_single_variable_feature']); $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecisions['boolean_feature']); @@ -2045,6 +2314,7 @@ public function testDecideAllCallsDecideForKeysAndReturnsItsResponse() 'boolean_single_variable_feature', 'string_single_variable_feature', 'multiple_variables_feature', + 'same_variation_flag', 'multi_variate_feature', 'mutex_group_feature', 'empty_feature' @@ -4836,7 +5106,7 @@ public function testGetEnabledFeaturesGivenNoFeatureIsEnabledForUser() ->getMock(); // Mock isFeatureEnabled to return false for all calls - $optimizelyMock->expects($this->exactly(9)) + $optimizelyMock->expects($this->exactly(10)) ->method('isFeatureEnabled') ->will($this->returnValue(false)); @@ -4862,7 +5132,7 @@ public function testGetEnabledFeaturesGivenFeaturesAreEnabledForUser() ]; // Mock isFeatureEnabled to return specific values - $optimizelyMock->expects($this->exactly(9)) + $optimizelyMock->expects($this->exactly(10)) ->method('isFeatureEnabled') ->will($this->returnValueMap($map)); @@ -4885,7 +5155,7 @@ public function testGetEnabledFeaturesGivenFeaturesAreEnabledForEmptyUserID() ]; // Mock isFeatureEnabled to return specific values - $optimizelyMock->expects($this->exactly(9)) + $optimizelyMock->expects($this->exactly(10)) ->method('isFeatureEnabled') ->will($this->returnValueMap($map)); @@ -4914,19 +5184,20 @@ public function testGetEnabledFeaturesWithUserAttributes() ['integer_single_variable_feature','user_id', $userAttributes, false], ['boolean_single_variable_feature','user_id', $userAttributes, false], ['string_single_variable_feature','user_id', $userAttributes, false], + ['same_variation_flag','user_id', $userAttributes, true], ['multi_variate_feature','user_id', $userAttributes, false], ['mutex_group_feature','user_id', $userAttributes, false], ['empty_feature','user_id', $userAttributes, true], ]; // Assert that isFeatureEnabled is called with the same attributes and mock to return value map - $optimizelyMock->expects($this->exactly(9)) + $optimizelyMock->expects($this->exactly(10)) ->method('isFeatureEnabled') ->with() ->will($this->returnValueMap($map)); $this->assertEquals( - ['empty_feature'], + ['same_variation_flag', 'empty_feature'], $optimizelyMock->getEnabledFeatures("user_id", $userAttributes) ); } @@ -4988,22 +5259,27 @@ public function testGetEnabledFeaturesCallsDecisionListenerForAllFeatures() FeatureDecision::DECISION_SOURCE_FEATURE_TEST ); $decision7 = new FeatureDecision( + $experiment, + $enabledFeatureVariation, + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); + $decision8 = new FeatureDecision( $disabledFeatureExperiment, $disabledFeatureVariation, FeatureDecision::DECISION_SOURCE_FEATURE_TEST ); - $decision8 = new FeatureDecision( + $decision9 = new FeatureDecision( $experiment, $enabledFeatureVariation, FeatureDecision::DECISION_SOURCE_ROLLOUT ); - $decision9 = new FeatureDecision( + $decision10 = new FeatureDecision( $disabledFeatureExperiment, $disabledFeatureVariation, FeatureDecision::DECISION_SOURCE_ROLLOUT ); - $decisionServiceMock->expects($this->exactly(9)) + $decisionServiceMock->expects($this->exactly(10)) ->method('getVariationForFeature') ->will($this->onConsecutiveCalls( $decision1, @@ -5014,7 +5290,8 @@ public function testGetEnabledFeaturesCallsDecisionListenerForAllFeatures() $decision6, $decision7, $decision8, - $decision9 + $decision9, + $decision10 )); $optimizelyMock->notificationCenter = $this->notificationCenterMock; @@ -5132,8 +5409,24 @@ public function testGetEnabledFeaturesCallsDecisionListenerForAllFeatures() ) ) ); - $this->notificationCenterMock->expects($this->at(6)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + array( + DecisionNotificationTypes::FEATURE, + 'user_id', + [], + (object) array( + 'featureKey'=>'same_variation_flag', + 'featureEnabled'=> true, + 'source'=> 'rollout', + 'sourceInfo'=> (object) array() + ) + ) + ); + + $this->notificationCenterMock->expects($this->at(7)) ->method('sendNotifications') ->with( NotificationType::DECISION, @@ -5153,7 +5446,7 @@ public function testGetEnabledFeaturesCallsDecisionListenerForAllFeatures() ) ); - $this->notificationCenterMock->expects($this->at(7)) + $this->notificationCenterMock->expects($this->at(8)) ->method('sendNotifications') ->with( NotificationType::DECISION, @@ -5170,7 +5463,7 @@ public function testGetEnabledFeaturesCallsDecisionListenerForAllFeatures() ) ); - $this->notificationCenterMock->expects($this->at(8)) + $this->notificationCenterMock->expects($this->at(9)) ->method('sendNotifications') ->with( NotificationType::DECISION, @@ -5193,6 +5486,7 @@ public function testGetEnabledFeaturesCallsDecisionListenerForAllFeatures() 'integer_single_variable_feature', 'string_single_variable_feature', 'multiple_variables_feature', + 'same_variation_flag', 'mutex_group_feature' ], $optimizelyMock->getEnabledFeatures("user_id") diff --git a/tests/OptimizelyUserContextTest.php b/tests/OptimizelyUserContextTest.php index c41667b0..a0f406d5 100644 --- a/tests/OptimizelyUserContextTest.php +++ b/tests/OptimizelyUserContextTest.php @@ -17,11 +17,14 @@ namespace Optimizely\Tests; use Exception; +use Optimizely\Decide\OptimizelyDecideOption; use TypeError; use Optimizely\Logger\NoOpLogger; use Optimizely\Optimizely; use Optimizely\OptimizelyUserContext; +use Optimizely\OptimizelyDecisionContext; +use Optimizely\OptimizelyForcedDecision; class OptimizelyUserContextTest extends \PHPUnit_Framework_TestCase { @@ -258,4 +261,404 @@ public function testJsonSerialize() 'attributes' => $attributes ], json_decode(json_encode($optUserContext), true)); } + + // Forced decision tests + + public function testForcedDecisionInvalidDatafileReturnStatus() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $invalidOptlyObject = new Optimizely("Invalid datafile"); + + $optUserContext = new OptimizelyUserContext($invalidOptlyObject, $userId, $attributes); + + $context = new OptimizelyDecisionContext("flag1", "targeted_delivery"); + $decision = new OptimizelyForcedDecision("variation1"); + + $setForcedDecision = $optUserContext->setForcedDecision($context, $decision); + $this->assertFalse($setForcedDecision); + + $getForcedDecision = $optUserContext->getForcedDecision($context); + $this->assertNull($getForcedDecision); + + $removeForcedDecision = $optUserContext->removeForcedDecision($context); + $this->assertFalse($removeForcedDecision); + + $removeAllForcedDecision = $optUserContext->removeAllForcedDecisions(); + $this->assertFalse($removeAllForcedDecision); + } + + public function testForcedDecisionValidDatafileReturnStatus() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + + $context = new OptimizelyDecisionContext("flag1", "targeted_delivery"); + $decision = new OptimizelyForcedDecision("variation1"); + + $setForcedDecision = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision); + + $getForcedDecision = $optUserContext->getForcedDecision($context); + $this->assertEquals($getForcedDecision, "variation1"); + + $removeForcedDecision = $optUserContext->removeForcedDecision($context); + $this->assertTrue($removeForcedDecision); + + $removeAllForcedDecision = $optUserContext->removeAllForcedDecisions(); + $this->assertTrue($removeAllForcedDecision); + } + + public function testForcedDecisionFlagToDecision() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + + $context = new OptimizelyDecisionContext("boolean_single_variable_feature", null); + $decision = new OptimizelyForcedDecision("177773"); + + $setForcedDecision = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision); + + $getForcedDecision = $optUserContext->getForcedDecision($context); + $this->assertEquals($getForcedDecision, "177773"); + + $decision = $optUserContext->decide('boolean_single_variable_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals($decision->getVariationKey(), "177773"); + $this->assertNull($decision->getRuleKey()); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals($decision->getFlagKey(), "boolean_single_variable_feature"); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(count($decision->getUserContext()->getAttributes()), 1); + $this->assertEquals($decision->getReasons(), [ + 'Decided by forced decision.', + 'Variation (177773) is mapped to flag (boolean_single_variable_feature) and user (test_user) in the forced decision map.' + ]); + + // Removing forced decision to test + $removeForcedDecision = $optUserContext->removeForcedDecision($context); + $this->assertTrue($removeForcedDecision); + + $decision = $optUserContext->decide('boolean_single_variable_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals($decision->getVariationKey(), "177778"); + $this->assertEquals($decision->getRuleKey(), "rollout_1_exp_3"); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals($decision->getFlagKey(), "boolean_single_variable_feature"); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(count($decision->getUserContext()->getAttributes()), 1); + $this->assertEquals($decision->getReasons(), [ + "The feature flag 'boolean_single_variable_feature' is not used in any experiments.", + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "1".', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "2".', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "test_user" meets condition for targeting rule "Everyone Else".', + 'Assigned bucket 3041 to user "test_user" with bucketing ID "test_user".', + 'User "test_user" is in the traffic group of targeting rule "Everyone Else".', + "User 'test_user' is bucketed into rollout for feature flag 'boolean_single_variable_feature'." + ]); + } + public function testForcedDecisionExperimentRuleToDecision() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + + $context = new OptimizelyDecisionContext("boolean_feature", 'test_experiment_2'); + $decision = new OptimizelyForcedDecision("test_variation_1"); + + $setForcedDecision = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision); + + $getForcedDecision = $optUserContext->getForcedDecision($context); + $this->assertEquals($getForcedDecision, "test_variation_1"); + + $decision = $optUserContext->decide('boolean_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals($decision->getVariationKey(), "test_variation_1"); + $this->assertEquals($decision->getRuleKey(), "test_experiment_2"); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals($decision->getFlagKey(), "boolean_feature"); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(count($decision->getUserContext()->getAttributes()), 1); + $this->assertEquals($decision->getReasons(), [ + 'Decided by forced decision.', + 'Variation (test_variation_1) is mapped to flag (boolean_feature), rule (test_experiment_2) and user (test_user) in the forced decision map.', + "The user 'test_user' is bucketed into experiment 'test_experiment_2' of feature 'boolean_feature'." + ]); + + // Removing forced decision to test + $removeForcedDecision = $optUserContext->removeForcedDecision($context); + $this->assertTrue($removeForcedDecision); + + $decision = $optUserContext->decide('boolean_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals($decision->getVariationKey(), "test_variation_2"); + $this->assertEquals($decision->getRuleKey(), "test_experiment_2"); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals($decision->getFlagKey(), "boolean_feature"); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(count($decision->getUserContext()->getAttributes()), 1); + $this->assertEquals($decision->getReasons(), [ + 'Audiences for experiment "test_experiment_2" collectively evaluated to TRUE.', + 'Assigned bucket 9075 to user "test_user" with bucketing ID "test_user".', + 'User "test_user" is in variation test_variation_2 of experiment test_experiment_2.', + "The user 'test_user' is bucketed into experiment 'test_experiment_2' of feature 'boolean_feature'." + ]); + } + public function testForcedDecisionRuleDeliveryRuleToDecision() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + + $context = new OptimizelyDecisionContext("boolean_single_variable_feature", "rollout_1_exp_3"); + $decision = new OptimizelyForcedDecision("177773"); + + $setForcedDecision = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision); + + $getForcedDecision = $optUserContext->getForcedDecision($context); + $this->assertEquals($getForcedDecision, "177773"); + + $decision = $optUserContext->decide('boolean_single_variable_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals($decision->getVariationKey(), "177773"); + $this->assertEquals($decision->getRuleKey(), "rollout_1_exp_3"); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals($decision->getFlagKey(), "boolean_single_variable_feature"); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(count($decision->getUserContext()->getAttributes()), 1); + $this->assertEquals($decision->getReasons(), [ + "The feature flag 'boolean_single_variable_feature' is not used in any experiments.", + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "1".', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "2".', + 'Decided by forced decision.', + 'Variation (177773) is mapped to flag (boolean_single_variable_feature), rule (rollout_1_exp_3) and user (test_user) in the forced decision map.', + "User 'test_user' is bucketed into rollout for feature flag 'boolean_single_variable_feature'." + ]); + + // Removing forced decision to test + $removeForcedDecision = $optUserContext->removeForcedDecision($context); + $this->assertTrue($removeForcedDecision); + + $decision = $optUserContext->decide('boolean_single_variable_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals($decision->getVariationKey(), "177778"); + $this->assertEquals($decision->getRuleKey(), "rollout_1_exp_3"); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals($decision->getFlagKey(), "boolean_single_variable_feature"); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(count($decision->getUserContext()->getAttributes()), 1); + $this->assertEquals($decision->getReasons(), [ + "The feature flag 'boolean_single_variable_feature' is not used in any experiments.", + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "1".', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "2".', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "test_user" meets condition for targeting rule "Everyone Else".', + 'Assigned bucket 3041 to user "test_user" with bucketing ID "test_user".', + 'User "test_user" is in the traffic group of targeting rule "Everyone Else".', + "User 'test_user' is bucketed into rollout for feature flag 'boolean_single_variable_feature'." + ]); + } + + public function testForcedDecisionInvalidDeliveryRuleToDecision() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + $context = new OptimizelyDecisionContext("boolean_single_variable_feature", "rollout_1_exp_3"); + $decision = new OptimizelyForcedDecision("invalid"); + $setForcedDecision = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision); + + $decision = $optUserContext->decide('boolean_single_variable_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals($decision->getVariationKey(), "177778"); + $this->assertEquals($decision->getRuleKey(), "rollout_1_exp_3"); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals($decision->getFlagKey(), "boolean_single_variable_feature"); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(count($decision->getUserContext()->getAttributes()), 1); + $this->assertEquals($decision->getReasons(), [ + "The feature flag 'boolean_single_variable_feature' is not used in any experiments.", + 'Audiences for rule 1 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "1".', + 'Audiences for rule 2 collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions for targeting rule "2".', + 'Invalid variation is mapped to flag (boolean_single_variable_feature), rule (rollout_1_exp_3) and user (test_user) in the forced decision map.', + 'Audiences for rule Everyone Else collectively evaluated to TRUE.', + 'User "test_user" meets condition for targeting rule "Everyone Else".', + 'Assigned bucket 3041 to user "test_user" with bucketing ID "test_user".', + 'User "test_user" is in the traffic group of targeting rule "Everyone Else".', + "User 'test_user' is bucketed into rollout for feature flag 'boolean_single_variable_feature'." + ]); + } + + public function testForcedDecisionInvalidExperimentRuleToDecision() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + $context = new OptimizelyDecisionContext("boolean_feature", null); + $decision = new OptimizelyForcedDecision("invalid"); + $setForcedDecision = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision); + + $decision = $optUserContext->decide('boolean_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals("test_variation_2", $decision->getVariationKey()); + $this->assertEquals("test_experiment_2", $decision->getRuleKey()); + $this->assertTrue($decision->getEnabled()); + $this->assertEquals("boolean_feature", $decision->getFlagKey()); + $this->assertEquals($decision->getUserContext()->getUserId(), $userId); + $this->assertEquals(1, count($decision->getUserContext()->getAttributes())); + $this->assertEquals([ + 'Invalid variation is mapped to flag (boolean_feature) and user (test_user) in the forced decision map.', + 'Audiences for experiment "test_experiment_2" collectively evaluated to TRUE.', + 'Assigned bucket 9075 to user "test_user" with bucketing ID "test_user".', + 'User "test_user" is in variation test_variation_2 of experiment test_experiment_2.', + "The user 'test_user' is bucketed into experiment 'test_experiment_2' of feature 'boolean_feature'." + ], $decision->getReasons()); + } + + public function testForcedDecisionConflicts() + { + $featureKey = "boolean_feature"; + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + $context = new OptimizelyDecisionContext($featureKey, null); + $decision = new OptimizelyForcedDecision("test_variation_1"); + $setForcedDecision1 = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision1); + + $context = new OptimizelyDecisionContext($featureKey, "test_experiment_2"); + $decision = new OptimizelyForcedDecision("test_variation_2"); + $setForcedDecision2 = $optUserContext->setForcedDecision($context, $decision); + $this->assertTrue($setForcedDecision2); + + // flag-to-decision is the 1st priority + + $decision = $optUserContext->decide('boolean_feature', [OptimizelyDecideOption::INCLUDE_REASONS]); + $this->assertEquals("test_variation_1", $decision->getVariationKey()); + $this->assertNull($decision->getRuleKey()); + $this->assertEquals([ + 'Decided by forced decision.', + 'Variation (test_variation_1) is mapped to flag (boolean_feature) and user (test_user) in the forced decision map.' + ], $decision->getReasons()); + } + + public function testGetForcedDecision() + { + $featureKey = "boolean_feature"; + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + $contextWithNullRuleKey = new OptimizelyDecisionContext($featureKey, null); + $decision = new OptimizelyForcedDecision("test_variation_1"); + $this->assertTrue($optUserContext->setForcedDecision($contextWithNullRuleKey, $decision)); + $this->assertEquals("test_variation_1", $optUserContext->getForcedDecision($contextWithNullRuleKey)); + + $context = new OptimizelyDecisionContext($featureKey, null); + $decision = new OptimizelyForcedDecision("test_variation_2"); + $this->assertTrue($optUserContext->setForcedDecision($context, $decision)); + $this->assertEquals("test_variation_2", $optUserContext->getForcedDecision($context)); + + $context = new OptimizelyDecisionContext($featureKey, "test_experiment_2"); + $decision = new OptimizelyForcedDecision("test_variation_1"); + $this->assertTrue($optUserContext->setForcedDecision($context, $decision)); + $this->assertEquals("test_variation_1", $optUserContext->getForcedDecision($context)); + + $context = new OptimizelyDecisionContext($featureKey, "test_experiment_2"); + $decision = new OptimizelyForcedDecision("test_variation_2"); + $this->assertTrue($optUserContext->setForcedDecision($context, $decision)); + $this->assertEquals("test_variation_2", $optUserContext->getForcedDecision($context)); + + $this->assertEquals("test_variation_2", $optUserContext->getForcedDecision($contextWithNullRuleKey)); + } + + public function testRemoveForcedDecision() + { + $featureKey = "boolean_feature"; + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + $contextWithFlag = new OptimizelyDecisionContext($featureKey, null); + $decisionForFlag = new OptimizelyForcedDecision("test_variation_1"); + $contextWithRule = new OptimizelyDecisionContext($featureKey, "test_experiment_2"); + $decisionForRule = new OptimizelyForcedDecision("test_variation_2"); + $this->assertTrue($optUserContext->setForcedDecision($contextWithFlag, $decisionForFlag)); + $this->assertTrue($optUserContext->setForcedDecision($contextWithRule, $decisionForRule)); + + $this->assertEquals("test_variation_1", $optUserContext->getForcedDecision($contextWithFlag)); + $this->assertEquals("test_variation_2", $optUserContext->getForcedDecision($contextWithRule)); + + $this->assertTrue($optUserContext->removeForcedDecision($contextWithFlag)); + $this->assertNull($optUserContext->getForcedDecision($contextWithFlag)); + $this->assertEquals("test_variation_2", $optUserContext->getForcedDecision($contextWithRule)); + + $this->assertTrue($optUserContext->removeForcedDecision($contextWithRule)); + $this->assertNull($optUserContext->getForcedDecision($contextWithRule)); + $this->assertNull($optUserContext->getForcedDecision($contextWithFlag)); + + $this->assertFalse($optUserContext->removeForcedDecision($contextWithFlag)); // no more saved decisions + } + + public function testRemoveAllForcedDecisions() + { + $featureKey = "boolean_feature"; + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + $validOptlyObject = new Optimizely($this->datafile); + + $optUserContext = new OptimizelyUserContext($validOptlyObject, $userId, $attributes); + $this->assertTrue($optUserContext->removeAllForcedDecisions()); // no saved decisions + + $contextWithFlag = new OptimizelyDecisionContext($featureKey, null); + $decisionForFlag = new OptimizelyForcedDecision("test_variation_1"); + $contextWithRule = new OptimizelyDecisionContext($featureKey, "test_experiment_2"); + $decisionForRule = new OptimizelyForcedDecision("test_variation_2"); + $this->assertTrue($optUserContext->setForcedDecision($contextWithFlag, $decisionForFlag)); + $this->assertTrue($optUserContext->setForcedDecision($contextWithRule, $decisionForRule)); + + $this->assertEquals("test_variation_1", $optUserContext->getForcedDecision($contextWithFlag)); + $this->assertEquals("test_variation_2", $optUserContext->getForcedDecision($contextWithRule)); + + $this->assertTrue($optUserContext->removeAllForcedDecisions()); + $this->assertNull($optUserContext->getForcedDecision($contextWithRule)); + $this->assertNull($optUserContext->getForcedDecision($contextWithFlag)); + + $this->assertTrue($optUserContext->removeAllForcedDecisions()); // no more saved decisions + } } diff --git a/tests/TestData.php b/tests/TestData.php index 10c9ed83..8eb4b09b 100644 --- a/tests/TestData.php +++ b/tests/TestData.php @@ -454,6 +454,21 @@ ], "version": "4", "audiences": [ + { + "id": "20803170009", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"def\"}]]]", + "name": "aud2" + }, + { + "id": "20787080332", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"abc\"}]]]", + "name": "aud1" + }, + { + "id": "20778250294", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"ga\"}]]]", + "name": "aud" + }, { "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"iPhone\"}]], [\"or\", [\"or\", {\"name\": \"location\", \"type\": \"custom_attribute\", \"value\": \"San Francisco\"}]]]", "id": "7718080042", @@ -758,6 +773,13 @@ } ] }, + { + "experimentIds": [], + "rolloutId": "rollout-5969-20778120250", + "variables": [], + "id": "5969", + "key": "same_variation_flag" + }, { "id": "155559", "key": "multi_variate_feature", @@ -899,6 +921,82 @@ } ] }, + { + "experiments": [{ + "status": "Running", + "audienceConditions": ["or", "20778250294"], + "audienceIds": ["20778250294"], + "variations": [{ + "variables": [], + "id": "16421", + "key": "new_variation", + "featureEnabled": true + }], + "forcedVariations": {}, + "key": "flag1_targeted_delivery", + "layerId": "9300000018514", + "trafficAllocation": [{ + "entityId": "16421", + "endOfRange": 10000 + }], + "id": "9300000018548" + }, { + "status": "Running", + "audienceConditions": ["or", "20803170009"], + "audienceIds": ["20803170009"], + "variations": [{ + "variables": [], + "id": "16421", + "key": "new_variation", + "featureEnabled": true + }], + "forcedVariations": {}, + "key": "flag1_targeted_delivery2", + "layerId": "9300000018515", + "trafficAllocation": [{ + "entityId": "16421", + "endOfRange": 10000 + }], + "id": "9300000018549" + }, { + "status": "Running", + "audienceConditions": ["or", "20787080332"], + "audienceIds": ["20787080332"], + "variations": [{ + "variables": [], + "id": "16421", + "key": "new_variation", + "featureEnabled": true + }], + "forcedVariations": {}, + "key": "flag1_targeted_delivery3", + "layerId": "9300000018516", + "trafficAllocation": [{ + "entityId": "16421", + "endOfRange": 10000 + }], + "id": "9300000018550" + }, { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [{ + "variables": [], + "id": "16419", + "key": "off", + "featureEnabled": false + }], + "forcedVariations": {}, + "key": "default-rollout-5969-20778120250", + "layerId": "default-layer-rollout-5969-20778120250", + "trafficAllocation": [{ + "entityId": "16419", + "endOfRange": 10000 + }], + "id": "default-rollout-5969-20778120250" + }], + "id": "rollout-5969-20778120250" + }, { "id": "166661", "experiments": [