From 1253af63fc57918d5096ab4d54763639d4d41277 Mon Sep 17 00:00:00 2001 From: Owais Date: Thu, 26 Oct 2017 18:17:39 +0500 Subject: [PATCH 01/20] Squashed commit of the following: commit 75224a4ed0b76bb41f4addb0a7a4640acfe617c9 Author: Owais Date: Thu Oct 26 18:14:31 2017 +0500 :memo: Final changes commit 75fa44d9e469754035add2bb98156b8596534e13 Author: Owais Date: Thu Oct 26 16:29:35 2017 +0500 # Conflicts: # tests/ProjectConfigTest.php # tests/UtilsTests/ValidatorTest.php commit a4430562410d0d3a2f117f4637305eca50d36ad6 Author: Owais Date: Thu Oct 26 12:21:16 2017 +0500 Partial fixes commit e03926b5680978c99113096a3cf25f16278227ff Author: Owais Date: Tue Oct 24 17:07:39 2017 +0500 Squashed commit of the following: commit 24569b893ab294accd470c89842d6cc70050e97e Author: Owais Date: Fri Oct 20 18:18:55 2017 +0500 :pen: Fixed Code Coverage commit a6fa1e6b99e540dc7815759bbc716fee39e4ef62 Author: Owais Date: Tue Oct 24 16:46:44 2017 +0500 :pen2: All Unit tests completed commit 72173d0a1b63de022648db354b4eeb0da710648a Author: Owais Date: Mon Oct 23 20:36:21 2017 +0500 :pen: 90% Unit tests done commit 9051dda5b6904bd11901bfc5d4405e7a0257a1b9 Author: Owais Date: Mon Oct 23 18:41:04 2017 +0500 :pen: Unit tests done for feature experiment commit 8a5722dddce8c701cb226e69dffce2cbced9b989 Author: Owais Date: Mon Oct 23 13:13:53 2017 +0500 :pen: Implementation First pass done commit 92dd219fbd1e5b0f0e7c73254951f34a3a335365 Author: Owais Date: Fri Oct 20 17:32:04 2017 +0500 :pen: Feature Flag bucketing to Experiment done :memo: Todo: Rollout Logic commit ad15a2c9a701359ab130d89283d7e3416edb0c74 Author: Owais Date: Fri Oct 20 12:52:47 2017 +0500 :pen: indentation commit dc41bf9f34549933d379292c7dcf1800b4f88634 Author: Owais Date: Thu Oct 19 17:36:46 2017 +0500 :pencil2: Added Logger to Validator :pencil2: Added method defs commit 547b7ad7a75fed3dde257d0827b535c59c226f9f Author: Owais Date: Thu Oct 19 17:11:22 2017 +0500 Feature Flags models and parsing from new v4 file --- .../DecisionService/DecisionService.php | 179 +++- src/Optimizely/Entity/FeatureFlag.php | 132 +++ src/Optimizely/Entity/FeatureVariable.php | 121 +++ src/Optimizely/Entity/Rollout.php | 68 ++ src/Optimizely/Entity/VariableUsage.php | 66 ++ src/Optimizely/Entity/Variation.php | 40 +- .../InvalidFeatureFlagException.php | 23 + .../Exceptions/InvalidRolloutException.php | 23 + src/Optimizely/ProjectConfig.php | 71 ++ src/Optimizely/Utils/ConfigParser.php | 16 +- src/Optimizely/Utils/Validator.php | 28 +- tests/BucketerTest.php | 21 +- .../DecisionServiceTest.php | 471 ++++++++++- tests/ProjectConfigTest.php | 333 +++----- tests/TestData.php | 764 ++++++++++++++++-- tests/UtilsTests/ValidatorTest.php | 21 +- 16 files changed, 2054 insertions(+), 323 deletions(-) create mode 100644 src/Optimizely/Entity/FeatureFlag.php create mode 100644 src/Optimizely/Entity/FeatureVariable.php create mode 100644 src/Optimizely/Entity/Rollout.php create mode 100644 src/Optimizely/Entity/VariableUsage.php create mode 100644 src/Optimizely/Exceptions/InvalidFeatureFlagException.php create mode 100644 src/Optimizely/Exceptions/InvalidRolloutException.php diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index a0925abc..780241eb 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -28,6 +28,8 @@ use Optimizely\UserProfile\UserProfile; use Optimizely\UserProfile\UserProfileUtils; use Optimizely\Utils\Validator; +use Optimizely\Entity\FeatureFlag; +use Optimizely\Entity\Rollout; // This value was decided between App Backend, Audience, and Oasis teams, but may possibly change. // We decided to prefix the reserved keyword with '$' because it is a symbol that is not @@ -93,14 +95,8 @@ public function __construct(LoggerInterface $logger, ProjectConfig $projectConfi */ public function getVariation(Experiment $experiment, $userId, $attributes = null) { - // by default, the bucketing ID should be the user ID - $bucketingId = $userId; - - // If the bucketing ID key is defined in attributes, then use that in place of the userID for the murmur hash key - if (!empty($attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) { - $bucketingId = $attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]; - $this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId)); - } + + $bucketingId = $this->getBucketingId($userId, $attributes); if (!$experiment->isExperimentRunning()) { $this->_logger->log(Logger::INFO, sprintf('Experiment "%s" is not running.', $experiment->getKey())); @@ -147,6 +143,173 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null return $variation; } + /** + * Gets the Bucketing Id for Bucketing + * @param string $userId + * @param array $userAttributes + * @return string + */ + private function getBucketingId($userId, $userAttributes){ + // by default, the bucketing ID should be the user ID + $bucketingId = $userId; + + // If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key + if (!empty($userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) { + $bucketingId = $userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]; + $this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId)); + } + + return $bucketingId; + } + + /** + * Get the variation the user is bucketed into for the given FeatureFlag + * @param FeatureFlag $featureFlag The feature flag the user wants to access + * @param string $userId user id + * @param array $userAttributes user attributes + * @return array/null {"experiment" : Experiment, "variation": Variation } / null + */ + public function getVariationForFeature(FeatureFlag $featureFlag, $userId, $userAttributes){ + //Evaluate in this order: + //1. Attempt to bucket user into all experiments in the feature flag. + //2. Attempt to bucket user into rollout in the feature flag. + + // Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments + $result = $this->getVariationForFeatureExperiment($featureFlag, $userId, $userAttributes); + if($result) + return $result; + + // Check if the feature flag has rollout and the user is bucketed into one of it's rules + $variation = $this->getVariationForFeatureRollout($featureFlag, $userId, $userAttributes); + if($variation){ + $this->_logger->log(Logger::INFO, + "User '{$userId}' is bucketed into a rollout for feature flag '{$featureFlag->getKey()}'." + ); + + return array( + "experiment" => null, + "variation" => $variation); + + } else{ + $this->_logger->log(Logger::INFO, + "User '{$userId}' is not bucketed into a rollout for feature flag '{$featureFlag->getKey()}'." + ); + + return null; + } + } + + /** + * Get the variation if the user is bucketed for one of the experiments on this feature flag + * @param FeatureFlag $featureFlag The feature flag the user wants to access + * @param string $userId user id + * @param array $userAttributes user userAttributes + * @return array/null {"experiment" : Experiment, "variation": Variation } / null + */ + public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $userId, $userAttributes){ + $feature_flag_key = $featureFlag->getKey(); + $experimentIds = $featureFlag->getExperimentIds(); + //Check if there are any experiment ids inside feature flag + if(empty($experimentIds)) + { + $this->_logger->log(Logger::DEBUG, + "The feature flag '{$feature_flag_key}' is not used in any experiments."); + return null; + } + + // Evaluate each experiment id and return the first bucketed experiment variation + foreach($experimentIds as $experiment_id){ + $experiment = $this->_projectConfig->getExperimentFromId($experiment_id); + if( $experiment == new Experiment()){ + // Error logged and exception thrown in ProjectConfig-getExperimentFromId + continue; + } + + $variation = $this->getVariation($experiment, $userId, $userAttributes); + if($variation instanceof Variation && $variation != new Variation){ + $this->_logger->log(Logger::INFO, + "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$feature_flag_key}'."); + return array( + "experiment"=> $experiment, + "variation" => $variation + ); + } + } + + $this->_logger->log(Logger::INFO, + "The user '{$userId}' is not bucketed into any of the experiments on the feature '{$feature_flag_key}'."); + + return null; + } + + /** + * Get the variation if the user is bucketed for one of the rollouts on this feature flag + * Evaluate the user for rules in priority order by seeing if the user satisfies the audience. + * Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation. + * @param FeatureFlag $featureFlag The feature flag the user wants to access + * @param string $userId user id + * @param array $userAttributes user userAttributes + * @return Variation/null + */ + public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId, $userAttributes){ + $bucketing_id = $this->getBucketingId($userId, $userAttributes); + $feature_flag_key = $featureFlag->getKey(); + $rollout_id = $featureFlag->getRolloutId(); + if(empty($rollout_id)){ + $this->_logger->log(Logger::DEBUG, + "Feature flag '{$feature_flag_key}' is not used in a rollout."); + return null; + } + $rollout = $this->_projectConfig->getRolloutFromId($rollout_id); + if($rollout == new Rollout()){ + // Error logged and thrown in getRolloutFromId + return null; + } + + $rolloutRules = $rollout->getExperiments(); + if(sizeof($rolloutRules) == 0) + return null; + + // Evaluate all rollout rules except for last one + for($i=0; $i_projectConfig, $experiment, $userAttributes)) { + $this->_logger->log( + Logger::DEBUG, + sprintf("User '%s' did not meet the audience conditions to be in rollout rule '%s'.", $userId, $experiment->getKey()) + ); + // Evaluate this user for the next rule + continue; + } + + $this->_logger->log(Logger::DEBUG, + sprintf("Attempting to bucket user '{$userId}' into rollout rule '%s'.", $experiment->getKey())); + + // Evaluate if user satisfies the traffic allocation for this rollout rule + $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); + if($variation && $variation != new Variation()){ + return $variation; + } else { + $this->_logger->log(Logger::DEBUG, + "User '{$userId}' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now."); + break; + } + } + + // Evaluate Everyone Else Rule / Last Rule now + $experiment = $rolloutRules[sizeof($rolloutRules)-1]; + $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); + if($variation && $variation != new Variation()){ + return $variation; + } else { + $this->_logger->log(Logger::DEBUG, + "User '{$userId}' was excluded from the 'Everyone Else' rule for feature flag"); + return null; + } + } + /** * Determine variation the user has been forced into. * diff --git a/src/Optimizely/Entity/FeatureFlag.php b/src/Optimizely/Entity/FeatureFlag.php new file mode 100644 index 00000000..9d9923ac --- /dev/null +++ b/src/Optimizely/Entity/FeatureFlag.php @@ -0,0 +1,132 @@ +_id = $id; + $this->_key = $key; + $this->_rolloutId = $rolloutId; + $this->_experimentIds = $experimentIds; + $this->_variables = ConfigParser::generateMap($variables, null, FeatureVariable::class); + } + + /** + * @return String feature flag ID + */ + public function getId(){ + return $this->_id; + } + + /** + * @param String $id feature flag ID + */ + public function setId($id){ + $this->_id = $id; + } + + /** + * @return String feature flag key + */ + public function getKey(){ + return $this->_key; + } + + /** + * @param String $key feature flag key + */ + public function setKey($key){ + $this->_key = $key; + } + + /** + * @return String attached rollout ID + */ + public function getRolloutId(){ + return $this->_rolloutId; + } + + /** + * @param String $rolloutId attached rollout ID + */ + public function setRolloutId($rolloutId){ + $this->_rolloutId = $rolloutId; + } + + /** + * @return [String] attached experiment IDs + */ + public function getExperimentIds(){ + return $this->_experimentIds; + } + + /** + * @param [String] $experimentIds attached experiment IDs + */ + public function setExperimentIds($experimentIds){ + $this->_experimentIds = $experimentIds; + } + + /** + * @return [FeatureVariable] feature variables that are part of this feature + */ + public function getVariables(){ + return $this->_variables; + } + + /** + * @param [FeatureVariable] $variables feature variables that are part of this feature + */ + public function setVariables($variables){ + $this->_variables = ConfigParser::generateMap($variables, null, FeatureVariable::class); + } +} diff --git a/src/Optimizely/Entity/FeatureVariable.php b/src/Optimizely/Entity/FeatureVariable.php new file mode 100644 index 00000000..45fdd488 --- /dev/null +++ b/src/Optimizely/Entity/FeatureVariable.php @@ -0,0 +1,121 @@ +_id = $id; + $this->_key = $key; + $this->_type = $type; + $this->_defaultValue = $defaultValue; + } + + /** + * @return String Feature variable ID + */ + public function getId(){ + return $this->_id; + } + + /** + * @param String $id Feature variable ID + */ + public function setId($id){ + $this->_id = $id; + } + + /** + * @return String Feature variable Key + */ + public function getKey(){ + return $this->_key; + } + + /** + * @param String $key Feature variable Key + */ + public function setKey($key){ + $this->_key = $key; + } + + /** + * @return String Feature variable primitive type + */ + public function getType(){ + return $this->_type; + } + + /** + * @param String $type Feature variable primitive type + */ + public function setType($type){ + $this->_type = $type; + } + + /** + * @return String Default value of the feature variable + */ + public function getDefaultValue(){ + return $this->_defaultValue; + } + + /** + * @param String $value Default value of the feature variable + */ + public function setDefaultValue($value){ + $this->_defaultValue = $value; + } +} diff --git a/src/Optimizely/Entity/Rollout.php b/src/Optimizely/Entity/Rollout.php new file mode 100644 index 00000000..8f59e9ff --- /dev/null +++ b/src/Optimizely/Entity/Rollout.php @@ -0,0 +1,68 @@ +_id = $id; + $this->_experiments = ConfigParser::generateMap($experiments, null, Experiment::class); + } + + /** + * @return String ID of the rollout + */ + public function getId(){ + return $this->_id; + } + + /** + * @param String $id ID of the rollout + */ + public function setId($id){ + $this->_id = $id; + } + + /** + * @return [Experiments] A list of experiments representing the different rules of the rollout + */ + public function getExperiments(){ + return $this->_experiments; + } + + /** + * @param [Experiments] $experiments A list of experiments representing the different rules of the rollout + */ + public function setExperiments($experiments){ + $this->_experiments = ConfigParser::generateMap($experiments, null, Experiment::class); + } +} \ No newline at end of file diff --git a/src/Optimizely/Entity/VariableUsage.php b/src/Optimizely/Entity/VariableUsage.php new file mode 100644 index 00000000..af5da677 --- /dev/null +++ b/src/Optimizely/Entity/VariableUsage.php @@ -0,0 +1,66 @@ +_id = $id; + $this->_value = $value; + } + + /** + * @return String ID of the live variable this usage is modifying + */ + public function getId(){ + return $this->_id; + } + + /** + * @param String $id ID of the live variable this usage is modifying + */ + public function setId($id){ + $this->_id = $id; + } + + /** + * @return String variable value for users in this particular variation + */ + public function getValue(){ + return $this->_value; + } + + /** + * @param String $value variable value for users in this particular variation + */ + public function setValue($value){ + $this->_value = $value; + } +} \ No newline at end of file diff --git a/src/Optimizely/Entity/Variation.php b/src/Optimizely/Entity/Variation.php index 03b25e13..14813520 100644 --- a/src/Optimizely/Entity/Variation.php +++ b/src/Optimizely/Entity/Variation.php @@ -1,6 +1,6 @@ associative array + */ + private $_variableIdToVariableUsageInstanceMap; + + + public function __construct($id = null, $key = null, $variableUsageInstances = []) { $this->_id = $id; $this->_key = $key; + + $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances,null,VariableUsage::class); + + if(!empty($this->_variableUsageInstances)){ + foreach(array_values($this->_variableUsageInstances) as $variableUsage){ + $_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; + } + } } /** @@ -68,4 +90,18 @@ public function setKey($key) { $this->_key = $key; } + + public function getVariables(){ + return $this->_variableUsageInstances; + } + + public function setVariables($variableUsageInstances){ + $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances,null,VariableUsage::class); + + if(!empty($this->_variableUsageInstances)){ + foreach(array_values($this->_variableUsageInstances) as $variableUsage){ + $_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; + } + } + } } diff --git a/src/Optimizely/Exceptions/InvalidFeatureFlagException.php b/src/Optimizely/Exceptions/InvalidFeatureFlagException.php new file mode 100644 index 00000000..d3a9ddd3 --- /dev/null +++ b/src/Optimizely/Exceptions/InvalidFeatureFlagException.php @@ -0,0 +1,23 @@ + associative array of feature keys to feature flags + */ + private $_featureKeyMap; + + /** + * internal mapping of rollout IDs to Rollout models. + * @var associative array of rollout ids to rollouts + */ + private $_rolloutIdMap; + /** * ProjectConfig constructor to load and set project configuration data. * @@ -150,12 +178,16 @@ public function __construct($datafile, $logger, $errorHandler) $events = $config['events'] ?: []; $attributes = $config['attributes'] ?: []; $audiences = $config['audiences'] ?: []; + $rollouts = isset($config['rollouts']) ? $config['rollouts'] : []; + $featureFlags = isset($config['featureFlags']) ? $config['featureFlags']: []; $this->_groupIdMap = ConfigParser::generateMap($groups, 'id', Group::class); $this->_experimentKeyMap = ConfigParser::generateMap($experiments, 'key', Experiment::class); $this->_eventKeyMap = ConfigParser::generateMap($events, 'key', Event::class); $this->_attributeKeyMap = ConfigParser::generateMap($attributes, 'key', Attribute::class); $this->_audienceIdMap = ConfigParser::generateMap($audiences, 'id', Audience::class); + $this->_rollouts = ConfigParser::generateMap($rollouts, null, Rollout::class); + $this->_featureFlags = ConfigParser::generateMap($featureFlags, null, FeatureFlag::class); forEach(array_values($this->_groupIdMap) as $group) { $experimentsInGroup = ConfigParser::generateMap($group->getExperiments(), 'key', Experiment::class); @@ -182,6 +214,14 @@ public function __construct($datafile, $logger, $errorHandler) $conditionDecoder->deserializeAudienceConditions($audience->getConditions()); $audience->setConditionsList($conditionDecoder->getConditionsList()); } + + foreach(array_values($this->_rollouts) as $rollout){ + $this->_rolloutIdMap[$rollout->getId()] = $rollout; + } + + foreach(array_values($this->_featureFlags) as $featureFlag){ + $this->_featureKeyMap[$featureFlag->getKey()] = $featureFlag; + } } /** @@ -268,6 +308,37 @@ public function getExperimentFromId($experimentId) return new Experiment(); } + /** + * @param String $featureKey + * @return FeatureFlag + */ + public function getFeatureFlagFromKey($featureKey) + { + if(isset($this->_featureKeyMap[$featureKey])){ + return $this->_featureKeyMap[$featureKey]; + } + + $this->_logger->log(Logger::ERROR, sprintf('FeatureFlag Key "%s" is not in datafile.', $featureKey)); + $this->_errorHandler->handleError(new InvalidFeatureFlagException('Provided feature flag is not in datafile.')); + return new FeatureFlag(); + } + + /** + * @param String $rolloutId + * @return Rollout + * + */ + public function getRolloutFromId($rolloutId) + { + if (isset($this->_rolloutIdMap[$rolloutId])) { + return $this->_rolloutIdMap[$rolloutId]; + } + + $this->_logger->log(Logger::ERROR, sprintf('Rollout with ID "%s" is not in the datafile.', $rolloutId)); + $this->_errorHandler->handleError(new InvalidRolloutException('Provided rollout is not in datafile.')); + return new Rollout(); + } + /** * @param $eventKey string Key of the event. * diff --git a/src/Optimizely/Utils/ConfigParser.php b/src/Optimizely/Utils/ConfigParser.php index ec150d8d..baa96e6d 100644 --- a/src/Optimizely/Utils/ConfigParser.php +++ b/src/Optimizely/Utils/ConfigParser.php @@ -32,12 +32,16 @@ public static function generateMap($entities, $entityId, $entityClass) $entityMap = []; forEach ($entities as $entity) { - $entityObject = new $entityClass; - forEach ($entity as $key => $value) - { - $propSetter = 'set'.ucfirst($key); - if (method_exists($entityObject, $propSetter)) { - $entityObject->$propSetter($value); + if($entity instanceof $entityClass){ + $entityObject = $entity; + } else { + $entityObject = new $entityClass; + forEach ($entity as $key => $value) + { + $propSetter = 'set'.ucfirst($key); + if (method_exists($entityObject, $propSetter)) { + $entityObject->$propSetter($value); + } } } diff --git a/src/Optimizely/Utils/Validator.php b/src/Optimizely/Utils/Validator.php index ea7bf6a0..f86a69e8 100644 --- a/src/Optimizely/Utils/Validator.php +++ b/src/Optimizely/Utils/Validator.php @@ -19,6 +19,8 @@ use JsonSchema; use Optimizely\Entity\Experiment; use Optimizely\ProjectConfig; +use Optimizely\Logger\LoggerInterface; +use Monolog\Logger; class Validator @@ -28,15 +30,27 @@ class Validator * * @return boolean Representing whether schema is valid or not. */ - public static function validateJsonSchema($datafile) + public static function validateJsonSchema($datafile, LoggerInterface $logger = null) { - $jsonSchemaObject = json_decode(file_get_contents(__DIR__.'/schema.json')); - $datafileJson = json_decode($datafile); + $data = json_decode($datafile); - $jsonValidator = new JsonSchema\Validator; - $jsonValidator->check($datafileJson, $jsonSchemaObject); - return $jsonValidator->isValid() ? true : false; - } + // Validate + $validator = new JsonSchema\Validator; + $validator->check($data, (object)['$ref' => 'file://' . __DIR__.'/schema.json']); + + if ($validator->isValid()) { + return true; + } else { + if($logger){ + $logger->log(Logger::DEBUG,"JSON does not validate. Violations:\n");; + foreach ($validator->getErrors() as $error) { + $logger->log(Logger::DEBUG,"[%s] %s\n", $error['property'], $error['message']); + } + } + + return false; + } + } /** * @param $attributes mixed Attributes of the user. diff --git a/tests/BucketerTest.php b/tests/BucketerTest.php index 31b80d48..93f84b6f 100644 --- a/tests/BucketerTest.php +++ b/tests/BucketerTest.php @@ -197,7 +197,12 @@ public function testBucketValidExperimentInGroup() 'User "testUserId" is in variation group_exp_1_var_1 of experiment group_experiment_1.'); $this->assertEquals( - new Variation('7722260071', 'group_exp_1_var_1'), + new Variation('7722260071', 'group_exp_1_var_1',[ + [ + "id" => "155563", + "value" => "groupie_1_v1" + ] + ]), $bucketer->bucket( $this->config, $this->config->getExperimentFromKey('group_experiment_1'), @@ -223,7 +228,12 @@ public function testBucketValidExperimentInGroup() 'User "testUserId" is in variation group_exp_1_var_2 of experiment group_experiment_1.'); $this->assertEquals( - new Variation('7722360022', 'group_exp_1_var_2'), + new Variation('7722360022', 'group_exp_1_var_2',[ + [ + "id" => "155563", + "value" => "groupie_1_v2" + ] + ]), $bucketer->bucket( $this->config, $this->config->getExperimentFromKey('group_experiment_1'), @@ -344,7 +354,12 @@ public function testBucketVariationGroupedExperimentsWithBucketingId() $bucketer = new Bucketer($this->loggerMock); $this->assertEquals( - new Variation('7725250007', 'group_exp_2_var_2'), + new Variation('7725250007', 'group_exp_2_var_2',[ + [ + "id" => "155563", + "value" => "groupie_2_v1" + ] + ]), $bucketer->bucket( $this->config, $this->config->getExperimentFromKey('group_experiment_2'), diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 0c650530..337b0ffb 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -23,17 +23,19 @@ use Optimizely\Entity\Experiment; use Optimizely\Entity\Variation; use Optimizely\ErrorHandler\NoOpErrorHandler; +use Optimizely\Logger\DefaultLogger; use Optimizely\Logger\NoOpLogger; use Optimizely\Optimizely; use Optimizely\ProjectConfig; use Optimizely\UserProfile\UserProfileServiceInterface; - +use Optimizely\Utils\Validator; class DecisionServiceTest extends \PHPUnit_Framework_TestCase { private $bucketerMock; private $config; private $decisionService; + private $decisionServiceMock; private $loggerMock; private $testUserId; private $userProvideServiceMock; @@ -57,6 +59,7 @@ public function setUp() $this->loggerMock = $this->getMockBuilder(NoOpLogger::class) ->setMethods(array('log')) ->getMock(); + $this->config = new ProjectConfig(DATAFILE, $this->loggerMock, new NoOpErrorHandler()); // Mock bucketer @@ -68,6 +71,13 @@ public function setUp() // Mock user profile service implementation $this->userProvideServiceMock = $this->getMockBuilder(UserProfileServiceInterface::class) ->getMock(); + + $this->decisionService = new DecisionService($this->loggerMock, $this->config); + + $this->decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->config)) + ->setMethods(array('getVariation')) + ->getMock(); } public function testGetVariationReturnsNullWhenExperimentIsNotRunning() @@ -76,8 +86,7 @@ public function testGetVariationReturnsNullWhenExperimentIsNotRunning() ->method('bucket'); $pausedExperiment = $this->config->getExperimentFromKey('paused_experiment'); - - $this->decisionService = new DecisionService($this->loggerMock, $this->config); + $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -96,7 +105,6 @@ public function testGetVariationBucketsUserWhenExperimentIsRunning() $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -124,7 +132,6 @@ public function testGetVariationReturnsWhitelistedVariation() ->method('log') ->with(Logger::INFO, 'User "user1" is forced in variation "control" of experiment "test_experiment".'); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -139,7 +146,12 @@ public function testGetVariationReturnsWhitelistedVariation() public function testGetVariationReturnsWhitelistedVariationForGroupedExperiment() { - $expectedVariation = new Variation('7722260071', 'group_exp_1_var_1'); + $expectedVariation = new Variation('7722260071', 'group_exp_1_var_1',[ + [ + "id" => "155563", + "value" => "groupie_1_v1" + ] + ]); $runningExperiment = $this->config->getExperimentFromKey('group_experiment_1'); $callIndex = 0; @@ -152,7 +164,6 @@ public function testGetVariationReturnsWhitelistedVariationForGroupedExperiment( ->method('log') ->with(Logger::INFO, 'User "user1" is forced in variation "group_exp_1_var_1" of experiment "group_experiment_1".'); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -179,7 +190,6 @@ public function testGetVariationBucketsWhenForcedVariationsIsEmpty() $experiment->setAccessible(true); $experiment->setValue($runningExperiment, array()); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -208,7 +218,6 @@ public function testGetVariationBucketsWhenWhitelistedVariationIsInvalid() 'user_1' => 'invalid' ]); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -230,7 +239,6 @@ public function testGetVariationBucketsUserWhenUserIsNotWhitelisted() $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -250,7 +258,6 @@ public function testGetVariationReturnsNullIfUserDoesNotMeetAudienceConditions() $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); @@ -598,6 +605,448 @@ public function testGetVariationWithBucketingId() $variationKey = $optlyObject->getVariation($this->experimentKey, $userId, $userAttributesWithBucketingId); $this->assertEquals($this->variationKeyControl, $variationKey, sprintf('Variation "%s" does not match expected user profile variation "%s".', $variationKey, $this->variationKeyControl)); + } + + //should return nil and log a message when the feature flag's experiment ids array is empty + public function testGetVariationForFeatureExperimentGivenNullExperimentIds(){ + + $feature_flag = $this->config->getFeatureFlagFromKey('empty_feature'); + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, "The feature flag 'empty_feature' is not used in any experiments."); + + $this->assertSame( + $this->decisionService->getVariationForFeatureExperiment($feature_flag,'user1',[]), + null + ); + } + + //should return nil and log a message when the experiment is not in the datafile + public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile(){ + + $boolean_feature = $this->config->getFeatureFlagFromKey('boolean_feature'); + $feature_flag = clone $boolean_feature; + // Use any string that is not an experiment id in the data file + $feature_flag->setExperimentIds(["29039203"]); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, 'Experiment ID "29039203" is not in datafile.'); + + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::INFO, + "The user 'user1' is not bucketed into any of the experiments on the feature 'boolean_feature'."); + + $this->assertSame( + $this->decisionService->getVariationForFeatureExperiment($feature_flag,'user1',[]), + null + ); + } + + //should return nil and log when the user is not bucketed into the feature flag's experiments + public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBucketed(){ + $multivariate_experiment = $this->config->getExperimentFromKey('test_experiment_multivariate'); + $map = [ [$multivariate_experiment, 'user1', [], null] ]; + + //make sure the user is not bucketed into the feature experiment + $this->decisionServiceMock->expects($this->at(0)) + ->method('getVariation') + ->will($this->returnValueMap($map)); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + "The user 'user1' is not bucketed into any of the experiments on the feature 'multi_variate_feature'."); + $feature_flag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); + $this->assertSame( + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []), + null); + } + + // should return the variation when the user is bucketed into a variation for the experiment on the feature flag + public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucketed(){ + // return the first variation of the `test_experiment_multivariate` experiment, which is attached to the `multi_variate_feature` + $variation = $this->config->getVariationFromId('test_experiment_multivariate','122231'); + $this->decisionServiceMock->expects($this->at(0)) + ->method('getVariation') + ->will($this->returnValue($variation)); + + $feature_flag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); + $expected_decision = [ + 'experiment' => $this->config->getExperimentFromKey('test_experiment_multivariate'), + 'variation' => $this->config->getVariationFromId('test_experiment_multivariate','122231') + ]; + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + "The user 'user1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'."); + + $this->assertEquals( + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []), + $expected_decision + ); + } + + // should return the variation the user is bucketed into when the user is bucketed into one of the experiments + public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed(){ + $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); + $variation = $mutex_exp->getVariations()[0]; + $this->decisionServiceMock->expects($this->at(0)) + ->method('getVariation') + ->will($this->returnValue($variation)); + + $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); + $variation = $mutex_exp->getVariations()[0]; + $expected_decision = [ + 'experiment' => $mutex_exp, + 'variation' => $variation + ]; + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + "The user 'user_1' is bucketed into experiment 'group_experiment_1' of feature 'boolean_feature'."); + $this->assertEquals( + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []), + $expected_decision + ); + } + + // should return nil and log a message when the user is not bucketed into any of the mutex experiments + public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed(){ + $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); + $variation = $mutex_exp->getVariations()[0]; + $this->decisionServiceMock->expects($this->at(0)) + ->method('getVariation') + ->will($this->returnValue(null)); + + + $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + "The user 'user_1' is not bucketed into any of the experiments on the feature 'boolean_feature'."); + $this->assertEquals( + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []), + null + ); + } + + //should return the bucketed experiment and variation + public function testGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment(){ + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->config)) + ->setMethods(array('getVariationForFeatureExperiment')) + ->getMock(); + + $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); + $expected_experiment_id = $feature_flag->getExperimentIds()[0]; + $expected_experiment = $this->config->getExperimentFromId($expected_experiment_id); + $expected_variation = $expected_experiment->getVariations()[0]; + $expected_decision = [ + 'experiment' => $expected_experiment, + 'variation' => $expected_variation + ]; + + $decisionServiceMock->expects($this->at(0)) + ->method('getVariationForFeatureExperiment') + ->will($this->returnValue($expected_decision)); + + $this->assertEquals( + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), + $expected_decision + ); + } + + // should return the bucketed variation and null experiment + public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->config)) + ->setMethods(array('getVariationForFeatureExperiment','getVariationForFeatureRollout')) + ->getMock(); + + $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); + $rollout_id = $feature_flag->getRolloutId(); + $rollout = $this->config->getRolloutFromId($rollout_id); + $experiment = $rollout->getExperiments()[0]; + $expected_variation = $experiment->getVariations()[0]; + $expected_decision = [ + 'experiment' => null, + 'variation' => $expected_variation + ]; + + $decisionServiceMock + ->method('getVariationForFeatureExperiment') + ->will($this->returnValue(null)); + + $decisionServiceMock + ->method('getVariationForFeatureRollout') + ->will($this->returnValue($expected_variation)); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + "User 'user_1' is bucketed into a rollout for feature flag 'string_single_variable_feature'."); + $this->assertEquals( + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), + $expected_decision + ); + } + + // should return null + public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExperimentNorToFeatureRollout(){ + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->config)) + ->setMethods(array('getVariationForFeatureExperiment','getVariationForFeatureRollout')) + ->getMock(); + + $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); + + $decisionServiceMock + ->method('getVariationForFeatureExperiment') + ->will($this->returnValue(null)); + + $decisionServiceMock + ->method('getVariationForFeatureRollout') + ->will($this->returnValue(null)); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + "User 'user_1' is not bucketed into a rollout for feature flag 'string_single_variable_feature'."); + + $this->assertEquals( + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), + null + ); + } + + // should return null + public function testGetVariationForFeatureRolloutWhenNoRolloutIsAssociatedToFeatureFlag(){ + // No rollout id is associated to boolean_feature + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, + "Feature flag 'boolean_feature' is not used in a rollout."); + + $this->assertEquals( + $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []), + null + ); + } + + // should return null + public function testGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile(){ + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + $feature_flag = clone $feature_flag; + // Set any string which is not a rollout id in the data file + $feature_flag->setRolloutId('invalid_rollout_id'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, + 'Rollout with ID "invalid_rollout_id" is not in the datafile.'); + + $this->assertEquals( + $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []), + null + ); + } + + // should return null + public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperiment(){ + // Mock Project Config + $configMock = $this->getMockBuilder(ProjectConfig::class) + ->setConstructorArgs(array(DATAFILE, $this->loggerMock, new NoOpErrorHandler())) + ->setMethods(array('getRolloutFromId')) + ->getMock(); + + $this->decisionService = new DecisionService($this->loggerMock, $configMock); + + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); + $rollout_id = $feature_flag->getRolloutId(); + $rollout = $this->config->getRolloutFromId($rollout_id); + $experiment_less_rollout = clone $rollout; + $experiment_less_rollout->setExperiments([]); + + $configMock + ->method('getRolloutFromId') + ->will($this->returnValue($experiment_less_rollout)); + + $this->assertEquals( + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', []), + null + ); + } + + // ============== when the user qualifies for targeting rule (audience match) ====================== + + // should return the variation the user is bucketed into when the user is bucketed into the targeting rule + public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRule(){ + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); + $rollout_id = $feature_flag->getRolloutId(); + $rollout = $this->config->getRolloutFromId($rollout_id); + $experiment = $rollout->getExperiments()[0]; + $expected_variation = $experiment->getVariations()[0]; + // Provide attributes such that user qualifies for audience + $user_attributes = ["browser_type" => "chrome"]; + + $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); + $bucketer->setAccessible(true); + $bucketer->setValue($this->decisionService, $this->bucketerMock); + + $this->bucketerMock + ->method('bucket') + ->willReturn($expected_variation); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment->getKey()}'."); + + $this->assertEquals( + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), + $expected_variation + ); + } + + // should return the variation the user is bucketed into when the user is bucketed into the "Everyone Else" rule' + // and the user is not bucketed into the targeting rule + public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargetingRuleButBucketedToEveryoneElseRule(){ + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); + $rollout_id = $feature_flag->getRolloutId(); + $rollout = $this->config->getRolloutFromId($rollout_id); + $experiment0 = $rollout->getExperiments()[0]; + // Everyone Else Rule + $experiment2 = $rollout->getExperiments()[2]; + $expected_variation = $experiment2->getVariations()[0]; + + // Provide attributes such that user qualifies for audience + $user_attributes = ["browser_type" => "chrome"]; + $this->decisionService = new DecisionService($this->loggerMock, $this->config); + $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); + $bucketer->setAccessible(true); + $bucketer->setValue($this->decisionService, $this->bucketerMock); + // Make bucket return null when called for first targeting rule + $this->bucketerMock->expects($this->at(0)) + ->method('bucket') + ->willReturn(null); + //Make bucket return expected variation when called second time for everyone else + $this->bucketerMock->expects($this->at(1)) + ->method('bucket') + ->willReturn($expected_variation); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'."); + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::DEBUG, + "User 'user_1' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now."); + + + $this->assertEquals( + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), + $expected_variation + ); + } + + // should log and return nil when the user is not bucketed into the targeting rule and + // the user is not bucketed into the "Everyone Else" rule' + public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTargetingRuleNorToEveryoneElseRule(){ + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); + $rollout_id = $feature_flag->getRolloutId(); + $rollout = $this->config->getRolloutFromId($rollout_id); + $experiment0 = $rollout->getExperiments()[0]; + // Everyone Else Rule + $experiment2 = $rollout->getExperiments()[2]; + + // Provide attributes such that user qualifies for audience + $user_attributes = ["browser_type" => "chrome"]; + $this->decisionService = new DecisionService($this->loggerMock, $this->config); + $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); + $bucketer->setAccessible(true); + $bucketer->setValue($this->decisionService, $this->bucketerMock); + // Make bucket return null when called for first targeting rule + $this->bucketerMock->expects($this->at(0)) + ->method('bucket') + ->willReturn(null); + //Make bucket return null when called second time for everyone else + $this->bucketerMock->expects($this->at(1)) + ->method('bucket') + ->willReturn(null); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'."); + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::DEBUG, + "User 'user_1' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now."); + $this->loggerMock->expects($this->at(2)) + ->method('log') + ->with(Logger::DEBUG, + "User 'user_1' was excluded from the 'Everyone Else' rule for feature flag"); + + $this->assertEquals( + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), + null + ); + } + + // ============== END of tests - when the user qualifies for targeting rule (audience match) ====================== + + // ===== - when the user does not qualify for the tageting rules (audience mismatch) ====== + + // should return expected variation when the user is attempted to be bucketed into all targeting rules + // including Everyone Else rule + public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargetingRule(){ + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); + $rollout_id = $feature_flag->getRolloutId(); + $rollout = $this->config->getRolloutFromId($rollout_id); + $experiment0 = $rollout->getExperiments()[0]; + $experiment1 = $rollout->getExperiments()[1]; + // Everyone Else Rule + $experiment2 = $rollout->getExperiments()[2]; + $expected_variation = $experiment2->getVariations()[0]; + + // Provide null attributes so that user does not qualify for audience + $user_attributes = []; + $this->decisionService = new DecisionService($this->loggerMock, $this->config); + $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); + $bucketer->setAccessible(true); + $bucketer->setValue($this->decisionService, $this->bucketerMock); + + // Expect bucket to be called exactly once for the everyone else/last rule. + // As we ignore Audience check only for thelast rule + $this->bucketerMock->expects($this->exactly(1)) + ->method('bucket') + ->willReturn($expected_variation); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, + "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment0->getKey()}'."); + + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::DEBUG, + "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment1->getKey()}'."); + + $this->assertEquals( + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), + $expected_variation + ); } } + diff --git a/tests/ProjectConfigTest.php b/tests/ProjectConfigTest.php index 2084a716..4d6e130a 100644 --- a/tests/ProjectConfigTest.php +++ b/tests/ProjectConfigTest.php @@ -23,13 +23,18 @@ use Optimizely\Entity\Audience; use Optimizely\Entity\Event; use Optimizely\Entity\Experiment; +use Optimizely\Entity\FeatureFlag; use Optimizely\Entity\Group; +use Optimizely\Entity\Rollout; use Optimizely\Entity\Variation; +use Optimizely\Entity\VariableUsage; use Optimizely\ErrorHandler\NoOpErrorHandler; use Optimizely\Exceptions\InvalidAttributeException; use Optimizely\Exceptions\InvalidAudienceException; use Optimizely\Exceptions\InvalidEventException; use Optimizely\Exceptions\InvalidExperimentException; +use Optimizely\Exceptions\InvalidFeatureFlagException; +use Optimizely\Exceptions\InvalidRolloutException; use Optimizely\Exceptions\InvalidGroupException; use Optimizely\Exceptions\InvalidVariationException; use Optimizely\Logger\DefaultLogger; @@ -62,7 +67,7 @@ public function testInit() // Check version $version = new \ReflectionProperty(ProjectConfig::class, '_version'); $version->setAccessible(true); - $this->assertEquals('2', $version->getValue($this->config)); + $this->assertEquals('4', $version->getValue($this->config)); // Check account ID $accountId = new \ReflectionProperty(ProjectConfig::class, '_accountId'); @@ -93,7 +98,11 @@ public function testInit() 'test_experiment' => $this->config->getExperimentFromKey('test_experiment'), 'paused_experiment' => $this->config->getExperimentFromKey('paused_experiment'), 'group_experiment_1' => $this->config->getExperimentFromKey('group_experiment_1'), - 'group_experiment_2' => $this->config->getExperimentFromKey('group_experiment_2') + 'group_experiment_2' => $this->config->getExperimentFromKey('group_experiment_2'), + 'test_experiment_multivariate' => $this->config->getExperimentFromKey('test_experiment_multivariate'), + 'test_experiment_with_feature_rollout' => $this->config->getExperimentFromKey('test_experiment_with_feature_rollout'), + 'test_experiment_double_feature' => $this->config->getExperimentFromKey('test_experiment_double_feature'), + 'test_experiment_integer_feature' => $this->config->getExperimentFromKey('test_experiment_integer_feature') ], $experimentKeyMap->getValue($this->config)); // Check experiment ID map @@ -103,7 +112,11 @@ public function testInit() '7716830082' => $this->config->getExperimentFromId('7716830082'), '7723330021' => $this->config->getExperimentFromId('7723330021'), '7718750065' => $this->config->getExperimentFromId('7718750065'), - '7716830585' => $this->config->getExperimentFromId('7716830585') + '7716830585' => $this->config->getExperimentFromId('7716830585'), + '122230' => $this->config->getExperimentFromId('122230'), + '122235' => $this->config->getExperimentFromId('122235'), + '122238' => $this->config->getExperimentFromId('122238'), + '122241' => $this->config->getExperimentFromId('122241') ], $experimentIdMap->getValue($this->config)); // Check event key map @@ -125,7 +138,8 @@ public function testInit() $audienceIdMap = new \ReflectionProperty(ProjectConfig::class, '_audienceIdMap'); $audienceIdMap->setAccessible(true); $this->assertEquals([ - '7718080042' => $this->config->getAudience('7718080042') + '7718080042' => $this->config->getAudience('7718080042'), + '11154' => $this->config->getAudience('11154') ], $audienceIdMap->getValue($this->config)); // Check variation key map @@ -147,125 +161,24 @@ public function testInit() 'group_experiment_2' => [ 'group_exp_2_var_1' => $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_1'), 'group_exp_2_var_2' => $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_2') - ] - ], $variationKeyMap->getValue($this->config)); - - // Check variation ID map - $variationIdMap = new \ReflectionProperty(ProjectConfig::class, '_variationIdMap'); - $variationIdMap->setAccessible(true); - $this->assertEquals([ - 'test_experiment' => [ - '7722370027' => $this->config->getVariationFromId('test_experiment', '7722370027'), - '7721010009' => $this->config->getVariationFromId('test_experiment', '7721010009') ], - 'paused_experiment' => [ - '7722370427' => $this->config->getVariationFromId('paused_experiment', '7722370427'), - '7721010509' => $this->config->getVariationFromId('paused_experiment', '7721010509') + 'test_experiment_multivariate' => [ + 'Fred' => $this->config->getVariationFromKey('test_experiment_multivariate', 'Fred'), + 'Feorge' => $this->config->getVariationFromKey('test_experiment_multivariate', 'Feorge'), + 'Gred' => $this->config->getVariationFromKey('test_experiment_multivariate', 'Gred'), + 'George' => $this->config->getVariationFromKey('test_experiment_multivariate', 'George') ], - 'group_experiment_1' => [ - '7722260071' => $this->config->getVariationFromId('group_experiment_1', '7722260071'), - '7722360022' => $this->config->getVariationFromId('group_experiment_1', '7722360022') + 'test_experiment_with_feature_rollout' => [ + 'control' => $this->config->getVariationFromKey('test_experiment_with_feature_rollout', 'control'), + 'variation' => $this->config->getVariationFromKey('test_experiment_with_feature_rollout', 'variation') ], - 'group_experiment_2' => [ - '7713030086' => $this->config->getVariationFromId('group_experiment_2', '7713030086'), - '7725250007' => $this->config->getVariationFromId('group_experiment_2', '7725250007') - ] - ], $variationIdMap->getValue($this->config)); - } - - public function testInitWithDatafileV3() - { - // Init with v3 datafile - $this->config = new ProjectConfig(DATAFILE_V3, $this->loggerMock, $this->errorHandlerMock); - - // Check version - $version = new \ReflectionProperty(ProjectConfig::class, '_version'); - $version->setAccessible(true); - $this->assertEquals('2', $version->getValue($this->config)); - - // Check account ID - $accountId = new \ReflectionProperty(ProjectConfig::class, '_accountId'); - $accountId->setAccessible(true); - $this->assertEquals('1592310167', $accountId->getValue($this->config)); - - // Check project ID - $projectId = new \ReflectionProperty(ProjectConfig::class, '_projectId'); - $projectId->setAccessible(true); - $this->assertEquals('7720880029', $projectId->getValue($this->config)); - - // Check revision - $revision = new \ReflectionProperty(ProjectConfig::class, '_revision'); - $revision->setAccessible(true); - $this->assertEquals('15', $revision->getValue($this->config)); - - // Check group ID map - $groupIdMap = new \ReflectionProperty(ProjectConfig::class, '_groupIdMap'); - $groupIdMap->setAccessible(true); - $this->assertEquals([ - '7722400015' => $this->config->getGroup('7722400015') - ], $groupIdMap->getValue($this->config)); - - // Check experiment key map - $experimentKeyMap = new \ReflectionProperty(ProjectConfig::class, '_experimentKeyMap'); - $experimentKeyMap->setAccessible(true); - $this->assertEquals([ - 'test_experiment' => $this->config->getExperimentFromKey('test_experiment'), - 'paused_experiment' => $this->config->getExperimentFromKey('paused_experiment'), - 'group_experiment_1' => $this->config->getExperimentFromKey('group_experiment_1'), - 'group_experiment_2' => $this->config->getExperimentFromKey('group_experiment_2') - ], $experimentKeyMap->getValue($this->config)); - - // Check experiment ID map - $experimentIdMap = new \ReflectionProperty(ProjectConfig::class, '_experimentIdMap'); - $experimentIdMap->setAccessible(true); - $this->assertEquals([ - '7716830082' => $this->config->getExperimentFromId('7716830082'), - '7723330021' => $this->config->getExperimentFromId('7723330021'), - '7718750065' => $this->config->getExperimentFromId('7718750065'), - '7716830585' => $this->config->getExperimentFromId('7716830585') - ], $experimentIdMap->getValue($this->config)); - - // Check event key map - $eventKeyMap = new \ReflectionProperty(ProjectConfig::class, '_eventKeyMap'); - $eventKeyMap->setAccessible(true); - $this->assertEquals([ - 'purchase' => $this->config->getEvent('purchase') - ], $eventKeyMap->getValue($this->config)); - - // Check attribute key map - $attributeKeyMap = new \ReflectionProperty(ProjectConfig::class, '_attributeKeyMap'); - $attributeKeyMap->setAccessible(true); - $this->assertEquals([ - 'device_type' => $this->config->getAttribute('device_type'), - 'location' => $this->config->getAttribute('location') - ], $attributeKeyMap->getValue($this->config)); - - // Check audience ID map - $audienceIdMap = new \ReflectionProperty(ProjectConfig::class, '_audienceIdMap'); - $audienceIdMap->setAccessible(true); - $this->assertEquals([ - '7718080042' => $this->config->getAudience('7718080042') - ], $audienceIdMap->getValue($this->config)); - - // Check variation key map - $variationKeyMap = new \ReflectionProperty(ProjectConfig::class, '_variationKeyMap'); - $variationKeyMap->setAccessible(true); - $this->assertEquals([ - 'test_experiment' => [ - 'control' => $this->config->getVariationFromKey('test_experiment', 'control'), - 'variation' => $this->config->getVariationFromKey('test_experiment', 'variation') + 'test_experiment_double_feature' => [ + 'control' => $this->config->getVariationFromKey('test_experiment_double_feature', 'control'), + 'variation' => $this->config->getVariationFromKey('test_experiment_double_feature', 'variation') ], - 'paused_experiment' => [ - 'control' => $this->config->getVariationFromKey('paused_experiment', 'control'), - 'variation' => $this->config->getVariationFromKey('paused_experiment', 'variation') - ], - 'group_experiment_1' => [ - 'group_exp_1_var_1' => $this->config->getVariationFromKey('group_experiment_1', 'group_exp_1_var_1'), - 'group_exp_1_var_2' => $this->config->getVariationFromKey('group_experiment_1', 'group_exp_1_var_2') - ], - 'group_experiment_2' => [ - 'group_exp_2_var_1' => $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_1'), - 'group_exp_2_var_2' => $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_2') + 'test_experiment_integer_feature' => [ + 'control' => $this->config->getVariationFromKey('test_experiment_integer_feature', 'control'), + 'variation' => $this->config->getVariationFromKey('test_experiment_integer_feature', 'variation') ] ], $variationKeyMap->getValue($this->config)); @@ -288,127 +201,61 @@ public function testInitWithDatafileV3() 'group_experiment_2' => [ '7713030086' => $this->config->getVariationFromId('group_experiment_2', '7713030086'), '7725250007' => $this->config->getVariationFromId('group_experiment_2', '7725250007') + ], + 'test_experiment_multivariate' => [ + '122231' => $this->config->getVariationFromId('test_experiment_multivariate', '122231'), + '122232' => $this->config->getVariationFromId('test_experiment_multivariate', '122232'), + '122233' => $this->config->getVariationFromId('test_experiment_multivariate', '122233'), + '122234' => $this->config->getVariationFromId('test_experiment_multivariate', '122234') + ], + 'test_experiment_with_feature_rollout' => [ + '122236' => $this->config->getVariationFromId('test_experiment_with_feature_rollout', '122236'), + '122237' => $this->config->getVariationFromId('test_experiment_with_feature_rollout', '122237') + ], + 'test_experiment_double_feature' => [ + '122239' => $this->config->getVariationFromId('test_experiment_double_feature', '122239'), + '122240' => $this->config->getVariationFromId('test_experiment_double_feature', '122240') + ], + 'test_experiment_integer_feature' => [ + '122242' => $this->config->getVariationFromId('test_experiment_integer_feature', '122242'), + '122243' => $this->config->getVariationFromId('test_experiment_integer_feature', '122243') ] ], $variationIdMap->getValue($this->config)); - } - - public function testInitWithMoreData() - { - // Init with datafile consisting of more fields - $this->config = new ProjectConfig(DATAFILE_MORE_DATA, $this->loggerMock, $this->errorHandlerMock); - - // Check version - $version = new \ReflectionProperty(ProjectConfig::class, '_version'); - $version->setAccessible(true); - $this->assertEquals('2', $version->getValue($this->config)); - - // Check account ID - $accountId = new \ReflectionProperty(ProjectConfig::class, '_accountId'); - $accountId->setAccessible(true); - $this->assertEquals('1592310167', $accountId->getValue($this->config)); - // Check project ID - $projectId = new \ReflectionProperty(ProjectConfig::class, '_projectId'); - $projectId->setAccessible(true); - $this->assertEquals('7720880029', $projectId->getValue($this->config)); - - // Check revision - $revision = new \ReflectionProperty(ProjectConfig::class, '_revision'); - $revision->setAccessible(true); - $this->assertEquals('15', $revision->getValue($this->config)); - // Check group ID map - $groupIdMap = new \ReflectionProperty(ProjectConfig::class, '_groupIdMap'); - $groupIdMap->setAccessible(true); + // Check feature flag key map + $featureFlagKeyMap = new \ReflectionProperty(ProjectConfig::class, '_featureKeyMap'); + $featureFlagKeyMap->setAccessible(true); $this->assertEquals([ - '7722400015' => $this->config->getGroup('7722400015') - ], $groupIdMap->getValue($this->config)); - - // Check experiment key map - $experimentKeyMap = new \ReflectionProperty(ProjectConfig::class, '_experimentKeyMap'); - $experimentKeyMap->setAccessible(true); + 'boolean_feature' => $this->config->getFeatureFlagFromKey('boolean_feature'), + 'double_single_variable_feature' => $this->config->getFeatureFlagFromKey('double_single_variable_feature'), + 'integer_single_variable_feature' => $this->config->getFeatureFlagFromKey('integer_single_variable_feature'), + 'boolean_single_variable_feature' => $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'), + 'string_single_variable_feature' => $this->config->getFeatureFlagFromKey('string_single_variable_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') + ], $featureFlagKeyMap->getValue($this->config)); + + + // Check rollout id map + $rolloutIdMap = new \ReflectionProperty(ProjectConfig::class, '_rolloutIdMap'); + $rolloutIdMap->setAccessible(true); $this->assertEquals([ - 'test_experiment' => $this->config->getExperimentFromKey('test_experiment'), - 'paused_experiment' => $this->config->getExperimentFromKey('paused_experiment'), - 'group_experiment_1' => $this->config->getExperimentFromKey('group_experiment_1'), - 'group_experiment_2' => $this->config->getExperimentFromKey('group_experiment_2') - ], $experimentKeyMap->getValue($this->config)); + '166660' => $this->config->getRolloutFromId('166660'), + '166661' => $this->config->getRolloutFromId('166661') + ], $rolloutIdMap->getValue($this->config)); - // Check experiment ID map - $experimentIdMap = new \ReflectionProperty(ProjectConfig::class, '_experimentIdMap'); - $experimentIdMap->setAccessible(true); - $this->assertEquals([ - '7716830082' => $this->config->getExperimentFromId('7716830082'), - '7723330021' => $this->config->getExperimentFromId('7723330021'), - '7718750065' => $this->config->getExperimentFromId('7718750065'), - '7716830585' => $this->config->getExperimentFromId('7716830585') - ], $experimentIdMap->getValue($this->config)); - // Check event key map - $eventKeyMap = new \ReflectionProperty(ProjectConfig::class, '_eventKeyMap'); - $eventKeyMap->setAccessible(true); - $this->assertEquals([ - 'purchase' => $this->config->getEvent('purchase') - ], $eventKeyMap->getValue($this->config)); - - // Check attribute key map - $attributeKeyMap = new \ReflectionProperty(ProjectConfig::class, '_attributeKeyMap'); - $attributeKeyMap->setAccessible(true); - $this->assertEquals([ - 'device_type' => $this->config->getAttribute('device_type'), - 'location' => $this->config->getAttribute('location') - ], $attributeKeyMap->getValue($this->config)); - - // Check audience ID map - $audienceIdMap = new \ReflectionProperty(ProjectConfig::class, '_audienceIdMap'); - $audienceIdMap->setAccessible(true); - $this->assertEquals([ - '7718080042' => $this->config->getAudience('7718080042') - ], $audienceIdMap->getValue($this->config)); - - // Check variation key map - $variationKeyMap = new \ReflectionProperty(ProjectConfig::class, '_variationKeyMap'); - $variationKeyMap->setAccessible(true); - $this->assertEquals([ - 'test_experiment' => [ - 'control' => $this->config->getVariationFromKey('test_experiment', 'control'), - 'variation' => $this->config->getVariationFromKey('test_experiment', 'variation') - ], - 'paused_experiment' => [ - 'control' => $this->config->getVariationFromKey('paused_experiment', 'control'), - 'variation' => $this->config->getVariationFromKey('paused_experiment', 'variation') - ], - 'group_experiment_1' => [ - 'group_exp_1_var_1' => $this->config->getVariationFromKey('group_experiment_1', 'group_exp_1_var_1'), - 'group_exp_1_var_2' => $this->config->getVariationFromKey('group_experiment_1', 'group_exp_1_var_2') - ], - 'group_experiment_2' => [ - 'group_exp_2_var_1' => $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_1'), - 'group_exp_2_var_2' => $this->config->getVariationFromKey('group_experiment_2', 'group_exp_2_var_2') - ] - ], $variationKeyMap->getValue($this->config)); + // Check variable usage + $variableUsages = [ + new VariableUsage("155560", "F"), + new VariableUsage("155561", "red") + ]; + $expectedVariation = new Variation("122231", "Fred", $variableUsages); + $actualVariation = $this->config->getVariationFromKey("test_experiment_multivariate", "Fred"); - // Check variation ID map - $variationIdMap = new \ReflectionProperty(ProjectConfig::class, '_variationIdMap'); - $variationIdMap->setAccessible(true); - $this->assertEquals([ - 'test_experiment' => [ - '7722370027' => $this->config->getVariationFromId('test_experiment', '7722370027'), - '7721010009' => $this->config->getVariationFromId('test_experiment', '7721010009') - ], - 'paused_experiment' => [ - '7722370427' => $this->config->getVariationFromId('paused_experiment', '7722370427'), - '7721010509' => $this->config->getVariationFromId('paused_experiment', '7721010509') - ], - 'group_experiment_1' => [ - '7722260071' => $this->config->getVariationFromId('group_experiment_1', '7722260071'), - '7722360022' => $this->config->getVariationFromId('group_experiment_1', '7722360022') - ], - 'group_experiment_2' => [ - '7713030086' => $this->config->getVariationFromId('group_experiment_2', '7713030086'), - '7725250007' => $this->config->getVariationFromId('group_experiment_2', '7725250007') - ] - ], $variationIdMap->getValue($this->config)); + $this->assertEquals($expectedVariation, $actualVariation); } public function testGetAccountId() @@ -483,6 +330,30 @@ public function testGetExperimentInvalidId() $this->assertEquals(new Experiment(), $this->config->getExperimentFromId('42')); } + public function testGetFeatureFlagInvalidKey() + { + $this->loggerMock->expects($this->once()) + ->method('log') + ->with(Logger::ERROR, 'FeatureFlag Key "42" is not in datafile.'); + $this->errorHandlerMock->expects($this->once()) + ->method('handleError') + ->with(new InvalidFeatureFlagException('Provided feature flag is not in datafile.')); + + $this->assertEquals(new FeatureFlag(), $this->config->getFeatureFlagFromKey('42')); + } + + public function testGetRolloutInvalidId() + { + $this->loggerMock->expects($this->once()) + ->method('log') + ->with(Logger::ERROR, 'Rollout with ID "42" is not in the datafile.'); + $this->errorHandlerMock->expects($this->once()) + ->method('handleError') + ->with(new InvalidRolloutException('Provided rollout is not in datafile.')); + + $this->assertEquals(new Rollout(), $this->config->getRolloutFromId('42')); + } + public function testGetEventValidKey() { $event = $this->config->getEvent('purchase'); diff --git a/tests/TestData.php b/tests/TestData.php index d42a194f..6aea300c 100644 --- a/tests/TestData.php +++ b/tests/TestData.php @@ -21,60 +21,716 @@ use Optimizely\Event\Dispatcher\EventDispatcherInterface; use Optimizely\Event\LogEvent; -define('DATAFILE', - '{"experiments": [{"status": "Running", "key": "test_experiment", "layerId": "7719770039", - "trafficAllocation": [{"entityId": "", "endOfRange": 1500}, {"entityId": "7722370027", "endOfRange": 4000}, - {"entityId": "7721010009", "endOfRange": 8000}], "audienceIds": ["7718080042"], - "variations": [{"id": "7722370027", "key": "control"}, {"id": "7721010009", "key": "variation"}], - "forcedVariations": {"user1": "control"}, "id": "7716830082"}, {"status": "Paused", "key": "paused_experiment", "layerId": "7719779139", - "trafficAllocation": [{"entityId": "7722370427", "endOfRange": 5000}, - {"entityId": "7721010509", "endOfRange": 8000}], "audienceIds": [], - "variations": [{"id": "7722370427", "key": "control"}, {"id": "7721010509", "key": "variation"}], - "forcedVariations": {}, "id": "7716830585"}], "version": "2", - "audiences": [{"conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"iPhone\"}]], [\"or\", [\"or\", {\"name\": \"location\", \"type\": \"custom_attribute\", \"value\": \"San Francisco\"}]]]", "id": "7718080042", "name": "iPhone users in San Francisco"}], - "groups": [{"policy": "random", "trafficAllocation": [{"entityId": "", "endOfRange": 500}, {"entityId": "7723330021", "endOfRange": 2000}, {"entityId": "7718750065", "endOfRange": 6000}], "experiments": [{"status": "Running", "key": "group_experiment_1", "layerId": "7721010011", "trafficAllocation": [{"entityId": "7722260071", "endOfRange": 5000}, {"entityId": "7722360022", "endOfRange": 10000}], "audienceIds": [], "variations": [{"id": "7722260071", "key": "group_exp_1_var_1"}, {"id": "7722360022", "key": "group_exp_1_var_2"}], "forcedVariations": {"user1": "group_exp_1_var_1"}, "id": "7723330021"}, {"status": "Running", "key": "group_experiment_2", "layerId": "7721020020", "trafficAllocation": [{"entityId": "7713030086", "endOfRange": 5000}, {"entityId": "7725250007", "endOfRange": 10000}], "audienceIds": [], - "variations": [{"id": "7713030086", "key": "group_exp_2_var_1"}, {"id": "7725250007", "key": "group_exp_2_var_2"}], "forcedVariations": {}, "id": "7718750065"}], "id": "7722400015"}], - "attributes": [{"id": "7723280020", "key": "device_type"}, {"id": "7723340004", "key": "location"}], - "projectId": "7720880029", "accountId": "1592310167", - "events": [{"experimentIds": ["7716830082", "7723330021", "7718750065", "7716830585"], "id": "7718020063", "key": "purchase"}],"anonymizeIP": false, - "revision": "15"}'); - -define('DATAFILE_V3', - '{"experiments": [{"status": "Running", "key": "test_experiment", "layerId": "7719770039", - "trafficAllocation": [{"entityId": "", "endOfRange": 1500}, {"entityId": "7722370027", "endOfRange": 4000}, - {"entityId": "7721010009", "endOfRange": 8000}], "audienceIds": ["7718080042"], - "variations": [{"id": "7722370027", "key": "control", "variables": [{"id": "8284765437", "value": "true"}]}, {"id": "7721010009", "key": "variation", "variables": [{"id": "8284765437", "value": "false"}]}], - "forcedVariations": {"user1": "control"}, "id": "7716830082"}, {"status": "Paused", "key": "paused_experiment", "layerId": "7719779139", - "trafficAllocation": [{"entityId": "7722370427", "endOfRange": 5000}, - {"entityId": "7721010509", "endOfRange": 8000}], "audienceIds": [], - "variations": [{"id": "7722370427", "key": "control", "variables": []}, {"id": "7721010509", "key": "variation", "variables": []}], - "forcedVariations": {}, "id": "7716830585"}], "version": "2", - "audiences": [{"conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"iPhone\"}]], [\"or\", [\"or\", {\"name\": \"location\", \"type\": \"custom_attribute\", \"value\": \"San Francisco\"}]]]", "id": "7718080042", "name": "iPhone users in San Francisco"}], - "groups": [{"policy": "random", "trafficAllocation": [{"entityId": "", "endOfRange": 500}, {"entityId": "7723330021", "endOfRange": 2000}, {"entityId": "7718750065", "endOfRange": 6000}], "experiments": [{"status": "Running", "key": "group_experiment_1", "layerId": "7721010011", "trafficAllocation": [{"entityId": "7722260071", "endOfRange": 5000}, {"entityId": "7722360022", "endOfRange": 10000}], "audienceIds": [], "variations": [{"id": "7722260071", "key": "group_exp_1_var_1", "variables": []}, {"id": "7722360022", "key": "group_exp_1_var_2", "variables": []}], "forcedVariations": {"user1": "group_exp_1_var_1"}, "id": "7723330021"}, {"status": "Running", "key": "group_experiment_2", "layerId": "7721020020", "trafficAllocation": [{"entityId": "7713030086", "endOfRange": 5000}, {"entityId": "7725250007", "endOfRange": 10000}], "audienceIds": [], - "variations": [{"id": "7713030086", "key": "group_exp_2_var_1", "variables": []}, {"id": "7725250007", "key": "group_exp_2_var_2", "variables": []}], "forcedVariations": {}, "id": "7718750065"}], "id": "7722400015"}], - "attributes": [{"id": "7723280020", "key": "device_type"}, {"id": "7723340004", "key": "location"}], - "projectId": "7720880029", "accountId": "1592310167", - "events": [{"experimentIds": ["7716830082", "7723330021", "7718750065", "7716830585"], "id": "7718020063", "key": "purchase"}], - "anonymizeIP": false, "variables": [{"defaultValue": "true", "type": "boolean", "id": "8284765437", "key": "is_working"}], - "revision": "15"}'); - -define('DATAFILE_MORE_DATA', - '{"experiments": [{"status": "Running", "key": "test_experiment", "layerId": "7719770039", - "trafficAllocation": [{"entityId": "", "endOfRange": 1500}, {"entityId": "7722370027", "endOfRange": 4000}, - {"entityId": "7721010009", "endOfRange": 8000}], "audienceIds": ["7718080042"], - "variations": [{"id": "7722370027", "key": "control"}, {"id": "7721010009", "key": "variation"}], - "forcedVariations": {"user1": "control"}, "id": "7716830082"}, {"status": "Paused", "key": "paused_experiment", "layerId": "7719779139", - "trafficAllocation": [{"entityId": "7722370427", "endOfRange": 5000}, - {"entityId": "7721010509", "endOfRange": 8000}], "audienceIds": [], - "variations": [{"id": "7722370427", "key": "control"}, {"id": "7721010509", "key": "variation", "some_additiona_key": "some_additional_value"}], - "forcedVariations": {}, "id": "7716830585"}], "version": "2", - "audiences": [{"conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"iPhone\"}]], [\"or\", [\"or\", {\"name\": \"location\", \"type\": \"custom_attribute\", \"value\": \"San Francisco\"}]]]", "id": "7718080042", "name": "iPhone users in San Francisco"}], - "groups": [{"policy": "random", "trafficAllocation": [{"entityId": "", "endOfRange": 500}, {"entityId": "7723330021", "endOfRange": 2000}, {"entityId": "7718750065", "endOfRange": 6000}], "experiments": [{"status": "Running", "key": "group_experiment_1", "layerId": "7721010011", "trafficAllocation": [{"entityId": "7722260071", "endOfRange": 5000}, {"entityId": "7722360022", "endOfRange": 10000}], "audienceIds": [], "variations": [{"id": "7722260071", "key": "group_exp_1_var_1"}, {"id": "7722360022", "key": "group_exp_1_var_2"}], "forcedVariations": {"user1": "group_exp_1_var_1"}, "id": "7723330021"}, {"status": "Running", "key": "group_experiment_2", "layerId": "7721020020", "trafficAllocation": [{"entityId": "7713030086", "endOfRange": 5000}, {"entityId": "7725250007", "endOfRange": 10000}], "audienceIds": [], - "variations": [{"id": "7713030086", "key": "group_exp_2_var_1"}, {"id": "7725250007", "key": "group_exp_2_var_2"}], "forcedVariations": {}, "id": "7718750065"}], "id": "7722400015"}], - "attributes": [{"id": "7723280020", "key": "device_type"}, {"id": "7723340004", "key": "location"}], - "projectId": "7720880029", "accountId": "1592310167", - "events": [{"experimentIds": ["7716830082", "7723330021", "7718750065", "7716830585"], "id": "7718020063", "key": "purchase"}],"anonymizeIP": false, - "revision": "15", "random_data_key": [{"key_1": "value_1", "key_2": "value_2"}]}'); +define('DATAFILE','{ + "experiments": [ + { + "status": "Running", + "key": "test_experiment", + "layerId": "7719770039", + "trafficAllocation": [ + { + "entityId": "", + "endOfRange": 1500 + }, + { + "entityId": "7722370027", + "endOfRange": 4000 + }, + { + "entityId": "7721010009", + "endOfRange": 8000 + } + ], + "audienceIds": [ + "7718080042" + ], + "variations": [ + { + "id": "7722370027", + "key": "control" + }, + { + "id": "7721010009", + "key": "variation" + } + ], + "forcedVariations": { + "user1": "control" + }, + "id": "7716830082" + }, + { + "status": "Paused", + "key": "paused_experiment", + "layerId": "7719779139", + "trafficAllocation": [ + { + "entityId": "7722370427", + "endOfRange": 5000 + }, + { + "entityId": "7721010509", + "endOfRange": 8000 + } + ], + "audienceIds": [ + + ], + "variations": [ + { + "id": "7722370427", + "key": "control" + }, + { + "id": "7721010509", + "key": "variation" + } + ], + "forcedVariations": { + + }, + "id": "7716830585" + }, + { + "key": "test_experiment_multivariate", + "status": "Running", + "layerId": "4", + "audienceIds": [ + "11154" + ], + "id": "122230", + "forcedVariations": { + + }, + "trafficAllocation": [ + { + "entityId": "122231", + "endOfRange": 2500 + }, + { + "entityId": "122232", + "endOfRange": 5000 + }, + { + "entityId": "122233", + "endOfRange": 7500 + }, + { + "entityId": "122234", + "endOfRange": 10000 + } + ], + "variations": [ + { + "id": "122231", + "key": "Fred", + "variables": [ + { + "id": "155560", + "value": "F" + }, + { + "id": "155561", + "value": "red" + } + ] + }, + { + "id": "122232", + "key": "Feorge", + "variables": [ + { + "id": "155560", + "value": "F" + }, + { + "id": "155561", + "value": "eorge" + } + ] + }, + { + "id": "122233", + "key": "Gred", + "variables": [ + { + "id": "155560", + "value": "G" + }, + { + "id": "155561", + "value": "red" + } + ] + }, + { + "id": "122234", + "key": "George", + "variables": [ + { + "id": "155560", + "value": "G" + }, + { + "id": "155561", + "value": "eorge" + } + ] + } + ] + }, + { + "key": "test_experiment_with_feature_rollout", + "status": "Running", + "layerId": "5", + "audienceIds": [ + + ], + "id": "122235", + "forcedVariations": { + + }, + "trafficAllocation": [ + { + "entityId": "122236", + "endOfRange": 5000 + }, + { + "entityId": "122237", + "endOfRange": 10000 + } + ], + "variations": [ + { + "id": "122236", + "key": "control", + "variables": [ + { + "id": "155558", + "value": "cta_1" + } + ] + }, + { + "id": "122237", + "key": "variation", + "variables": [ + { + "id": "155558", + "value": "cta_2" + } + ] + } + ] + }, + { + "key": "test_experiment_double_feature", + "status": "Running", + "layerId": "5", + "audienceIds": [ + + ], + "id": "122238", + "forcedVariations": { + + }, + "trafficAllocation": [ + { + "entityId": "122239", + "endOfRange": 5000 + }, + { + "entityId": "122240", + "endOfRange": 10000 + } + ], + "variations": [ + { + "id": "122239", + "key": "control", + "variables": [ + { + "id": "155551", + "value": "42.42" + } + ] + }, + { + "id": "122240", + "key": "variation", + "variables": [ + { + "id": "155551", + "value": "13.37" + } + ] + } + ] + }, + { + "key": "test_experiment_integer_feature", + "status": "Running", + "layerId": "6", + "audienceIds": [ + + ], + "id": "122241", + "forcedVariations": { + + }, + "trafficAllocation": [ + { + "entityId": "122242", + "endOfRange": 5000 + }, + { + "entityId": "122243", + "endOfRange": 10000 + } + ], + "variations": [ + { + "id": "122242", + "key": "control", + "variables": [ + { + "id": "155553", + "value": "42" + } + ] + }, + { + "id": "122243", + "key": "variation", + "variables": [ + { + "id": "155553", + "value": "13" + } + ] + } + ] + } + ], + "version": "4", + "audiences": [ + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"iPhone\"}]], [\"or\", [\"or\", {\"name\": \"location\", \"type\": \"custom_attribute\", \"value\": \"San Francisco\"}]]]", + "id": "7718080042", + "name": "iPhone users in San Francisco" + }, + { + "name": "Chrome users", + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", \"type\": \"custom_attribute\", \"value\": \"chrome\"}]]]", + "id": "11154" + } + ], + "groups": [ + { + "policy": "random", + "trafficAllocation": [ + { + "entityId": "", + "endOfRange": 500 + }, + { + "entityId": "7723330021", + "endOfRange": 2000 + }, + { + "entityId": "7718750065", + "endOfRange": 6000 + } + ], + "experiments": [ + { + "status": "Running", + "key": "group_experiment_1", + "layerId": "7721010011", + "trafficAllocation": [ + { + "entityId": "7722260071", + "endOfRange": 5000 + }, + { + "entityId": "7722360022", + "endOfRange": 10000 + } + ], + "audienceIds": [ + + ], + "variations": [ + { + "id": "7722260071", + "key": "group_exp_1_var_1", + "variables": [ + { + "id": "155563", + "value": "groupie_1_v1" + } + ] + }, + { + "id": "7722360022", + "key": "group_exp_1_var_2", + "variables": [ + { + "id": "155563", + "value": "groupie_1_v2" + } + ] + } + ], + "forcedVariations": { + "user1": "group_exp_1_var_1" + }, + "id": "7723330021" + }, + { + "status": "Running", + "key": "group_experiment_2", + "layerId": "7721020020", + "trafficAllocation": [ + { + "entityId": "7713030086", + "endOfRange": 5000 + }, + { + "entityId": "7725250007", + "endOfRange": 10000 + } + ], + "audienceIds": [ + + ], + "variations": [ + { + "id": "7713030086", + "key": "group_exp_2_var_1", + "variables": [ + { + "id": "155563", + "value": "groupie_2_v1" + } + ] + }, + { + "id": "7725250007", + "key": "group_exp_2_var_2", + "variables": [ + { + "id": "155563", + "value": "groupie_2_v1" + } + ] + } + ], + "forcedVariations": { + + }, + "id": "7718750065" + } + ], + "id": "7722400015" + } + ], + "attributes": [ + { + "id": "7723280020", + "key": "device_type" + }, + { + "id": "7723340004", + "key": "location" + } + ], + "projectId": "7720880029", + "accountId": "1592310167", + "events": [ + { + "experimentIds": [ + "7716830082", + "7723330021", + "7718750065", + "7716830585" + ], + "id": "7718020063", + "key": "purchase" + } + ], + "anonymizeIP": false, + "revision": "15", + "featureFlags": [ + { + "id": "155549", + "key": "boolean_feature", + "rolloutId": "", + "experimentIds": [ + "7723330021", + "7718750065" + ], + "variables": [ + + ] + }, + { + "id": "155550", + "key": "double_single_variable_feature", + "rolloutId": "", + "experimentIds": [ + "122238" + ], + "variables": [ + { + "id": "155551", + "key": "double_variable", + "type": "double", + "defaultValue": "14.99" + } + ] + }, + { + "id": "155552", + "key": "integer_single_variable_feature", + "rolloutId": "", + "experimentIds": [ + "122241" + ], + "variables": [ + { + "id": "155553", + "key": "integer_variable", + "type": "integer", + "defaultValue": "7" + } + ] + }, + { + "id": "155554", + "key": "boolean_single_variable_feature", + "rolloutId": "166660", + "experimentIds": [ + + ], + "variables": [ + { + "id": "155556", + "key": "boolean_variable", + "type": "boolean", + "defaultValue": "true" + } + ] + }, + { + "id": "155557", + "key": "string_single_variable_feature", + "rolloutId": "166661", + "experimentIds": [ + "122235" + ], + "variables": [ + { + "id": "155558", + "key": "string_variable", + "type": "string", + "defaultValue": "wingardium leviosa" + } + ] + }, + { + "id": "155559", + "key": "multi_variate_feature", + "rolloutId": "", + "experimentIds": [ + "122230" + ], + "variables": [ + { + "id": "155560", + "key": "first_letter", + "type": "string", + "defaultValue": "H" + }, + { + "id": "155561", + "key": "rest_of_name", + "type": "string", + "defaultValue": "arry" + } + ] + }, + { + "id": "155562", + "key": "mutex_group_feature", + "rolloutId": "", + "experimentIds": [ + "7723330021", + "7718750065" + ], + "variables": [ + { + "id": "155563", + "key": "correlating_variation_name", + "type": "string", + "defaultValue": "null" + } + ] + }, + { + "id": "155564", + "key": "empty_feature", + "rolloutId": "", + "experimentIds": [ + + ], + "variables": [ + + ] + } + ], + "rollouts": [ + { + "id": "166660", + "experiments": [ + { + "id": "177770", + "key": "177770", + "status": "Running", + "layerId": "166660", + "audienceIds": [ + "11154" + ], + "variations": [ + { + "id": "177771", + "key": "177771", + "variables": [ + { + "id": "155556", + "value": "true" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "177771", + "endOfRange": 1000 + } + ] + }, + { + "id": "177772", + "key": "177772", + "status": "Running", + "layerId": "166660", + "audienceIds": [ + "11155" + ], + "variations": [ + { + "id": "177773", + "key": "177773", + "variables": [ + { + "id": "155556", + "value": "false" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "177773", + "endOfRange": 10000 + } + ] + }, + { + "id": "177776", + "key": "177776", + "status": "Running", + "layerId": "166660", + "audienceIds": [ + + ], + "variations": [ + { + "id": "177778", + "key": "177778", + "variables": [ + { + "id": "155556", + "value": "false" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "177778", + "endOfRange": 10000 + } + ] + } + ] + }, + { + "id": "166661", + "experiments": [ + { + "id": "177774", + "key": "177774", + "status": "Running", + "layerId": "166661", + "audienceIds": [ + "11154" + ], + "variations": [ + { + "id": "177775", + "key": "177775", + "variables": [ + + ] + } + ], + "trafficAllocation": [ + { + "entityId": "177775", + "endOfRange": 1500 + } + ] + }, + { + "id": "177779", + "key": "177779", + "status": "Running", + "layerId": "166661", + "audienceIds": [ + + ], + "variations": [ + { + "id": "177780", + "key": "177780", + "variables": [ + + ] + } + ], + "trafficAllocation": [ + { + "entityId": "177780", + "endOfRange": 1500 + } + ] + } + ] + } + ] +}'); /** * Class TestBucketer diff --git a/tests/UtilsTests/ValidatorTest.php b/tests/UtilsTests/ValidatorTest.php index 175f2bcd..7406deee 100644 --- a/tests/UtilsTests/ValidatorTest.php +++ b/tests/UtilsTests/ValidatorTest.php @@ -17,14 +17,24 @@ namespace Optimizely\Tests; +use Monolog\Logger; use Optimizely\ErrorHandler\NoOpErrorHandler; use Optimizely\Logger\NoOpLogger; use Optimizely\ProjectConfig; use Optimizely\Utils\Validator; - class ValidatorTest extends \PHPUnit_Framework_TestCase { + protected $loggerMock; + + protected function setUp() + { + // Mock Logger + $this->loggerMock = $this->getMockBuilder(NoOpLogger::class) + ->setMethods(array('log')) + ->getMock(); + } + public function testValidateJsonSchemaValidFile() { $this->assertTrue(Validator::validateJsonSchema(DATAFILE)); @@ -42,6 +52,15 @@ public function testValidateJsonSchemaNoJsonContent() $this->assertFalse(Validator::validateJsonSchema($invalidDatafile)); } + public function testValidateJsonSchemaInvalidJsonWithLogger(){ + $invalidDatafile = '{"key1": "val1"}'; + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG,"JSON does not validate. Violations:\n"); + $this->assertFalse(Validator::validateJsonSchema($invalidDatafile, $this->loggerMock)); + + } + public function testAreAttributesValidValidAttributes() { // Empty attributes From 4018df97e779cc77e8e0858aed63486b7221d2e1 Mon Sep 17 00:00:00 2001 From: Owais Date: Fri, 27 Oct 2017 10:22:56 +0500 Subject: [PATCH 02/20] :pencil2: isFeatureEnabled :pencil2: getVariableValueForType --- src/Optimizely/Entity/Variation.php | 8 + .../InvalidFeatureVariableException.php | 23 +++ src/Optimizely/Optimizely.php | 152 +++++++++++++++--- src/Optimizely/ProjectConfig.php | 28 ++++ 4 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 src/Optimizely/Exceptions/InvalidFeatureVariableException.php diff --git a/src/Optimizely/Entity/Variation.php b/src/Optimizely/Entity/Variation.php index 14813520..be28c19a 100644 --- a/src/Optimizely/Entity/Variation.php +++ b/src/Optimizely/Entity/Variation.php @@ -95,6 +95,14 @@ public function getVariables(){ return $this->_variableUsageInstances; } + public function getVariableUsage($variableId){ + $variable_usage = $_variableIdToVariableUsageInstanceMap[$variableId]; + if(isset($variable_usage)) + return $variable_usage; + else + return null; + } + public function setVariables($variableUsageInstances){ $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances,null,VariableUsage::class); diff --git a/src/Optimizely/Exceptions/InvalidFeatureVariableException.php b/src/Optimizely/Exceptions/InvalidFeatureVariableException.php new file mode 100644 index 00000000..ffab68e0 --- /dev/null +++ b/src/Optimizely/Exceptions/InvalidFeatureVariableException.php @@ -0,0 +1,23 @@ +getVariation($experimentKey, $userId, $attributes); if (is_null($variationKey)) { $this->_logger->log(Logger::INFO, sprintf('Not activating user "%s".', $userId)); - return $variationKey; - } - - $impressionEvent = $this->_eventBuilder - ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); - $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); - $this->_logger->log( - Logger::DEBUG, - sprintf('Dispatching impression event to URL %s with params %s.', - $impressionEvent->getUrl(), http_build_query($impressionEvent->getParams()) - ) - ); - - try { - $this->_eventDispatcher->dispatchEvent($impressionEvent); - } - catch (Throwable $exception) { - $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', $exception->getMessage())); - } - catch (Exception $exception) { - $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', $exception->getMessage())); + return null; } + $this->sendImpression($experimentKey, $variationKey, $userId, $attributes); + return $variationKey; } @@ -388,4 +368,130 @@ public function getForcedVariation($experimentKey, $userId) return null; } } + + + public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ + + if (!$this->_isValid) { + $this->_logger->log(Logger::ERROR, "Datafile has invalid format. Failing '".__FUNCTION__."'"); + return null; + } + + if(!$featureFlagKey){ + $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty"); + return false; + } + + if(!$userId){ + $this->_logger->log(Logger::ERROR, "User ID cannot be empty"); + return false; + } + + $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); + if($feature_flag == new FeatureFlag){ + // Error logged in ProjectConfig - getFeatureFlagFromKey + return false; + } + + $variation = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); + if(!$variation){ + $this->_logger->log(Logger::INFO,"Feature Flag '{$featureFlagKey}' is not enabled for user '{$userId}'."); + return false; + } + + if($variation["experiment"]){ + $experiment_key = $variation["experiment"]->getKey(); + $variation_key = $variation["variation"]->getKey(); + + $this->sendImpression($experiment_key, $variation_key, $userId, $attributes); + } else { + $this->_logger->log(Logger::INFO,"The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); + } + + $this->_logger->log(Logger::INFO,"Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'."); + return true; + } + + public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $userId, $attributes, $variableType){ + + if(!$featureFlagKey){ + $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty"); + return null; + } + + if(!$variableKey){ + $this->_logger->log(Logger::ERROR, "Variable key cannot be empty"); + return null; + } + + if(!$userId){ + $this->_logger->log(Logger::ERROR, "User ID cannot be empty"); + return null; + } + + $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); + if($feature_flag == new FeatureFlag){ + // Error logged in ProjectConfig - getFeatureFlagFromKey + return null; + } + + $variable = $this->_config->getFeatureVariableFromKey($featureFlagKey, $variableKey); + if(!$variable){ + // Error message logged in ProjectConfig- getFeatureVariableFromKey + return null; + } + + if($variableType != $variable->getType()){ + $this->_logger->log( + Logger::ERROR,"Variable is of type {$variable->getType}, but you requested it as type {$variableType}"); + return null; + } + + $decision = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); + $variable_value = $variable->getDefaultValue(); + + if(!$decision){ + $this->_logger->log(Logger::INFO,"User '{$userId}'is not in any variation, ". + "returning default value'{$variable_value}'."); + } else { + $variation = $decision['variation']; + $variable_usage = $variation->getVariableUsage($variable->getId()); + if($variable_usage){ + $variable_value = $variable_usage->getValue(); + $this->_logger->log(Logger::INFO, + "Returning variable value '{$variable_value}' for variation '{$variation->getKey()}' ". + "of feature flag '{$featureFlagKey}'"); + } else { + $this->_logger->log(Logger::INFO, + "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}, '". + "returning default value '{$variable_value}.'"); + } + } + + return $variable_value; + } + + public function sendImpression($experimentKey, $variationKey, $userId, $attributes){ + $impressionEvent = $this->_eventBuilder + ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); + $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); + $this->_logger->log( + Logger::DEBUG, + sprintf('Dispatching impression event to URL %s with params %s.', + $impressionEvent->getUrl(), http_build_query($impressionEvent->getParams()) + ) + ); + + try { + $this->_eventDispatcher->dispatchEvent($impressionEvent); + } + catch (Throwable $exception) { + $this->_logger->log(Logger::ERROR, sprintf( + 'Unable to dispatch impression event. Error %s', $exception->getMessage())); + } + catch (Exception $exception) { + $this->_logger->log(Logger::ERROR, sprintf( + 'Unable to dispatch impression event. Error %s', $exception->getMessage())); + } + } } diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index 81585dd9..bd94193a 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -23,6 +23,7 @@ use Optimizely\Entity\Event; use Optimizely\Entity\Experiment; use Optimizely\Entity\FeatureFlag; +use Optimizely\Entity\FeatureVariable; use Optimizely\Entity\Group; use Optimizely\Entity\Rollout; use Optimizely\Entity\Variation; @@ -154,6 +155,8 @@ class ProjectConfig */ private $_rolloutIdMap; + private $_featureFlagVariableMap; + /** * ProjectConfig constructor to load and set project configuration data. * @@ -222,6 +225,13 @@ public function __construct($datafile, $logger, $errorHandler) foreach(array_values($this->_featureFlags) as $featureFlag){ $this->_featureKeyMap[$featureFlag->getKey()] = $featureFlag; } + + if($this->_featureKeyMap){ + foreach(array_values($this->_featureKeyMap) as $featureKey => $featureFlag){ + $this->_featureFlagVariableMap[$featureKey] = ConfigParser::generateMap( + $featureFlag->getVariables(), 'key', FeatureVariable::class); + } + } } /** @@ -430,6 +440,24 @@ public function getVariationFromId($experimentKey, $variationId) return new Variation(); } + public function getFeatureVariableFromKey($featureFlagKey, $variableKey) + { + $feature_flag = $this->getFeatureFlagFromKey($featureFlagKey); + if($feature_flag == new FeatureFlag()) + return null; + + if(isset($this->_featureFlagVariableMap[$featureFlagKey]) && + isset($this->_featureFlagVariableMap[$featureFlagKey][$variableKey])) { + return $this->_featureFlagVariableMap[$featureFlagKey][$variableKey]; + } + + $this->_logger->log(Logger::ERROR, sprintf( + 'No variable key "%s" defined in datafile for feature flag "%s".', $variableKey, $featureFlagKey)); + $this->_errorHandler->handleError( + new InvalidFeatureVariableException('Provided feature variable is not in datafile.')); + return null; + } + public function isVariationIdValid($experimentKey, $variationId) { return isset($this->_variationIdMap[$experimentKey]) && From e9b75cd5b349444edef2e3d256d6bde55fffea97 Mon Sep 17 00:00:00 2001 From: Owais Date: Fri, 27 Oct 2017 11:46:41 +0500 Subject: [PATCH 03/20] :pen: Partial --- src/Optimizely/Optimizely.php | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index d1fb1ef4..d399d2d0 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -379,18 +379,18 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ if(!$featureFlagKey){ $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty"); - return false; + return null; } if(!$userId){ $this->_logger->log(Logger::ERROR, "User ID cannot be empty"); - return false; + return null; } $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); if($feature_flag == new FeatureFlag){ // Error logged in ProjectConfig - getFeatureFlagFromKey - return false; + return null; } $variation = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); @@ -471,6 +471,34 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u return $variable_value; } + public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId, $attributes = null){ + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::BOOLEAN_TYPE); + + return $variable_value; + } + + public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId, $attributes = null){ + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::INTEGER_TYPE); + + return $variable_value; + } + + public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, $attributes = null){ + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::DOUBLE_TYPE); + + return $variable_value; + } + + public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, $attributes = null){ + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::STRING_TYPE); + + return $variable_value; + } + public function sendImpression($experimentKey, $variationKey, $userId, $attributes){ $impressionEvent = $this->_eventBuilder ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); From c848b547b4aa8dc8eea9170fcaf3ce63f6a5dce7 Mon Sep 17 00:00:00 2001 From: Owais Date: Fri, 27 Oct 2017 13:16:27 +0500 Subject: [PATCH 04/20] :pen: Code Implementation done. Todo: Unit Tests --- src/Optimizely/Optimizely.php | 13 ++++++ src/Optimizely/Utils/VariableTypeUtils.php | 54 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/Optimizely/Utils/VariableTypeUtils.php diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index d399d2d0..452b7752 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -34,6 +34,7 @@ use Optimizely\UserProfile\UserProfileServiceInterface; use Optimizely\Utils\EventTagUtils; use Optimizely\Utils\Validator; +use Optimizely\Utils\VariableTypeUtils; /** * Class Optimizely @@ -475,6 +476,9 @@ public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::BOOLEAN_TYPE); + if(!is_null($variable_value)) + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + return $variable_value; } @@ -482,6 +486,9 @@ public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::INTEGER_TYPE); + if(!is_null($variable_value)) + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + return $variable_value; } @@ -489,6 +496,9 @@ public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::DOUBLE_TYPE); + if(!is_null($variable_value)) + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + return $variable_value; } @@ -496,6 +506,9 @@ public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::STRING_TYPE); + if(!is_null($variable_value)) + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + return $variable_value; } diff --git a/src/Optimizely/Utils/VariableTypeUtils.php b/src/Optimizely/Utils/VariableTypeUtils.php new file mode 100644 index 00000000..94758beb --- /dev/null +++ b/src/Optimizely/Utils/VariableTypeUtils.php @@ -0,0 +1,54 @@ +log(Logger::ERROR, "Unable to cast variable value '{$value}' to type '{$variableType}'."); + + return $return_value; + } + +} \ No newline at end of file From 73526fb236248548ffdb89ce4366bbde640a9bd2 Mon Sep 17 00:00:00 2001 From: Owais Date: Fri, 27 Oct 2017 16:38:26 +0500 Subject: [PATCH 05/20] :nit: --- src/Optimizely/Optimizely.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 452b7752..c47efa59 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -394,15 +394,15 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ return null; } - $variation = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); - if(!$variation){ + $decision = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); + if(!$decision){ $this->_logger->log(Logger::INFO,"Feature Flag '{$featureFlagKey}' is not enabled for user '{$userId}'."); return false; } - if($variation["experiment"]){ - $experiment_key = $variation["experiment"]->getKey(); - $variation_key = $variation["variation"]->getKey(); + if($decision["experiment"]){ + $experiment_key = $decision["experiment"]->getKey(); + $variation_key = $decision["variation"]->getKey(); $this->sendImpression($experiment_key, $variation_key, $userId, $attributes); } else { From 79788eead1a6612799ac779006e3c74fc6876184 Mon Sep 17 00:00:00 2001 From: Owais Date: Mon, 30 Oct 2017 16:13:41 +0500 Subject: [PATCH 06/20] :pen: Decision Object introduced :pen: Logic slightly changed based on Decision :pen: nits addressed --- src/Optimizely/DecisionService/Decision.php | 68 +++++++++++++++++++ .../DecisionService/DecisionService.php | 57 ++++++++-------- .../DecisionServiceTest.php | 42 ++++++------ 3 files changed, 118 insertions(+), 49 deletions(-) create mode 100644 src/Optimizely/DecisionService/Decision.php diff --git a/src/Optimizely/DecisionService/Decision.php b/src/Optimizely/DecisionService/Decision.php new file mode 100644 index 00000000..f25fffe0 --- /dev/null +++ b/src/Optimizely/DecisionService/Decision.php @@ -0,0 +1,68 @@ +_experimentId = $experimentId; + $this->_variationId = $variationId; + $this->_source = $source; + } + + public function getExperimentId() + { + return $this->_experimentId; + } + + public function getVariationId() + { + return $this->_variationId; + } + + public function getSource() + { + return $this->_source; + } +} \ No newline at end of file diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 780241eb..7b714bbe 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -20,16 +20,15 @@ use Monolog\Logger; use Optimizely\Bucketer; use Optimizely\Entity\Experiment; +use Optimizely\Entity\FeatureFlag; +use Optimizely\Entity\Rollout; use Optimizely\Entity\Variation; use Optimizely\Logger\LoggerInterface; use Optimizely\ProjectConfig; -use Optimizely\UserProfile\Decision; use Optimizely\UserProfile\UserProfileServiceInterface; use Optimizely\UserProfile\UserProfile; use Optimizely\UserProfile\UserProfileUtils; use Optimizely\Utils\Validator; -use Optimizely\Entity\FeatureFlag; -use Optimizely\Entity\Rollout; // This value was decided between App Backend, Audience, and Oasis teams, but may possibly change. // We decided to prefix the reserved keyword with '$' because it is a symbol that is not @@ -42,10 +41,11 @@ * * The decision service contains all logic around how a user decision is made. This includes all of the following (in order): * 1. Checking experiment status. - * 2. Checking whitelisting. - * 3. Check sticky bucketing. - * 4. Checking audience targeting. - * 5. Using Murmurhash3 to bucket the user. + * 2. Checking force bucketing + * 3. Checking whitelisting. + * 4. Check sticky bucketing. + * 5. Checking audience targeting. + * 6. Using Murmurhash3 to bucket the user. * * @package Optimizely */ @@ -144,13 +144,14 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null } /** - * Gets the Bucketing Id for Bucketing + * Gets the Bucketing ID for Bucketing * @param string $userId - * @param array $userAttributes + * @param array $userAttributes + * * @return string */ private function getBucketingId($userId, $userAttributes){ - // by default, the bucketing ID should be the user ID + // By default, the bucketing ID should be the user ID $bucketingId = $userId; // If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key @@ -167,28 +168,26 @@ private function getBucketingId($userId, $userAttributes){ * @param FeatureFlag $featureFlag The feature flag the user wants to access * @param string $userId user id * @param array $userAttributes user attributes - * @return array/null {"experiment" : Experiment, "variation": Variation } / null + * @return Decision / null */ public function getVariationForFeature(FeatureFlag $featureFlag, $userId, $userAttributes){ //Evaluate in this order: - //1. Attempt to bucket user into all experiments in the feature flag. - //2. Attempt to bucket user into rollout in the feature flag. + //1. Attempt to bucket user into experiment using feature flag. + //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 - $result = $this->getVariationForFeatureExperiment($featureFlag, $userId, $userAttributes); - if($result) - return $result; + $decision = $this->getVariationForFeatureExperiment($featureFlag, $userId, $userAttributes); + if($decision) + return $decision; // Check if the feature flag has rollout and the user is bucketed into one of it's rules - $variation = $this->getVariationForFeatureRollout($featureFlag, $userId, $userAttributes); - if($variation){ + $decision = $this->getVariationForFeatureRollout($featureFlag, $userId, $userAttributes); + if($decision){ $this->_logger->log(Logger::INFO, "User '{$userId}' is bucketed into a rollout for feature flag '{$featureFlag->getKey()}'." ); - return array( - "experiment" => null, - "variation" => $variation); + return $decision; } else{ $this->_logger->log(Logger::INFO, @@ -204,7 +203,7 @@ public function getVariationForFeature(FeatureFlag $featureFlag, $userId, $userA * @param FeatureFlag $featureFlag The feature flag the user wants to access * @param string $userId user id * @param array $userAttributes user userAttributes - * @return array/null {"experiment" : Experiment, "variation": Variation } / null + * @return Decision / null */ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $userId, $userAttributes){ $feature_flag_key = $featureFlag->getKey(); @@ -229,10 +228,8 @@ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $user if($variation instanceof Variation && $variation != new Variation){ $this->_logger->log(Logger::INFO, "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$feature_flag_key}'."); - return array( - "experiment"=> $experiment, - "variation" => $variation - ); + + return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_EXPERIMENT); } } @@ -249,7 +246,7 @@ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $user * @param FeatureFlag $featureFlag The feature flag the user wants to access * @param string $userId user id * @param array $userAttributes user userAttributes - * @return Variation/null + * @return Decision/ null */ public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId, $userAttributes){ $bucketing_id = $this->getBucketingId($userId, $userAttributes); @@ -290,7 +287,7 @@ public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId // Evaluate if user satisfies the traffic allocation for this rollout rule $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); if($variation && $variation != new Variation()){ - return $variation; + return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); } else { $this->_logger->log(Logger::DEBUG, "User '{$userId}' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now."); @@ -302,7 +299,7 @@ public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId $experiment = $rolloutRules[sizeof($rolloutRules)-1]; $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); if($variation && $variation != new Variation()){ - return $variation; + return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); } else { $this->_logger->log(Logger::DEBUG, "User '{$userId}' was excluded from the 'Everyone Else' rule for feature flag"); @@ -431,7 +428,7 @@ private function saveVariation(Experiment $experiment, Variation $variation, Use $decision = $userProfile->getDecisionForExperiment($experimentId); $variationId = $variation->getId(); if (is_null($decision)) { - $decision = new Decision($variationId); + $decision = new \Optimizely\UserProfile\Decision($variationId); } else { $decision->setVariationId($variationId); } diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 337b0ffb..1ddb7e9b 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -19,6 +19,7 @@ use Exception; use Monolog\Logger; use Optimizely\Bucketer; +use Optimizely\DecisionService\Decision; use Optimizely\DecisionService\DecisionService; use Optimizely\Entity\Experiment; use Optimizely\Entity\Variation; @@ -667,16 +668,14 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNot // should return the variation when the user is bucketed into a variation for the experiment on the feature flag public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucketed(){ // return the first variation of the `test_experiment_multivariate` experiment, which is attached to the `multi_variate_feature` + $experiment = $this->config->getExperimentFromKey('test_experiment_multivariate'); $variation = $this->config->getVariationFromId('test_experiment_multivariate','122231'); $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') ->will($this->returnValue($variation)); $feature_flag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); - $expected_decision = [ - 'experiment' => $this->config->getExperimentFromKey('test_experiment_multivariate'), - 'variation' => $this->config->getVariationFromId('test_experiment_multivariate','122231') - ]; + $expected_decision = new Decision($experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -699,10 +698,8 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBuck $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); $variation = $mutex_exp->getVariations()[0]; - $expected_decision = [ - 'experiment' => $mutex_exp, - 'variation' => $variation - ]; + $expected_decision = new Decision($mutex_exp->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -775,10 +772,8 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $rollout = $this->config->getRolloutFromId($rollout_id); $experiment = $rollout->getExperiments()[0]; $expected_variation = $experiment->getVariations()[0]; - $expected_decision = [ - 'experiment' => null, - 'variation' => $expected_variation - ]; + $expected_decision = new Decision( + $experiment->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); $decisionServiceMock ->method('getVariationForFeatureExperiment') @@ -786,7 +781,7 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $decisionServiceMock ->method('getVariationForFeatureRollout') - ->will($this->returnValue($expected_variation)); + ->will($this->returnValue($expected_decision)); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -895,7 +890,10 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); $experiment = $rollout->getExperiments()[0]; - $expected_variation = $experiment->getVariations()[0]; + $expected_variation = $experiment->getVariations()[0]; + + $expected_decision = new Decision( + $experiment->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -914,7 +912,7 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $this->assertEquals( $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_variation + $expected_decision ); } @@ -927,7 +925,10 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $experiment0 = $rollout->getExperiments()[0]; // Everyone Else Rule $experiment2 = $rollout->getExperiments()[2]; - $expected_variation = $experiment2->getVariations()[0]; + $expected_variation = $experiment2->getVariations()[0]; + + $expected_decision = new Decision( + $experiment2->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -956,7 +957,7 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $this->assertEquals( $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_variation + $expected_decision ); } @@ -1018,7 +1019,10 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $experiment1 = $rollout->getExperiments()[1]; // Everyone Else Rule $experiment2 = $rollout->getExperiments()[2]; - $expected_variation = $experiment2->getVariations()[0]; + $expected_variation = $experiment2->getVariations()[0]; + + $expected_decision = new Decision( + $experiment2->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); // Provide null attributes so that user does not qualify for audience $user_attributes = []; @@ -1045,7 +1049,7 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $this->assertEquals( $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_variation + $expected_decision ); } } From fafed363a3d3569714f903ec0e66ed6cea4f88c5 Mon Sep 17 00:00:00 2001 From: Owais Date: Mon, 30 Oct 2017 18:31:12 +0500 Subject: [PATCH 07/20] Testing - In Progress --- src/Optimizely/Entity/Variation.php | 2 +- src/Optimizely/Optimizely.php | 21 +++--- src/Optimizely/Utils/VariableTypeUtils.php | 5 +- tests/OptimizelyTest.php | 40 ++++++++++ tests/UtilsTests/VariableTypeUtilsTest.php | 85 ++++++++++++++++++++++ 5 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 tests/UtilsTests/VariableTypeUtilsTest.php diff --git a/src/Optimizely/Entity/Variation.php b/src/Optimizely/Entity/Variation.php index be28c19a..e72029a3 100644 --- a/src/Optimizely/Entity/Variation.php +++ b/src/Optimizely/Entity/Variation.php @@ -95,7 +95,7 @@ public function getVariables(){ return $this->_variableUsageInstances; } - public function getVariableUsage($variableId){ + public function getVariableUsageById($variableId){ $variable_usage = $_variableIdToVariableUsageInstanceMap[$variableId]; if(isset($variable_usage)) return $variable_usage; diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index c47efa59..3fe903a4 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -223,7 +223,7 @@ public function activate($experimentKey, $userId, $attributes = null) return null; } - $this->sendImpression($experimentKey, $variationKey, $userId, $attributes); + $this->sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes); return $variationKey; } @@ -374,17 +374,17 @@ public function getForcedVariation($experimentKey, $userId) public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ if (!$this->_isValid) { - $this->_logger->log(Logger::ERROR, "Datafile has invalid format. Failing '".__FUNCTION__."'"); + $this->_logger->log(Logger::ERROR, "Datafile has invalid format. Failing '".__FUNCTION__."'."); return null; } if(!$featureFlagKey){ - $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty"); + $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty."); return null; } if(!$userId){ - $this->_logger->log(Logger::ERROR, "User ID cannot be empty"); + $this->_logger->log(Logger::ERROR, "User ID cannot be empty."); return null; } @@ -404,7 +404,7 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ $experiment_key = $decision["experiment"]->getKey(); $variation_key = $decision["variation"]->getKey(); - $this->sendImpression($experiment_key, $variation_key, $userId, $attributes); + $this->sendImpressionEvent($experiment_key, $variation_key, $userId, $attributes); } else { $this->_logger->log(Logger::INFO,"The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); } @@ -456,7 +456,7 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u "returning default value'{$variable_value}'."); } else { $variation = $decision['variation']; - $variable_usage = $variation->getVariableUsage($variable->getId()); + $variable_usage = $variation->getVariableUsageById($variable->getId()); if($variable_usage){ $variable_value = $variable_usage->getValue(); $this->_logger->log(Logger::INFO, @@ -487,7 +487,7 @@ public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::INTEGER_TYPE); if(!is_null($variable_value)) - return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::INTEGER_TYPE, $this->_logger); return $variable_value; } @@ -497,7 +497,7 @@ public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::DOUBLE_TYPE); if(!is_null($variable_value)) - return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::DOUBLE_TYPE, $this->_logger); return $variable_value; } @@ -506,13 +506,10 @@ public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::STRING_TYPE); - if(!is_null($variable_value)) - return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); - return $variable_value; } - public function sendImpression($experimentKey, $variationKey, $userId, $attributes){ + public function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes){ $impressionEvent = $this->_eventBuilder ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); diff --git a/src/Optimizely/Utils/VariableTypeUtils.php b/src/Optimizely/Utils/VariableTypeUtils.php index 94758beb..4a4a18cd 100644 --- a/src/Optimizely/Utils/VariableTypeUtils.php +++ b/src/Optimizely/Utils/VariableTypeUtils.php @@ -24,7 +24,10 @@ class VariableTypeUtils { - public static function castStringToType($value, $variableType, $logger = null){ + public static function castStringToType($value, $variableType, LoggerInterface $logger = null){ + if($variableType == FeatureVariable::STRING_TYPE) + return $value; + $return_value = null; switch($variableType){ diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 1a11ba5b..cb035fbf 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -1730,4 +1730,44 @@ public function testGetVariationBucketingIdAttribute() $this->assertEquals(null, $variationKey, sprintf('Invalid variation key "%s" for getVariation with bucketing ID "%s".', $variationKey, $this->testBucketingIdControl)); } + public function testIsFeatureEnabledGivenInvalidDataFile(){ + $optlyObject = new Optimizely('Random datafile', null, $this->loggerMock); + $optlyObject->activate('some_experiment', 'some_user'); + + $this->expectOutputRegex("/Datafile has invalid format. Failing 'isFeatureEnabled'./"); + $optlyObject->isFeatureEnabled("boolean_feature", "user_id"); + } + + public function testIsFeatureEnabledGivenInvalidArguments(){ + // should return null and log a message when feature flag key is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled("","user_id"), null); + + // should return null and log a message when feature flag key is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled(null,"user_id"), null); + + // should return null and log a message when user id is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled("boolean_feature", ""), null); + + // should return null and log a message when user id is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled("boolean_feature", null), null); + + } + + } diff --git a/tests/UtilsTests/VariableTypeUtilsTest.php b/tests/UtilsTests/VariableTypeUtilsTest.php new file mode 100644 index 00000000..41c31537 --- /dev/null +++ b/tests/UtilsTests/VariableTypeUtilsTest.php @@ -0,0 +1,85 @@ +loggerMock = $this->getMockBuilder(NoOpLogger::class) + ->setMethods(array('log')) + ->getMock(); + + $this->variableUtilObj = new VariableTypeUtils(); + } + + public function testValueCastingToBoolean(){ + $this->assertSame($this->variableUtilObj->castStringToType('true', 'boolean'), true); + $this->assertSame($this->variableUtilObj->castStringToType('True', 'boolean'), true); + $this->assertSame($this->variableUtilObj->castStringToType('false', 'boolean'), false); + $this->assertSame($this->variableUtilObj->castStringToType('somestring', 'boolean'), false); + } + + public function testValueCastingToInteger(){ + $this->assertSame($this->variableUtilObj->castStringToType('1000', 'integer'), 1000); + $this->assertSame($this->variableUtilObj->castStringToType('123', 'integer'), 123); + + // should return nil and log a message if value can not be casted to an integer + $value = 'any-non-numeric-string'; + $type = 'integer'; + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with(Logger::ERROR, + "Unable to cast variable value '{$value}' to type '{$type}'."); + + $this->assertSame($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock), null); + } + + public function testValueCastingToDouble(){ + $this->assertSame($this->variableUtilObj->castStringToType('1000', 'double'), 1000.0); + $this->assertSame($this->variableUtilObj->castStringToType('3.0', 'double'), 3.0); + $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'double'), 13.37); + + // should return nil and log a message if value can not be casted to a double + $value = 'any-non-numeric-string'; + $type = 'double'; + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with(Logger::ERROR, + "Unable to cast variable value '{$value}' to type '{$type}'."); + + $this->assertSame($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock), null); + } + + public function testValueCastingToString(){ + $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'string'), '13.37'); + $this->assertSame($this->variableUtilObj->castStringToType('a string', 'string'), 'a string'); + $this->assertSame($this->variableUtilObj->castStringToType('3', 'string'), '3'); + $this->assertSame($this->variableUtilObj->castStringToType('false', 'string'), 'false'); + } +} + From e3ed7da540ee84d44d62452db3308fe848861eaa Mon Sep 17 00:00:00 2001 From: Owais Date: Mon, 30 Oct 2017 20:05:53 +0500 Subject: [PATCH 08/20] :pencil2: Unit Testing 50% done --- src/Optimizely/Optimizely.php | 15 +++-- src/Optimizely/ProjectConfig.php | 5 +- tests/OptimizelyTest.php | 100 ++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 3fe903a4..1fd11e9c 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -23,6 +23,8 @@ use Monolog\Logger; use Optimizely\DecisionService\DecisionService; use Optimizely\Entity\Experiment; +use Optimizely\Entity\FeatureFlag; +use Optimizely\Entity\Rollout; use Optimizely\Logger\DefaultLogger; use Optimizely\ErrorHandler\ErrorHandlerInterface; use Optimizely\ErrorHandler\NoOpErrorHandler; @@ -413,20 +415,21 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ return true; } - public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $userId, $attributes, $variableType){ - + public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $userId, + $attributes = null, $variableType = null) + { if(!$featureFlagKey){ - $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty"); + $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty."); return null; } if(!$variableKey){ - $this->_logger->log(Logger::ERROR, "Variable key cannot be empty"); + $this->_logger->log(Logger::ERROR, "Variable key cannot be empty."); return null; } if(!$userId){ - $this->_logger->log(Logger::ERROR, "User ID cannot be empty"); + $this->_logger->log(Logger::ERROR, "User ID cannot be empty."); return null; } @@ -444,7 +447,7 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u if($variableType != $variable->getType()){ $this->_logger->log( - Logger::ERROR,"Variable is of type {$variable->getType}, but you requested it as type {$variableType}"); + Logger::ERROR,"Variable is of type '{$variable->getType()}', but you requested it as type '{$variableType}'."); return null; } diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index bd94193a..fe30f32f 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -33,6 +33,7 @@ use Optimizely\Exceptions\InvalidEventException; use Optimizely\Exceptions\InvalidExperimentException; use Optimizely\Exceptions\InvalidFeatureFlagException; +use Optimizely\Exceptions\InvalidFeatureVariableException; use Optimizely\Exceptions\InvalidGroupException; use Optimizely\Exceptions\InvalidRolloutException; use Optimizely\Exceptions\InvalidVariationException; @@ -227,9 +228,9 @@ public function __construct($datafile, $logger, $errorHandler) } if($this->_featureKeyMap){ - foreach(array_values($this->_featureKeyMap) as $featureKey => $featureFlag){ + foreach($this->_featureKeyMap as $featureKey => $featureFlag){ $this->_featureFlagVariableMap[$featureKey] = ConfigParser::generateMap( - $featureFlag->getVariables(), 'key', FeatureVariable::class); + $featureFlag->getVariables(), 'key', FeatureVariable::class); } } } diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index cb035fbf..9b41a27b 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -1732,10 +1732,9 @@ public function testGetVariationBucketingIdAttribute() public function testIsFeatureEnabledGivenInvalidDataFile(){ $optlyObject = new Optimizely('Random datafile', null, $this->loggerMock); - $optlyObject->activate('some_experiment', 'some_user'); $this->expectOutputRegex("/Datafile has invalid format. Failing 'isFeatureEnabled'./"); - $optlyObject->isFeatureEnabled("boolean_feature", "user_id"); + $optlyObject->isFeatureEnabled("boolean_feature", "user_id"); } public function testIsFeatureEnabledGivenInvalidArguments(){ @@ -1766,8 +1765,105 @@ public function testIsFeatureEnabledGivenInvalidArguments(){ ->with(Logger::ERROR, "User ID cannot be empty."); $this->assertSame($this->optimizelyObject->isFeatureEnabled("boolean_feature", null), null); + } + + public function testIsFeatureEnabledGivenFeatureFlagNotFound(){ + $feature_key = "abcd"; // Any string that is not a feature flag key in the data file + + //should return null and log a message when no feature flag found against a valid feature key + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "FeatureFlag Key \"{$feature_key}\" is not in datafile."); + $this->assertSame($this->optimizelyObject->isFeatureEnabled($feature_key, "user_id"), null); + } + + + public function testGetFeatureVariableValueForTypeGivenInvalidArguments(){ + // should return null and log a message when feature flag key is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "", "double_variable", "user_id"), null); + + // should return null and log a message when feature flag key is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + null, "double_variable", "user_id"), null); + + // should return null and log a message when variable key is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Variable key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", "", "user_id"), null); + + // should return null and log a message when variable key is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Variable key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", null, "user_id"), null); + // should return null and log a message when user id is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", "double_variable", ""), null); + + // should return null and log a message when user id is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", "double_variable", null), null); } + public function testGetFeatureVariableValueForTypeGivenFeatureFlagNotFound(){ + $feature_key = "abcd"; // Any string that is not a feature flag key in the data file + + //should return null and log a message when no feature flag found against a valid feature key + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "FeatureFlag Key \"{$feature_key}\" is not in datafile."); + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + $feature_key , "double_variable", 'user_id'), null); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureVariableNotFound(){ + $feature_key = "boolean_feature"; // Any exisiting feature key in the data file + $variable_key = "abcd"; // Any string that is not a variable key in the data file + + //should return null and log a message when no feature flag found against a valid feature key + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "No variable key \"{$variable_key}\" defined in datafile ". + "for feature flag \"{$feature_key}\"."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + $feature_key , $variable_key, 'user_id'), null); + } + + public function testGetFeatureVariableValueForTypeGivenInvalidFeatureVariableType(){ + // should return null and log a message when a feature variable does exist but is + // called for another type + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Variable is of type 'double', but you requested it as type 'string'."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "double_single_variable_feature" , "double_variable", "user_id", null, "string") + , null); + + } } From bd142cfb730a717bccb8909fcb53c0239048f0c9 Mon Sep 17 00:00:00 2001 From: Owais Date: Tue, 31 Oct 2017 10:14:30 +0500 Subject: [PATCH 09/20] :pencil2: Unit Testing completed --- src/Optimizely/Entity/Variation.php | 9 +- src/Optimizely/Optimizely.php | 7 +- tests/OptimizelyTest.php | 340 ++++++++++++++++++++++++++++ 3 files changed, 348 insertions(+), 8 deletions(-) diff --git a/src/Optimizely/Entity/Variation.php b/src/Optimizely/Entity/Variation.php index e72029a3..95574300 100644 --- a/src/Optimizely/Entity/Variation.php +++ b/src/Optimizely/Entity/Variation.php @@ -54,7 +54,7 @@ public function __construct($id = null, $key = null, $variableUsageInstances = [ if(!empty($this->_variableUsageInstances)){ foreach(array_values($this->_variableUsageInstances) as $variableUsage){ - $_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; + $this->_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; } } } @@ -96,9 +96,8 @@ public function getVariables(){ } public function getVariableUsageById($variableId){ - $variable_usage = $_variableIdToVariableUsageInstanceMap[$variableId]; - if(isset($variable_usage)) - return $variable_usage; + if(isset($this->_variableIdToVariableUsageInstanceMap[$variableId])) + return $this->_variableIdToVariableUsageInstanceMap[$variableId]; else return null; } @@ -108,7 +107,7 @@ public function setVariables($variableUsageInstances){ if(!empty($this->_variableUsageInstances)){ foreach(array_values($this->_variableUsageInstances) as $variableUsage){ - $_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; + $this->_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; } } } diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 1fd11e9c..4963352e 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -24,6 +24,7 @@ use Optimizely\DecisionService\DecisionService; use Optimizely\Entity\Experiment; use Optimizely\Entity\FeatureFlag; +use Optimizely\Entity\FeatureVariable; use Optimizely\Entity\Rollout; use Optimizely\Logger\DefaultLogger; use Optimizely\ErrorHandler\ErrorHandlerInterface; @@ -456,7 +457,7 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u if(!$decision){ $this->_logger->log(Logger::INFO,"User '{$userId}'is not in any variation, ". - "returning default value'{$variable_value}'."); + "returning default value '{$variable_value}'."); } else { $variation = $decision['variation']; $variable_usage = $variation->getVariableUsageById($variable->getId()); @@ -467,8 +468,8 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u "of feature flag '{$featureFlagKey}'"); } else { $this->_logger->log(Logger::INFO, - "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}, '". - "returning default value '{$variable_value}.'"); + "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}' ". + "returning default value '{$variable_value}'."); } } diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 9b41a27b..d6059715 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -18,6 +18,7 @@ use Exception; use Monolog\Logger; +use Optimizely\DecisionService\DecisionService; use Optimizely\ErrorHandler\NoOpErrorHandler; use Optimizely\Event\LogEvent; use Optimizely\Exceptions\InvalidAttributeException; @@ -1777,6 +1778,120 @@ public function testIsFeatureEnabledGivenFeatureFlagNotFound(){ $this->assertSame($this->optimizelyObject->isFeatureEnabled($feature_key, "user_id"), null); } + public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + // should return false when no variation is returned for user + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue(null)); + + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, "Feature Flag 'double_single_variable_feature' is not enabled for user 'user_id'."); + + $this->assertSame( + $optimizelyMock->isFeatureEnabled('double_single_variable_feature', 'user_id'), + false); + } + + public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserBeingExperimented(){ + $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, $this->projectConfig)) + ->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 + $expected_decision = [ + 'experiment' => $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'), + 'variation' => $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control') + ]; + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with('test_experiment_double_feature', 'control', 'user_id', []); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, "Feature Flag 'double_single_variable_feature' is enabled for user 'user_id'."); + + $this->assertSame( + $optimizelyMock->isFeatureEnabled('double_single_variable_feature', 'user_id', []), + true); + } + + public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperimented(){ + $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, $this->projectConfig)) + ->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 + $rollout = $this->projectConfig->getRolloutFromId('166660'); + $experiment = $rollout->getExperiments()[0]; + $variation = $experiment->getVariations()[0]; + $expected_decision = [ + 'experiment' => null, + 'variation' => $variation + ]; + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + "The user 'user_id' is not being experimented on Feature Flag 'boolean_single_variable_feature'."); + + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::INFO, "Feature Flag 'boolean_single_variable_feature' is enabled for user 'user_id'."); + + $this->assertSame( + $optimizelyMock->isFeatureEnabled('boolean_single_variable_feature', 'user_id', []), + true); + } public function testGetFeatureVariableValueForTypeGivenInvalidArguments(){ // should return null and log a message when feature flag key is empty @@ -1866,4 +1981,229 @@ public function testGetFeatureVariableValueForTypeGivenInvalidFeatureVariableTyp , null); } + + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUser(){ + // should return default value + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($this->optimizelyObject, $decisionServiceMock); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue(null)); + + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with(Logger::INFO, + "User 'user_id'is not in any variation, returning default value '14.99'."); + + $this->assertSame( + $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), + '14.99'); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleIsInVariation(){ + // should return specific value + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($this->optimizelyObject, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + $expected_decision = [ + 'experiment' => $experiment, + 'variation' => $variation + ]; + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with(Logger::INFO, + "Returning variable value '42.42' for variation 'control' ". + "of feature flag 'double_single_variable_feature'"); + + $this->assertSame( + $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), + '42.42'); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleNotInVariation(){ + // should return default value + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($this->optimizelyObject, $decisionServiceMock); + + // Mock getVariationForFeature to return experiment/variation from a different feature + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_integer_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_integer_feature', 'control'); + $expected_decision = [ + 'experiment' => $experiment, + 'variation' => $variation + ]; + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with(Logger::INFO, + "Variable 'double_variable' is not used in variation 'control' returning default value '14.99'."); + + $this->assertSame( + $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), + '14.99'); + } + + + public function testGetFeatureVariableBooleanCaseTrue(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return 'true' + $map = [['boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean', 'true']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableBoolean('boolean_single_variable_feature', 'boolean_variable', 'user_id', []), + true + ); + } + + public function testGetFeatureVariableBooleanCaseFalse(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return any string but 'true' + $map = [['boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean', '14.33']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableBoolean('boolean_single_variable_feature', 'boolean_variable', 'user_id', []), + false + ); + } + + public function testGetFeatureVariableIntegerWhenCasted(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a numeric string + $map = [['integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer', '90']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableInteger('integer_single_variable_feature', 'integer_variable', 'user_id', []), + 90 + ); + } + + public function testGetFeatureVariableIntegerWhenNotCasted(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a non-numeric string + $map = [['integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer', 'abc90']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableInteger('integer_single_variable_feature', 'integer_variable', 'user_id', []), + null + ); + } + + public function testGetFeatureVariableDoubleWhenCasted(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a numeric string + $map = [['double_single_variable_feature', 'double_variable', 'user_id', [], 'double', '5.789']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('double_single_variable_feature', 'double_variable', 'user_id', [], 'double') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableDouble('double_single_variable_feature', 'double_variable', 'user_id', []), + 5.789 + ); + } + + public function testGetFeatureVariableDoubleWhenNotCasted(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a non-numeric string + $map = [['double_single_variable_feature', 'double_variable', 'user_id', [], 'double', 'abc5.789']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('double_single_variable_feature', 'double_variable', 'user_id', [], 'double') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableDouble('double_single_variable_feature', 'double_variable', 'user_id', []), + null + ); + } + + public function testGetFeatureVariableString(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + $map = [['string_single_variable_feature', 'string_variable', 'user_id', [], 'string', '59abc0p']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('string_single_variable_feature', 'string_variable', 'user_id', [], 'string') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableString('string_single_variable_feature', 'string_variable', 'user_id', []), + '59abc0p' + ); + } } From aead65039f67e0e6c20fa36a36a6b959ee0c795f Mon Sep 17 00:00:00 2001 From: Owais Date: Tue, 31 Oct 2017 16:05:09 +0500 Subject: [PATCH 10/20] :pen: Feature Flag Validator --- src/Optimizely/Optimizely.php | 5 +++++ src/Optimizely/Utils/Validator.php | 29 +++++++++++++++++++++++++++++ tests/OptimizelyTest.php | 19 +++++++++++++++++++ tests/UtilsTests/ValidatorTest.php | 27 +++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 4963352e..786e4e43 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -397,6 +397,11 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ return null; } + //validate feature flag + if(!Validator::isFeatureFlagValid($this->_config, $feature_flag)){ + return null; + } + $decision = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); if(!$decision){ $this->_logger->log(Logger::INFO,"Feature Flag '{$featureFlagKey}' is not enabled for user '{$userId}'."); diff --git a/src/Optimizely/Utils/Validator.php b/src/Optimizely/Utils/Validator.php index f86a69e8..14248b9e 100644 --- a/src/Optimizely/Utils/Validator.php +++ b/src/Optimizely/Utils/Validator.php @@ -105,4 +105,33 @@ public static function isUserInExperiment($config, $experiment, $userAttributes) return false; } + + /** + * Checks that if there are more than one experiment IDs + * in the feature flag, they must belong to the same mutex group + * + * @param ProjectConfig $config The project config to verify against + * @param FeatureFlag $featureFlag The feature to validate + * + * @return boolean True if feature flag is valid + */ + public static function isFeatureFlagValid($config, $featureFlag) + { + $experimentIds = $featureFlag->getExperimentIds(); + + if(empty($experimentIds)) + return true; + if(sizeof($experimentIds) == 1) + return true; + + $groupId = $config->getExperimentFromId($experimentIds[0])->getGroupId(); + foreach($experimentIds as $id){ + $experiment = $config->getExperimentFromId($id); + $grpId = $experiment->getGroupId(); + if($groupId != $grpId) + return false; + } + + return true; + } } diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index d6059715..960203fe 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -1778,6 +1778,25 @@ public function testIsFeatureEnabledGivenFeatureFlagNotFound(){ $this->assertSame($this->optimizelyObject->isFeatureEnabled($feature_key, "user_id"), null); } + public function testIsFeatureEnabledGivenInvalidFeatureFlag(){ + // Create local config copy for this method to add error + $projectConfig = new ProjectConfig($this->datafile, $this->loggerMock, new NoOpErrorHandler()); + $optimizelyObj = new Optimizely($this->datafile); + + $config = new \ReflectionProperty(Optimizely::class, '_config'); + $config->setAccessible(true); + $config->setValue($optimizelyObj, $projectConfig); + + $feature_flag = $projectConfig->getFeatureFlagFromKey('mutex_group_feature'); + // Add such an experiment to the list of experiment ids, that does not belong to the same mutex group + $experimentIds = $feature_flag->getExperimentIds(); + $experimentIds [] = '122241'; + $feature_flag->setExperimentIds($experimentIds); + + //should return null when feature flag is invalid + $this->assertSame($optimizelyObj->isFeatureEnabled('mutex_group_feature', "user_id"), null); + } + public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser(){ $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) diff --git a/tests/UtilsTests/ValidatorTest.php b/tests/UtilsTests/ValidatorTest.php index 7406deee..3ac558bd 100644 --- a/tests/UtilsTests/ValidatorTest.php +++ b/tests/UtilsTests/ValidatorTest.php @@ -170,4 +170,31 @@ public function testIsUserInExperimentAudienceNoMatch() ['device_type' => 'Android', 'location' => 'San Francisco'] )); } + + public function testIsFeatureFlagValid(){ + $config = new ProjectConfig(DATAFILE, new NoOpLogger(), new NoOpErrorHandler()); + $feature_flag_source = $config->getFeatureFlagFromKey('mutex_group_feature'); + + // should return true when no experiment ids exist + $feature_flag = clone $feature_flag_source; + $feature_flag->setExperimentIds([]); + $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), true); + + // should return true when only 1 experiment id exist + $feature_flag = clone $feature_flag_source; + $feature_flag->setExperimentIds([]); + $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), true); + + // should return true when more than 1 experiment ids exist that belong to the same group + $feature_flag = clone $feature_flag_source; + $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), true); + + //should return false when more than 1 experiment ids exist that belong to different group + $feature_flag = clone $feature_flag_source; + $experimentIds = $feature_flag->getExperimentIds(); + $experimentIds [] = '122241'; + $feature_flag->setExperimentIds($experimentIds); + + $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), false); + } } From 52f69608aeeb76f4b2e0c5f933ad834cb3269102 Mon Sep 17 00:00:00 2001 From: Owais Date: Wed, 1 Nov 2017 17:00:19 +0500 Subject: [PATCH 11/20] Merge Master # Conflicts: # src/Optimizely/Entity/Variation.php # src/Optimizely/ProjectConfig.php # src/Optimizely/Utils/Validator.php # tests/DecisionServiceTests/DecisionServiceTest.php # tests/ProjectConfigTest.php # tests/TestData.php --- composer.json | 3 +- src/Optimizely/Bucketer.php | 2 +- src/Optimizely/Entity/Variation.php | 5 +- src/Optimizely/Event/Builder/EventBuilder.php | 8 +- src/Optimizely/Event/Builder/Params.php | 6 +- src/Optimizely/ProjectConfig.php | 7 +- src/Optimizely/Utils/EventTagUtils.php | 14 +- src/Optimizely/Utils/Validator.php | 11 +- tests/BucketerTest.php | 10 +- .../DecisionServiceTest.php | 8 +- tests/EventTests/EventBuilderTest.php | 787 ++++++------------ tests/ProjectConfigTest.php | 3 +- tests/TestData.php | 8 +- tests/UtilsTests/EventTagUtilsTest.php | 6 +- 14 files changed, 309 insertions(+), 569 deletions(-) diff --git a/composer.json b/composer.json index 863daa63..7a672af0 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "justinrainbow/json-schema": "^1.6 || ^2.0 || ^4.0", "lastguest/murmurhash": "1.3.0", "guzzlehttp/guzzle": "~5.3|~6.2", - "monolog/monolog": "~1.21" + "monolog/monolog": "~1.21", + "icecave/parity": "^1.0" }, "require-dev": { "phpunit/phpunit": "~4.8|~5.0", diff --git a/src/Optimizely/Bucketer.php b/src/Optimizely/Bucketer.php index 8ee98cbd..51843b21 100644 --- a/src/Optimizely/Bucketer.php +++ b/src/Optimizely/Bucketer.php @@ -84,7 +84,7 @@ protected function generateBucketValue($bucketingKey) { $hashCode = $this->generateHashCode($bucketingKey); $ratio = $hashCode / Bucketer::$MAX_HASH_VALUE; - return floor($ratio * Bucketer::$MAX_TRAFFIC_VALUE); + return intval(floor($ratio * Bucketer::$MAX_TRAFFIC_VALUE)); } /** diff --git a/src/Optimizely/Entity/Variation.php b/src/Optimizely/Entity/Variation.php index 14813520..35556bb1 100644 --- a/src/Optimizely/Entity/Variation.php +++ b/src/Optimizely/Entity/Variation.php @@ -50,7 +50,7 @@ public function __construct($id = null, $key = null, $variableUsageInstances = [ $this->_id = $id; $this->_key = $key; - $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances,null,VariableUsage::class); + $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null, VariableUsage::class); if(!empty($this->_variableUsageInstances)){ foreach(array_values($this->_variableUsageInstances) as $variableUsage){ @@ -96,7 +96,8 @@ public function getVariables(){ } public function setVariables($variableUsageInstances){ - $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances,null,VariableUsage::class); + + $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null , VariableUsage::class); if(!empty($this->_variableUsageInstances)){ foreach(array_values($this->_variableUsageInstances) as $variableUsage){ diff --git a/src/Optimizely/Event/Builder/EventBuilder.php b/src/Optimizely/Event/Builder/EventBuilder.php index a706d3ce..576e8ef2 100644 --- a/src/Optimizely/Event/Builder/EventBuilder.php +++ b/src/Optimizely/Event/Builder/EventBuilder.php @@ -105,7 +105,7 @@ private function getCommonParams($config, $userId, $attributes) ENTITY_ID => $attributeEntity->getId(), KEY => $attributeKey, TYPE => CUSTOM_ATTRIBUTE_FEATURE_TYPE, - VALUE => $attributeValue, + VALUE => $attributeValue ]; } } @@ -139,8 +139,8 @@ private function getImpressionParams(Experiment $experiment, $variationId) [ ENTITY_ID => $experiment->getLayerId(), TIMESTAMP => time()*1000, - KEY => ACTIVATE_EVENT_KEY, - UUID => GeneratorUtils::getRandomUuid() + UUID => GeneratorUtils::getRandomUuid(), + KEY => ACTIVATE_EVENT_KEY ] ] @@ -169,7 +169,7 @@ private function getConversionParams($config, $eventKey, $experimentVariationMap $singleSnapshot[DECISIONS] = [ [ CAMPAIGN_ID => $experiment->getLayerId(), - EXPERIMENT_ID => $experimentId, + EXPERIMENT_ID => $experiment->getId(), VARIATION_ID => $variationId ] ]; diff --git a/src/Optimizely/Event/Builder/Params.php b/src/Optimizely/Event/Builder/Params.php index 0feff093..29a872a8 100644 --- a/src/Optimizely/Event/Builder/Params.php +++ b/src/Optimizely/Event/Builder/Params.php @@ -23,15 +23,15 @@ define('REVISION', 'revision'); define('EXPERIMENT_ID', 'experiment_id'); define('VARIATION_ID', 'variation_id'); -define('CAMPAIGN_ID','campaign_id'); +define('CAMPAIGN_ID', 'campaign_id'); define('VISITOR_ID', 'visitor_id'); define('DECISIONS', 'decisions'); define('EVENTS', 'events');; define('CLIENT_ENGINE', 'client_name'); define('CLIENT_VERSION', 'client_version'); define('CUSTOM_ATTRIBUTE_FEATURE_TYPE','custom'); -define('ACTIVATE_EVENT_KEY','campaign_activated'); -define('SNAPSHOTS','snapshots'); +define('ACTIVATE_EVENT_KEY', 'campaign_activated'); +define('SNAPSHOTS', 'snapshots'); define('ATTRIBUTES', 'attributes'); define('KEY', 'key'); define('TYPE', 'type'); diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index 81585dd9..8c9b7466 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -309,8 +309,9 @@ public function getExperimentFromId($experimentId) } /** - * @param String $featureKey - * @return FeatureFlag + * @param String $featureKey Key of the feature flag + * + * @return FeatureFlag Entity corresponding to the key. */ public function getFeatureFlagFromKey($featureKey) { @@ -325,8 +326,8 @@ public function getFeatureFlagFromKey($featureKey) /** * @param String $rolloutId - * @return Rollout * + * @return Rollout */ public function getRolloutFromId($rolloutId) { diff --git a/src/Optimizely/Utils/EventTagUtils.php b/src/Optimizely/Utils/EventTagUtils.php index 6f020679..42bc62b6 100644 --- a/src/Optimizely/Utils/EventTagUtils.php +++ b/src/Optimizely/Utils/EventTagUtils.php @@ -43,7 +43,7 @@ public static function getRevenueValue($eventTags) { return null; } - if (!isset($eventTags[self::REVENUE_EVENT_METRIC_NAME]) or !$eventTags[self::REVENUE_EVENT_METRIC_NAME]) { + if (!isset($eventTags[self::REVENUE_EVENT_METRIC_NAME])) { return null; } @@ -65,30 +65,30 @@ public static function getNumericValue($eventTags, LoggerInterface $logger = nul if (!$eventTags) { if($logger) - $logger->log(Logger::DEBUG,"Event tags is undefined."); + $logger->log(Logger::DEBUG, "Event tags is undefined."); return null; } else if (!is_array($eventTags)) { if($logger) - $logger->log(Logger::DEBUG,"Event tags is not a dictionary."); + $logger->log(Logger::DEBUG, "Event tags is not a dictionary."); return null; } else if (!isset($eventTags[self::NUMERIC_EVENT_METRIC_NAME])) { if($logger) - $logger->log(Logger::DEBUG,"The numeric metric key is not defined in the event tags or is null."); + $logger->log(Logger::DEBUG, "The numeric metric key is not defined in the event tags or is null."); return null; } else if (!is_numeric($eventTags[self::NUMERIC_EVENT_METRIC_NAME])) { if($logger) - $logger->log(Logger::DEBUG,"Numeric metric value is not an integer or float, or is not a numeric string."); + $logger->log(Logger::DEBUG, "Numeric metric value is not an integer or float, or is not a numeric string."); return null; } else if(is_nan($eventTags[self::NUMERIC_EVENT_METRIC_NAME]) || is_infinite(floatval($eventTags[self::NUMERIC_EVENT_METRIC_NAME]))){ if($logger) - $logger->log(Logger::DEBUG,"Provided numeric value is in an invalid format."); + $logger->log(Logger::DEBUG, "Provided numeric value is in an invalid format."); return null; } $rawValue = $eventTags[self::NUMERIC_EVENT_METRIC_NAME]; // # Log the final numeric metric value if($logger){ - $logger->log(Logger::INFO,"The numeric metric value {$rawValue} will be sent to results."); + $logger->log(Logger::INFO, "The numeric metric value {$rawValue} will be sent to results."); } return floatval($rawValue); diff --git a/src/Optimizely/Utils/Validator.php b/src/Optimizely/Utils/Validator.php index f86a69e8..1e892c45 100644 --- a/src/Optimizely/Utils/Validator.php +++ b/src/Optimizely/Utils/Validator.php @@ -17,11 +17,10 @@ namespace Optimizely\Utils; use JsonSchema; +use Monolog\Logger; use Optimizely\Entity\Experiment; -use Optimizely\ProjectConfig; use Optimizely\Logger\LoggerInterface; -use Monolog\Logger; - +use Optimizely\ProjectConfig; class Validator { @@ -42,10 +41,10 @@ public static function validateJsonSchema($datafile, LoggerInterface $logger = n return true; } else { if($logger){ - $logger->log(Logger::DEBUG,"JSON does not validate. Violations:\n");; + $logger->log(Logger::DEBUG, "JSON does not validate. Violations:\n");; foreach ($validator->getErrors() as $error) { - $logger->log(Logger::DEBUG,"[%s] %s\n", $error['property'], $error['message']); - } + $logger->log(Logger::DEBUG, "[%s] %s\n", $error['property'], $error['message']); + } } return false; diff --git a/tests/BucketerTest.php b/tests/BucketerTest.php index 93f84b6f..2dad22ae 100644 --- a/tests/BucketerTest.php +++ b/tests/BucketerTest.php @@ -61,23 +61,23 @@ public function testGenerateBucketValue() $generateBucketValueMethod = new \ReflectionMethod(Bucketer::class, 'generateBucketValue'); $generateBucketValueMethod->setAccessible(true); - $this->assertEquals( + $this->assertSame( 5254, $generateBucketValueMethod->invoke(new Bucketer($this->loggerMock), $this->getBucketingKey('ppid1', '1886780721')) ); - $this->assertEquals( + $this->assertSame( 4299, $generateBucketValueMethod->invoke(new Bucketer($this->loggerMock), $this->getBucketingKey('ppid2', '1886780721')) ); - $this->assertEquals( + $this->assertSame( 2434, $generateBucketValueMethod->invoke(new Bucketer($this->loggerMock), $this->getBucketingKey('ppid2', '1886780722')) ); - $this->assertEquals( + $this->assertSame( 5439, $generateBucketValueMethod->invoke(new Bucketer($this->loggerMock), $this->getBucketingKey('ppid3', '1886780721')) ); - $this->assertEquals( + $this->assertSame( 6128, $generateBucketValueMethod->invoke( new Bucketer($this->loggerMock), diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 1ddb7e9b..0a4f1e2f 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -147,7 +147,7 @@ public function testGetVariationReturnsWhitelistedVariation() public function testGetVariationReturnsWhitelistedVariationForGroupedExperiment() { - $expectedVariation = new Variation('7722260071', 'group_exp_1_var_1',[ + $expectedVariation = new Variation('7722260071', 'group_exp_1_var_1', [ [ "id" => "155563", "value" => "groupie_1_v1" @@ -567,15 +567,15 @@ public function testGetVariationWithBucketingId() // check invalid audience with bucketing ID $variationKey = $optlyObject->getVariation($this->experimentKey, $userId, $invalidUserAttributesWithBucketingId); - $this->assertEquals(null, $variationKey); + $this->assertNull($variationKey); // check null audience with bucketing Id $variationKey = $optlyObject->getVariation($this->experimentKey, $userId, null); - $this->assertEquals(null, $variationKey); + $this->assertNull($variationKey); // test that an experiment that's not running returns a null variation $variationKey = $optlyObject->getVariation($pausedExperimentKey, $userId, $userAttributesWithBucketingId); - $this->assertEquals(null, $variationKey); + $this->assertNull($variationKey); // check forced variation $this->assertTrue($optlyObject->setForcedVariation($this->experimentKey, $userId, $this->variationKeyControl), sprintf('Set variation to "%s" failed.', $this->variationKeyControl)); diff --git a/tests/EventTests/EventBuilderTest.php b/tests/EventTests/EventBuilderTest.php index 91e79017..12f0ab40 100644 --- a/tests/EventTests/EventBuilderTest.php +++ b/tests/EventTests/EventBuilderTest.php @@ -16,6 +16,9 @@ */ namespace Optimizely\Tests; + +use Icecave\Parity\Parity; +use SebastianBergmann\Diff\Differ; use Optimizely\Bucketer; use Optimizely\DecisionService\DecisionService; use Optimizely\ErrorHandler\NoOpErrorHandler; @@ -41,6 +44,52 @@ public function setUp() $this->eventBuilder = new EventBuilder(); $this->timestamp = time()*1000; $this->uuid = 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c'; + $this->differ = new Differ; + + $this->expectedEventUrl = 'https://logx.optimizely.com/v1/events'; + $this->expectedEventParams = [ + 'account_id' => '1592310167', + 'project_id' => '7720880029', + 'visitors'=> [[ + 'snapshots'=> [[ + 'decisions'=> [[ + 'campaign_id'=> '7719770039', + 'experiment_id'=> '7716830082', + 'variation_id'=> '7721010009' + ]], + 'events'=> [[ + 'entity_id'=> '7719770039', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'campaign_activated' + ]] + ]], + 'visitor_id'=> 'testUserId', + 'attributes'=> [] + ]], + 'revision' => '15', + 'client_name' => 'php-sdk', + 'client_version' => '1.4.0', + 'anonymize_ip'=> false, + ]; + $this->expectedEventHttpVerb = 'POST'; + $this->expectedEventHeaders = ['Content-Type' => 'application/json']; + } + + /** + * Performs Deep Strict Comparison of two objects + * @param LogEvent $e1 + * @param LogEvent $e2 + * @return [Boolean,String] [True,""] when equal, otherwise [False,"diff-string"] + */ + private function areLogEventsEqual($e1,$e2){ + $msg = ""; + $isEqual = Parity::isEqualTo($e1,$e2); + if(!$isEqual){ + $msg = $this->differ->diff(var_export($e1,true),var_export($e2,true)); + } + + return [$isEqual,$msg]; } /** @@ -61,37 +110,9 @@ private function fakeParamsToReconcile($logE){ public function testCreateImpressionEventNoAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7721010009', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7719770039', - 'uuid'=> $this->uuid, - 'key'=> 'campaign_activated' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); + $logEvent = $this->eventBuilder->createImpressionEvent( $this->config, 'test_experiment', @@ -99,50 +120,22 @@ public function testCreateImpressionEventNoAttributesNoValue() $this->testUserId, null ); - + $logEvent = $this->fakeParamsToReconcile($logEvent); - - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($this->expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateImpressionEventWithAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => 'iPhone', - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7721010009', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7719770039', - 'uuid'=> $this->uuid, - 'key'=> 'campaign_activated' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['attributes'][] = + [ 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 'iPhone', + ]; + $this->expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'device_type' => 'iPhone', @@ -157,7 +150,8 @@ public function testCreateImpressionEventWithAttributesNoValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($this->expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } /** @@ -165,45 +159,16 @@ public function testCreateImpressionEventWithAttributesNoValue() */ public function testCreateImpressionEventWithFalseAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => false, - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7721010009', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7719770039', - 'uuid'=> $this->uuid, - 'key'=> 'campaign_activated' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); - + $this->expectedEventParams['visitors'][0]['attributes'][] = + [ 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 'false', + ]; + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ - 'device_type' => false, + 'device_type' => 'false', 'company' => 'Optimizely' ]; $logEvent = $this->eventBuilder->createImpressionEvent( @@ -215,7 +180,8 @@ public function testCreateImpressionEventWithFalseAttributesNoValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } /** @@ -223,42 +189,14 @@ public function testCreateImpressionEventWithFalseAttributesNoValue() */ public function testCreateImpressionEventWithZeroAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => 0, - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7721010009', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7719770039', - 'uuid'=> $this->uuid, - 'key'=> 'campaign_activated' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['attributes'][] = + [ 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 0, + ]; + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'device_type' => 0, @@ -273,7 +211,8 @@ public function testCreateImpressionEventWithZeroAttributesNoValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } /** @@ -281,37 +220,8 @@ public function testCreateImpressionEventWithZeroAttributesNoValue() */ public function testCreateImpressionEventWithInvalidAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7721010009', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7719770039', - 'uuid'=> $this->uuid, - 'key'=> 'campaign_activated' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'invalid_attribute' => 'sorry_not_sorry' @@ -325,42 +235,22 @@ public function testCreateImpressionEventWithInvalidAttributesNoValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateConversionEventNoAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7721010009', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase' + ]; + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); + $logEvent = $this->eventBuilder->createConversionEvent( $this->config, 'purchase', @@ -370,47 +260,29 @@ public function testCreateConversionEventNoAttributesNoValue() null ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateConversionEventWithAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => 'iPhone' - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7722370027', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['attributes'][] = + [ 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 'iPhone', + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase' + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['decisions'][0]['variation_id'] = '7722370027'; + + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'device_type' => 'iPhone', @@ -426,43 +298,24 @@ public function testCreateConversionEventWithAttributesNoValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } //Should not fill in userFeatures for getConversion when attribute is not in the datafile public function testCreateConversionEventInvalidAttributesNoValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7722370027', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase' + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['decisions'][0]['variation_id'] = '7722370027'; + + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'invalid_attribute'=> 'sorry_not_sorry' @@ -477,49 +330,30 @@ public function testCreateConversionEventInvalidAttributesNoValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateConversionEventNoAttributesWithValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7722370027', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase', - 'revenue' => 42, - 'value'=> 13.37, - 'tags' => [ - 'revenue' => 42, - 'value' => '13.37' - ] - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); - + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase', + 'revenue' => 42, + 'value'=> 13.37, + 'tags' => [ + 'revenue' => 42, + 'value' => '13.37' + ] + ]; + + $this->expectedEventParams['visitors'][0]['snapshots'][0]['decisions'][0]['variation_id'] = '7722370027'; + + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $logEvent = $this->eventBuilder->createConversionEvent( $this->config, 'purchase', @@ -530,55 +364,37 @@ public function testCreateConversionEventNoAttributesWithValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateConversionEventWithAttributesWithValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => 'iPhone' - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7722370027', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase', - 'revenue' => 42, - 'value' => 13.37, - 'tags' => [ - 'revenue' => 42, - 'non-revenue' => 'definitely', - 'value'=> '13.37' - ] - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['attributes'][] = + [ 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 'iPhone', + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase', + 'revenue' => 42, + 'value'=> 13.37, + 'tags' => [ + 'revenue' => 42, + 'non-revenue' => 'definitely', + 'value' => '13.37' + ] + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['decisions'][0]['variation_id'] = '7722370027'; + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); + $userAttributes = [ 'device_type' => 'iPhone', 'company' => 'Optimizely' @@ -597,52 +413,33 @@ public function testCreateConversionEventWithAttributesWithValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateConversionEventWithAttributesWithNumericTag() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => 'iPhone' - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7722370027', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase', - 'value' => 13.37, - 'tags' => [ - 'value'=> '13.37' - ] - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['attributes'][] = + [ 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 'iPhone', + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase', + 'value'=> 13.37, + 'tags' => [ + 'value' => '13.37' + ] + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['decisions'][0]['variation_id'] = '7722370027'; + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'device_type' => 'iPhone', 'company' => 'Optimizely' @@ -659,47 +456,28 @@ public function testCreateConversionEventWithAttributesWithNumericTag() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateConversionEventNoAttributesWithInvalidValue() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7722370027', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase', - 'tags' => [ - 'revenue' => '42', - 'non-revenue' => 'definitely', - 'value' => 'invalid value' - ] - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase', + 'tags' => [ + 'revenue' => '42', + 'non-revenue' => 'definitely', + 'value' => 'invalid value' + ] + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['decisions'][0]['variation_id'] = '7722370027'; + + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $logEvent = $this->eventBuilder->createConversionEvent( $this->config, @@ -715,54 +493,29 @@ public function testCreateConversionEventNoAttributesWithInvalidValue() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } // Bucketing ID should be part of the impression event sans the ID // (since this custom attribute entity is not generated by GAE) public function testCreateImpressionEventWithBucketingIDAttribute() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => 'iPhone', - ],[ - 'entity_id' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID, - 'key' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY, - 'type' => 'custom', - 'value' => 'variation', - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7721010009', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7719770039', - 'uuid'=> $this->uuid, - 'key'=> 'campaign_activated' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['attributes'] = + [[ + 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 'iPhone', + ],[ + 'entity_id' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID, + 'key' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY, + 'type' => 'custom', + 'value' => 'variation', + ]]; + + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'device_type' => 'iPhone', @@ -779,52 +532,35 @@ public function testCreateImpressionEventWithBucketingIDAttribute() $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } public function testCreateConversionEventWithBucketingIDAttribute() { - $expectedLogEvent = new LogEvent( - 'https://logx.optimizely.com/v1/events', - [ - 'project_id' => '7720880029', - 'account_id' => '1592310167', - 'anonymize_ip'=> false, - 'revision' => '15', - 'client_name' => 'php-sdk', - 'client_version' => '1.4.0', - 'visitors'=> [[ - 'attributes'=> [[ - 'entity_id' => '7723280020', - 'key' => 'device_type', - 'type' => 'custom', - 'value' => 'iPhone', - ],[ - 'entity_id' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID, - 'key' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY, - 'type' => 'custom', - 'value' => 'variation', - ]], - 'visitor_id'=> 'testUserId', - 'snapshots'=> [[ - 'decisions'=> [[ - 'variation_id'=> '7722370027', - 'experiment_id'=> '7716830082', - 'campaign_id'=> '7719770039' - ]], - 'events'=> [[ - 'timestamp'=> $this->timestamp, - 'entity_id'=> '7718020063', - 'uuid'=> $this->uuid, - 'key'=> 'purchase' - ] - ]] - ]], - ] - ], - 'POST', - ['Content-Type' => 'application/json'] - ); + $this->expectedEventParams['visitors'][0]['attributes'] = + [[ + 'entity_id' => '7723280020', + 'key' => 'device_type', + 'type' => 'custom', + 'value' => 'iPhone', + ],[ + 'entity_id' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID, + 'key' => RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY, + 'type' => 'custom', + 'value' => 'variation', + ]]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['events'][0] = + [ + 'entity_id'=> '7718020063', + 'timestamp'=> $this->timestamp, + 'uuid'=> $this->uuid, + 'key'=> 'purchase' + ]; + $this->expectedEventParams['visitors'][0]['snapshots'][0]['decisions'][0]['variation_id'] = '7722370027'; + + $expectedLogEvent = new LogEvent($this->expectedEventUrl,$this->expectedEventParams, + $this->expectedEventHttpVerb,$this->expectedEventHeaders); $userAttributes = [ 'device_type' => 'iPhone', @@ -841,6 +577,7 @@ public function testCreateConversionEventWithBucketingIDAttribute() ); $logEvent = $this->fakeParamsToReconcile($logEvent); - $this->assertEquals($expectedLogEvent, $logEvent); + $result = $this->areLogEventsEqual($expectedLogEvent, $logEvent); + $this->assertTrue($result[0], $result[1]); } } diff --git a/tests/ProjectConfigTest.php b/tests/ProjectConfigTest.php index 4d6e130a..d689dade 100644 --- a/tests/ProjectConfigTest.php +++ b/tests/ProjectConfigTest.php @@ -139,7 +139,7 @@ public function testInit() $audienceIdMap->setAccessible(true); $this->assertEquals([ '7718080042' => $this->config->getAudience('7718080042'), - '11154' => $this->config->getAudience('11154') + '11155' => $this->config->getAudience('11155') ], $audienceIdMap->getValue($this->config)); // Check variation key map @@ -347,6 +347,7 @@ public function testGetRolloutInvalidId() $this->loggerMock->expects($this->once()) ->method('log') ->with(Logger::ERROR, 'Rollout with ID "42" is not in the datafile.'); + $this->errorHandlerMock->expects($this->once()) ->method('handleError') ->with(new InvalidRolloutException('Provided rollout is not in datafile.')); diff --git a/tests/TestData.php b/tests/TestData.php index 6aea300c..0559b072 100644 --- a/tests/TestData.php +++ b/tests/TestData.php @@ -96,7 +96,7 @@ "status": "Running", "layerId": "4", "audienceIds": [ - "11154" + "11155" ], "id": "122230", "forcedVariations": { @@ -322,7 +322,7 @@ { "name": "Chrome users", "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", \"type\": \"custom_attribute\", \"value\": \"chrome\"}]]]", - "id": "11154" + "id": "11155" } ], "groups": [ @@ -599,7 +599,7 @@ "status": "Running", "layerId": "166660", "audienceIds": [ - "11154" + "11155" ], "variations": [ { @@ -685,7 +685,7 @@ "status": "Running", "layerId": "166661", "audienceIds": [ - "11154" + "11155" ], "variations": [ { diff --git a/tests/UtilsTests/EventTagUtilsTest.php b/tests/UtilsTests/EventTagUtilsTest.php index 6813971f..8cf88075 100644 --- a/tests/UtilsTests/EventTagUtilsTest.php +++ b/tests/UtilsTests/EventTagUtilsTest.php @@ -60,9 +60,9 @@ public function testGetRevenueValueWithInvalidRevenueTag() { } public function testGetRevenueValueWithRevenueTag() { - $this->assertEquals(65536, EventTagUtils::getRevenueValue(array('revenue' => 65536))); - $this->assertEquals(9223372036854775807, EventTagUtils::getRevenueValue(array('revenue' => 9223372036854775807))); - $this->assertEquals(0, EventTagUtils::getRevenueValue(array('revenue' => 0))); + $this->assertSame(65536, EventTagUtils::getRevenueValue(array('revenue' => 65536))); + $this->assertSame(9223372036854775807, EventTagUtils::getRevenueValue(array('revenue' => 9223372036854775807))); + $this->assertSame(0, EventTagUtils::getRevenueValue(array('revenue' => 0))); } public function testGetNumericValueWithUndefinedTags() { From 9b841f1e9370013997981fe1871f07c765b8522d Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 2 Nov 2017 00:47:30 +0500 Subject: [PATCH 12/20] :pen: Added comments --- src/Optimizely/Entity/Variation.php | 24 +++++++-- src/Optimizely/Optimizely.php | 62 +++++++++++++++++++++- src/Optimizely/ProjectConfig.php | 11 ++++ src/Optimizely/Utils/VariableTypeUtils.php | 2 +- tests/OptimizelyTest.php | 8 ++- tests/UtilsTests/ValidatorTest.php | 16 +++--- tests/UtilsTests/VariableTypeUtilsTest.php | 16 +++--- 7 files changed, 114 insertions(+), 25 deletions(-) diff --git a/src/Optimizely/Entity/Variation.php b/src/Optimizely/Entity/Variation.php index 4fc4a458..88b408dc 100644 --- a/src/Optimizely/Entity/Variation.php +++ b/src/Optimizely/Entity/Variation.php @@ -52,11 +52,7 @@ public function __construct($id = null, $key = null, $variableUsageInstances = [ $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null, VariableUsage::class); - if(!empty($this->_variableUsageInstances)){ - foreach(array_values($this->_variableUsageInstances) as $variableUsage){ - $this->_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; - } - } + $this->generateVariableIdToVariableUsageMap(); } /** @@ -91,10 +87,18 @@ public function setKey($key) $this->_key = $key; } + /** + * @return [VariableUsage] Variable usage instances in this variation + */ public function getVariables(){ return $this->_variableUsageInstances; } + /** + * @param string Variable ID + * + * @return VariableUsage Variable usage instance corresponding to given variable ID + */ public function getVariableUsageById($variableId){ if(isset($this->_variableIdToVariableUsageInstanceMap[$variableId])) return $this->_variableIdToVariableUsageInstanceMap[$variableId]; @@ -102,9 +106,19 @@ public function getVariableUsageById($variableId){ return null; } + /** + * @param [VariableUsage] array of variable usage instances + */ public function setVariables($variableUsageInstances){ $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null , VariableUsage::class); + $this->generateVariableIdToVariableUsageMap(); + } + /** + * Generates variable ID to Variable usage instance map + * from variable usage instances + */ + private function generateVariableIdToVariableUsageMap(){ if(!empty($this->_variableUsageInstances)){ foreach(array_values($this->_variableUsageInstances) as $variableUsage){ $this->_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 786e4e43..bb2e6542 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -373,7 +373,15 @@ public function getForcedVariation($experimentKey, $userId) } } - + /** + * Determine whether a feature is enabled. + * Sends an impression event if the user is bucketed into an experiment using the feature. + * @param string Feature flag key + * @param string User ID + * @param array Associative array of user attributes + * + * @return boolean + */ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ if (!$this->_isValid) { @@ -421,6 +429,16 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ return true; } + /** + * Get the string value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * @param string Variable type + * + * @return string Feature variable value / null + */ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $userId, $attributes = null, $variableType = null) { @@ -481,6 +499,15 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u return $variable_value; } + /** + * Get the Boolean value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string boolean variable value / null + */ public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId, $attributes = null){ $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::BOOLEAN_TYPE); @@ -491,6 +518,15 @@ public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId return $variable_value; } + /** + * Get the Integer value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string integer variable value / null + */ public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId, $attributes = null){ $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::INTEGER_TYPE); @@ -501,6 +537,15 @@ public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId return $variable_value; } + /** + * Get the Double value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string double variable value / null + */ public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, $attributes = null){ $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::DOUBLE_TYPE); @@ -511,6 +556,15 @@ public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, return $variable_value; } + /** + * Get the String value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string variable value / null + */ public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, $attributes = null){ $variable_value = $this->getFeatureVariableValueForType( $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::STRING_TYPE); @@ -518,6 +572,12 @@ public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, return $variable_value; } + /** + * @param string Experiment key + * @param string Variation key + * @param string User ID + * @param array Associative array of user attributes + */ public function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes){ $impressionEvent = $this->_eventBuilder ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index 0794905c..085aa966 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -156,6 +156,10 @@ class ProjectConfig */ private $_rolloutIdMap; + /** + * Feature Flag key to Feature Variable key to Feature Variable map + * @var > + */ private $_featureFlagVariableMap; /** @@ -443,6 +447,13 @@ public function getVariationFromId($experimentKey, $variationId) return new Variation(); } + /** + * Gets the feature variable instance given feature flag key and variable key + * @param string Feature flag key + * @param string Variable key + * + * @return FeatureVariable / null + */ public function getFeatureVariableFromKey($featureFlagKey, $variableKey) { $feature_flag = $this->getFeatureFlagFromKey($featureFlagKey); diff --git a/src/Optimizely/Utils/VariableTypeUtils.php b/src/Optimizely/Utils/VariableTypeUtils.php index 4a4a18cd..9a5888d7 100644 --- a/src/Optimizely/Utils/VariableTypeUtils.php +++ b/src/Optimizely/Utils/VariableTypeUtils.php @@ -36,7 +36,7 @@ public static function castStringToType($value, $variableType, LoggerInterface $ break; case FeatureVariable::INTEGER_TYPE : - if(is_numeric($value)){ + if(ctype_digit($value)){ $return_value = (int) $value; } break; diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 960203fe..49a85f6c 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -1798,12 +1798,12 @@ public function testIsFeatureEnabledGivenInvalidFeatureFlag(){ } public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser(){ + // should return false when no variation is returned for user $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) ->getMock(); - // should return false when no variation is returned for user $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) ->setMethods(array('getVariationForFeature')) @@ -1813,10 +1813,12 @@ public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser(){ $decisionService->setAccessible(true); $decisionService->setValue($optimizelyMock, $decisionServiceMock); + // mock getVariationForFeature to return null $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') ->will($this->returnValue(null)); + // assert that impression event is not sent $optimizelyMock->expects($this->never()) ->method('sendImpressionEvent'); @@ -1829,7 +1831,7 @@ public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser(){ false); } - public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserBeingExperimented(){ + public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimented(){ $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) @@ -1854,6 +1856,7 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserBeingExperim ->method('getVariationForFeature') ->will($this->returnValue($expected_decision)); + // assert that sendImpressionEvent is called with expected params $optimizelyMock->expects($this->exactly(1)) ->method('sendImpressionEvent') ->with('test_experiment_double_feature', 'control', 'user_id', []); @@ -1895,6 +1898,7 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx ->method('getVariationForFeature') ->will($this->returnValue($expected_decision)); + // assert that sendImpressionEvent is not called $optimizelyMock->expects($this->never()) ->method('sendImpressionEvent'); diff --git a/tests/UtilsTests/ValidatorTest.php b/tests/UtilsTests/ValidatorTest.php index 3ac558bd..64c23fda 100644 --- a/tests/UtilsTests/ValidatorTest.php +++ b/tests/UtilsTests/ValidatorTest.php @@ -178,23 +178,23 @@ public function testIsFeatureFlagValid(){ // should return true when no experiment ids exist $feature_flag = clone $feature_flag_source; $feature_flag->setExperimentIds([]); - $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), true); + $this->assertTrue(Validator::isFeatureFlagValid($config, $feature_flag)); - // should return true when only 1 experiment id exist + // should return true when only one experiment id exists $feature_flag = clone $feature_flag_source; - $feature_flag->setExperimentIds([]); - $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), true); + $feature_flag->setExperimentIds(['122241']); + $this->assertTrue(Validator::isFeatureFlagValid($config, $feature_flag)); - // should return true when more than 1 experiment ids exist that belong to the same group + // should return true when more than one experiment ids exist that belong to the same group $feature_flag = clone $feature_flag_source; - $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), true); + $this->assertTrue(Validator::isFeatureFlagValid($config, $feature_flag)); - //should return false when more than 1 experiment ids exist that belong to different group + //should return false when more than one experiment ids exist that belong to different group $feature_flag = clone $feature_flag_source; $experimentIds = $feature_flag->getExperimentIds(); $experimentIds [] = '122241'; $feature_flag->setExperimentIds($experimentIds); - $this->assertSame(Validator::isFeatureFlagValid($config, $feature_flag), false); + $this->assertFalse(Validator::isFeatureFlagValid($config, $feature_flag)); } } diff --git a/tests/UtilsTests/VariableTypeUtilsTest.php b/tests/UtilsTests/VariableTypeUtilsTest.php index 41c31537..ab6a25e4 100644 --- a/tests/UtilsTests/VariableTypeUtilsTest.php +++ b/tests/UtilsTests/VariableTypeUtilsTest.php @@ -38,25 +38,25 @@ protected function setUp() } public function testValueCastingToBoolean(){ - $this->assertSame($this->variableUtilObj->castStringToType('true', 'boolean'), true); - $this->assertSame($this->variableUtilObj->castStringToType('True', 'boolean'), true); - $this->assertSame($this->variableUtilObj->castStringToType('false', 'boolean'), false); - $this->assertSame($this->variableUtilObj->castStringToType('somestring', 'boolean'), false); + $this->assertTrue($this->variableUtilObj->castStringToType('true', 'boolean')); + $this->assertTrue($this->variableUtilObj->castStringToType('True', 'boolean')); + $this->assertFalse($this->variableUtilObj->castStringToType('false', 'boolean')); + $this->assertFalse($this->variableUtilObj->castStringToType('somestring', 'boolean')); } public function testValueCastingToInteger(){ $this->assertSame($this->variableUtilObj->castStringToType('1000', 'integer'), 1000); $this->assertSame($this->variableUtilObj->castStringToType('123', 'integer'), 123); - // should return nil and log a message if value can not be casted to an integer - $value = 'any-non-numeric-string'; + // should return nulll and log a message if value can not be casted to an integer + $value = '123.5'; // any string with non-decimal digits $type = 'integer'; $this->loggerMock->expects($this->exactly(1)) ->method('log') ->with(Logger::ERROR, "Unable to cast variable value '{$value}' to type '{$type}'."); - $this->assertSame($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock), null); + $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); } public function testValueCastingToDouble(){ @@ -72,7 +72,7 @@ public function testValueCastingToDouble(){ ->with(Logger::ERROR, "Unable to cast variable value '{$value}' to type '{$type}'."); - $this->assertSame($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock), null); + $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); } public function testValueCastingToString(){ From 0e955c54bf1b9310226c2e620d7036d3f81cfe1a Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 2 Nov 2017 19:04:39 +0500 Subject: [PATCH 13/20] :pen: Fixed a bug due to change in decision logic --- src/Optimizely/Optimizely.php | 16 +++++++++++----- tests/OptimizelyTest.php | 32 ++++++++++++++------------------ 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index bb2e6542..e1d5edf6 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -21,6 +21,7 @@ use Optimizely\Exceptions\InvalidEventTagException; use Throwable; use Monolog\Logger; +use Optimizely\DecisionService\Decision; use Optimizely\DecisionService\DecisionService; use Optimizely\Entity\Experiment; use Optimizely\Entity\FeatureFlag; @@ -416,11 +417,13 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ return false; } - if($decision["experiment"]){ - $experiment_key = $decision["experiment"]->getKey(); - $variation_key = $decision["variation"]->getKey(); + if($decision->getSource() == Decision::DECISION_SOURCE_EXPERIMENT){ + $experiment_id = $decision->getExperimentId(); + $variation_id = $decision->getVariationId(); + $experiment = $this->_config->getExperimentFromId($experiment_id); + $variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id); - $this->sendImpressionEvent($experiment_key, $variation_key, $userId, $attributes); + $this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes); } else { $this->_logger->log(Logger::INFO,"The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); } @@ -482,7 +485,10 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u $this->_logger->log(Logger::INFO,"User '{$userId}'is not in any variation, ". "returning default value '{$variable_value}'."); } else { - $variation = $decision['variation']; + $experiment_id = $decision->getExperimentId(); + $variation_id = $decision->getVariationId(); + $experiment = $this->_config->getExperimentFromId($experiment_id); + $variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id); $variable_usage = $variation->getVariableUsageById($variable->getId()); if($variable_usage){ $variable_value = $variable_usage->getValue(); diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 49a85f6c..ce46a93f 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -18,6 +18,7 @@ use Exception; use Monolog\Logger; +use Optimizely\DecisionService\Decision; use Optimizely\DecisionService\DecisionService; use Optimizely\ErrorHandler\NoOpErrorHandler; use Optimizely\Event\LogEvent; @@ -1847,10 +1848,11 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper $decisionService->setValue($optimizelyMock, $decisionServiceMock); // Mock getVariationForFeature to return a valid decision with experiment and variation keys - $expected_decision = [ - 'experiment' => $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'), - 'variation' => $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control') - ]; + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new Decision( + $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -1889,10 +1891,8 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx $rollout = $this->projectConfig->getRolloutFromId('166660'); $experiment = $rollout->getExperiments()[0]; $variation = $experiment->getVariations()[0]; - $expected_decision = [ - 'experiment' => null, - 'variation' => $variation - ]; + $expected_decision = new Decision( + $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -2030,7 +2030,7 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledFo '14.99'); } - public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleIsInVariation(){ + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableIsInVariation(){ // should return specific value $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) @@ -2043,10 +2043,8 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - $expected_decision = [ - 'experiment' => $experiment, - 'variation' => $variation - ]; + $expected_decision = new Decision( + $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -2063,7 +2061,7 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs '42.42'); } - public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVaribaleNotInVariation(){ + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableNotInVariation(){ // should return default value $decisionServiceMock = $this->getMockBuilder(DecisionService::class) @@ -2078,10 +2076,8 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs // Mock getVariationForFeature to return experiment/variation from a different feature $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_integer_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_integer_feature', 'control'); - $expected_decision = [ - 'experiment' => $experiment, - 'variation' => $variation - ]; + $expected_decision = new Decision( + $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') From 2abf1adbf9e25e9cfdcbb90af02431b41a19d71a Mon Sep 17 00:00:00 2001 From: Owais Date: Tue, 7 Nov 2017 17:57:25 +0500 Subject: [PATCH 14/20] :pen: Styling issues addressed and minor refactoring --- src/Optimizely/DecisionService/Decision.php | 82 ++--- .../DecisionService/DecisionService.php | 312 ++++++++++-------- .../DecisionServiceTest.php | 16 +- 3 files changed, 217 insertions(+), 193 deletions(-) diff --git a/src/Optimizely/DecisionService/Decision.php b/src/Optimizely/DecisionService/Decision.php index f25fffe0..6782f923 100644 --- a/src/Optimizely/DecisionService/Decision.php +++ b/src/Optimizely/DecisionService/Decision.php @@ -18,51 +18,51 @@ class Decision { - const DECISION_SOURCE_EXPERIMENT = 'experiment'; - const DECISION_SOURCE_ROLLOUT = 'rollout'; + const DECISION_SOURCE_EXPERIMENT = 'experiment'; + const DECISION_SOURCE_ROLLOUT = 'rollout'; - /** - * @var string The ID experiment in this decision. - */ - private $_experimentId; + /** + * @var string The ID experiment in this decision. + */ + private $_experimentId; - /** - * @var string The ID variation in this decision. - */ - private $_variationId; + /** + * @var string The ID variation in this decision. + */ + private $_variationId; - /** - * The source of the decision. Either DECISION_SOURCE_EXPERIMENT or DECISION_SOURCE_ROLLOUT - * @var string - */ - private $_source; + /** + * The source of the decision. Either DECISION_SOURCE_EXPERIMENT or DECISION_SOURCE_ROLLOUT + * @var string + */ + private $_source; - /** - * Decision constructor. - * - * @param $experimentId - * @param $variationId - * @param $source - */ - public function __construct($experimentId, $variationId, $source) - { - $this->_experimentId = $experimentId; - $this->_variationId = $variationId; - $this->_source = $source; - } + /** + * Decision constructor. + * + * @param $experimentId + * @param $variationId + * @param $source + */ + public function __construct($experimentId, $variationId, $source) + { + $this->_experimentId = $experimentId; + $this->_variationId = $variationId; + $this->_source = $source; + } - public function getExperimentId() - { - return $this->_experimentId; - } + public function getExperimentId() + { + return $this->_experimentId; + } - public function getVariationId() - { - return $this->_variationId; - } + public function getVariationId() + { + return $this->_variationId; + } - public function getSource() - { - return $this->_source; - } -} \ No newline at end of file + public function getSource() + { + return $this->_source; + } +} diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 7b714bbe..63ceac2e 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -143,170 +143,194 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null return $variation; } - /** - * Gets the Bucketing ID for Bucketing - * @param string $userId - * @param array $userAttributes - * - * @return string - */ - private function getBucketingId($userId, $userAttributes){ - // By default, the bucketing ID should be the user ID - $bucketingId = $userId; - - // If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key - if (!empty($userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) { - $bucketingId = $userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]; - $this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId)); + /** + * Gets the Bucketing ID for Bucketing + * @param string $userId user ID + * @param array $userAttributes user attributes + * + * @return string the bucketing ID assigned to user + */ + private function getBucketingId($userId, $userAttributes) + { + // By default, the bucketing ID should be the user ID + $bucketingId = $userId; + + // If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key + if (!empty($userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) { + $bucketingId = $userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]; + $this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId)); + } + + return $bucketingId; } - return $bucketingId; - } + /** + * Get the variation the user is bucketed into for the given FeatureFlag + * @param FeatureFlag $featureFlag The feature flag the user wants to access + * @param string $userId user ID + * @param array $userAttributes user attributes + * @return Decision if getVariationForFeatureExperiment or getVariationForFeatureRollout returns a Decision + * null otherwise + */ + public function getVariationForFeature(FeatureFlag $featureFlag, $userId, $userAttributes) + { + //Evaluate in this order: + //1. Attempt to bucket user into experiment using feature flag. + //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($featureFlag, $userId, $userAttributes); + if ($decision) { + return $decision; + } - /** - * Get the variation the user is bucketed into for the given FeatureFlag - * @param FeatureFlag $featureFlag The feature flag the user wants to access - * @param string $userId user id - * @param array $userAttributes user attributes - * @return Decision / null - */ - public function getVariationForFeature(FeatureFlag $featureFlag, $userId, $userAttributes){ - //Evaluate in this order: - //1. Attempt to bucket user into experiment using feature flag. - //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($featureFlag, $userId, $userAttributes); - if($decision) - return $decision; - - // Check if the feature flag has rollout and the user is bucketed into one of it's rules - $decision = $this->getVariationForFeatureRollout($featureFlag, $userId, $userAttributes); - if($decision){ - $this->_logger->log(Logger::INFO, - "User '{$userId}' is bucketed into a rollout for feature flag '{$featureFlag->getKey()}'." - ); - - return $decision; + // Check if the feature flag has rollout and the user is bucketed into one of it's rules + $decision = $this->getVariationForFeatureRollout($featureFlag, $userId, $userAttributes); + if ($decision) { + $this->_logger->log( + Logger::INFO, + "User '{$userId}' is bucketed into rollout for feature flag '{$featureFlag->getKey()}'." + ); - } else{ - $this->_logger->log(Logger::INFO, - "User '{$userId}' is not bucketed into a rollout for feature flag '{$featureFlag->getKey()}'." - ); + return $decision; + } else { + $this->_logger->log( + Logger::INFO, + "User '{$userId}' is not bucketed into rollout for feature flag '{$featureFlag->getKey()}'." + ); - return null; + return null; + } } - } - /** - * Get the variation if the user is bucketed for one of the experiments on this feature flag - * @param FeatureFlag $featureFlag The feature flag the user wants to access - * @param string $userId user id - * @param array $userAttributes user userAttributes - * @return Decision / null - */ - public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $userId, $userAttributes){ - $feature_flag_key = $featureFlag->getKey(); - $experimentIds = $featureFlag->getExperimentIds(); - //Check if there are any experiment ids inside feature flag - if(empty($experimentIds)) + /** + * Get the variation if the user is bucketed for one of the experiments on this feature flag + * @param FeatureFlag $featureFlag The feature flag the user wants to access + * @param string $userId user id + * @param array $userAttributes user userAttributes + * @return Decision if a variation is returned for the user + * null if feature flag is not used in any experiments or no variation is returned for the user + */ + public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $userId, $userAttributes) { - $this->_logger->log(Logger::DEBUG, - "The feature flag '{$feature_flag_key}' is not used in any experiments."); - return null; - } + $feature_flag_key = $featureFlag->getKey(); + $experimentIds = $featureFlag->getExperimentIds(); + //Check if there are any experiment ids inside feature flag + if (empty($experimentIds)) { + $this->_logger->log( + Logger::DEBUG, + "The feature flag '{$feature_flag_key}' is not used in any experiments." + ); + return null; + } - // Evaluate each experiment id and return the first bucketed experiment variation - foreach($experimentIds as $experiment_id){ - $experiment = $this->_projectConfig->getExperimentFromId($experiment_id); - if( $experiment == new Experiment()){ - // Error logged and exception thrown in ProjectConfig-getExperimentFromId - continue; - } + // Evaluate each experiment id and return the first bucketed experiment variation + foreach ($experimentIds as $experiment_id) { + $experiment = $this->_projectConfig->getExperimentFromId($experiment_id); + if ($experiment && !($experiment->getKey())) { + // Error logged and exception thrown in ProjectConfig-getExperimentFromId + continue; + } + + $variation = $this->getVariation($experiment, $userId, $userAttributes); + if ($variation && $variation->getKey()) { + $this->_logger->log( + Logger::INFO, + "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$feature_flag_key}'." + ); + + return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_EXPERIMENT); + } + } - $variation = $this->getVariation($experiment, $userId, $userAttributes); - if($variation instanceof Variation && $variation != new Variation){ - $this->_logger->log(Logger::INFO, - "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$feature_flag_key}'."); + $this->_logger->log( + Logger::INFO, + "The user '{$userId}' is not bucketed into any of the experiments using the feature '{$feature_flag_key}'." + ); - return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_EXPERIMENT); - } + return null; } - $this->_logger->log(Logger::INFO, - "The user '{$userId}' is not bucketed into any of the experiments on the feature '{$feature_flag_key}'."); - - return null; - } + /** + * Get the variation if the user is bucketed into rollout on this feature flag + * Evaluate the user for rules in priority order by seeing if the user satisfies the audience. + * Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation. + * @param FeatureFlag $featureFlag The feature flag the user wants to access + * @param string $userId user id + * @param array $userAttributes user userAttributes + * @return Decision if a variation is returned for the user + * null if feature flag is not used in a rollout or + * no rollout found against the rollout ID or + * no variation is returned for the user + */ + public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId, $userAttributes) + { + $bucketing_id = $this->getBucketingId($userId, $userAttributes); + $feature_flag_key = $featureFlag->getKey(); + $rollout_id = $featureFlag->getRolloutId(); + if (empty($rollout_id)) { + $this->_logger->log( + Logger::DEBUG, + "Feature flag '{$feature_flag_key}' is not used in a rollout." + ); + return null; + } + $rollout = $this->_projectConfig->getRolloutFromId($rollout_id); + if ($rollout && !($rollout->getId())) { + // Error logged and thrown in getRolloutFromId + return null; + } - /** - * Get the variation if the user is bucketed for one of the rollouts on this feature flag - * Evaluate the user for rules in priority order by seeing if the user satisfies the audience. - * Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation. - * @param FeatureFlag $featureFlag The feature flag the user wants to access - * @param string $userId user id - * @param array $userAttributes user userAttributes - * @return Decision/ null - */ - public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId, $userAttributes){ - $bucketing_id = $this->getBucketingId($userId, $userAttributes); - $feature_flag_key = $featureFlag->getKey(); - $rollout_id = $featureFlag->getRolloutId(); - if(empty($rollout_id)){ - $this->_logger->log(Logger::DEBUG, - "Feature flag '{$feature_flag_key}' is not used in a rollout."); - return null; - } - $rollout = $this->_projectConfig->getRolloutFromId($rollout_id); - if($rollout == new Rollout()){ - // Error logged and thrown in getRolloutFromId - return null; - } + $rolloutRules = $rollout->getExperiments(); + if (sizeof($rolloutRules) == 0) { + return null; + } - $rolloutRules = $rollout->getExperiments(); - if(sizeof($rolloutRules) == 0) - return null; + // Evaluate all rollout rules except for last one + for ($i = 0; $i < sizeof($rolloutRules) - 1; $i++) { + $experiment = $rolloutRules[$i]; - // Evaluate all rollout rules except for last one - for($i=0; $i_projectConfig, $experiment, $userAttributes)) { + $this->_logger->log( + Logger::DEBUG, + sprintf("User '%s' did not meet the audience conditions to be in rollout rule '%s'.", $userId, $experiment->getKey()) + ); + // Evaluate this user for the next rule + continue; + } - // Evaluate if user meets the audience condition of this rollout rule - if (!Validator::isUserInExperiment($this->_projectConfig, $experiment, $userAttributes)) { - $this->_logger->log( - Logger::DEBUG, - sprintf("User '%s' did not meet the audience conditions to be in rollout rule '%s'.", $userId, $experiment->getKey()) - ); - // Evaluate this user for the next rule - continue; - } - - $this->_logger->log(Logger::DEBUG, - sprintf("Attempting to bucket user '{$userId}' into rollout rule '%s'.", $experiment->getKey())); + $this->_logger->log( + Logger::DEBUG, + sprintf("Attempting to bucket user '{$userId}' into rollout rule '%s'.", $experiment->getKey()) + ); - // Evaluate if user satisfies the traffic allocation for this rollout rule - $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); - if($variation && $variation != new Variation()){ - return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); - } else { - $this->_logger->log(Logger::DEBUG, - "User '{$userId}' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now."); - break; - } + // Evaluate if user satisfies the traffic allocation for this rollout rule + $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); + if ($variation && $variation->getKey()) { + return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); + } else { + $this->_logger->log( + Logger::DEBUG, + "User '{$userId}' was excluded due to traffic allocation. Checking 'Everyone Else' rule now." + ); + break; + } + } + // Evaluate Everyone Else Rule / Last Rule now + $experiment = $rolloutRules[sizeof($rolloutRules)-1]; + $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); + if ($variation && $variation->getKey()) { + return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); + } else { + $this->_logger->log( + Logger::DEBUG, + "User '{$userId}' was excluded from the 'Everyone Else' rule for feature flag" + ); + return null; + } } - // Evaluate Everyone Else Rule / Last Rule now - $experiment = $rolloutRules[sizeof($rolloutRules)-1]; - $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); - if($variation && $variation != new Variation()){ - return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); - } else { - $this->_logger->log(Logger::DEBUG, - "User '{$userId}' was excluded from the 'Everyone Else' rule for feature flag"); - return null; - } - } - /** * Determine variation the user has been forced into. * diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 0a4f1e2f..77902590 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -637,7 +637,7 @@ public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile $this->loggerMock->expects($this->at(1)) ->method('log') ->with(Logger::INFO, - "The user 'user1' is not bucketed into any of the experiments on the feature 'boolean_feature'."); + "The user 'user1' is not bucketed into any of the experiments using the feature 'boolean_feature'."); $this->assertSame( $this->decisionService->getVariationForFeatureExperiment($feature_flag,'user1',[]), @@ -658,7 +658,7 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNot $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::INFO, - "The user 'user1' is not bucketed into any of the experiments on the feature 'multi_variate_feature'."); + "The user 'user1' is not bucketed into any of the experiments using the feature 'multi_variate_feature'."); $feature_flag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); $this->assertSame( $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []), @@ -725,7 +725,7 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuc $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::INFO, - "The user 'user_1' is not bucketed into any of the experiments on the feature 'boolean_feature'."); + "The user 'user_1' is not bucketed into any of the experiments using the feature 'boolean_feature'."); $this->assertEquals( $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []), null @@ -786,7 +786,7 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::INFO, - "User 'user_1' is bucketed into a rollout for feature flag 'string_single_variable_feature'."); + "User 'user_1' is bucketed into rollout for feature flag 'string_single_variable_feature'."); $this->assertEquals( $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), @@ -814,7 +814,7 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::INFO, - "User 'user_1' is not bucketed into a rollout for feature flag 'string_single_variable_feature'."); + "User 'user_1' is not bucketed into rollout for feature flag 'string_single_variable_feature'."); $this->assertEquals( $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), @@ -952,7 +952,7 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $this->loggerMock->expects($this->at(1)) ->method('log') ->with(Logger::DEBUG, - "User 'user_1' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now."); + "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now."); $this->assertEquals( @@ -993,11 +993,11 @@ public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheT $this->loggerMock->expects($this->at(1)) ->method('log') ->with(Logger::DEBUG, - "User 'user_1' was excluded due to traffic allocation. Checking 'Eveyrone Else' rule now."); + "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now."); $this->loggerMock->expects($this->at(2)) ->method('log') ->with(Logger::DEBUG, - "User 'user_1' was excluded from the 'Everyone Else' rule for feature flag"); + "User 'user_1' was excluded from the 'Everyone Else' rule for feature flag"); $this->assertEquals( $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), From ee38453e5bb04339362451c1672c8cc8a49c94ab Mon Sep 17 00:00:00 2001 From: Owais Date: Tue, 7 Nov 2017 18:40:31 +0500 Subject: [PATCH 15/20] :pen: Indentation/Styling --- src/Optimizely/Optimizely.php | 154 +++++++++------ src/Optimizely/ProjectConfig.php | 2 +- src/Optimizely/Utils/Validator.php | 17 +- src/Optimizely/Utils/VariableTypeUtils.php | 64 +++--- tests/OptimizelyTest.php | 218 ++++++++++++++------- tests/UtilsTests/ValidatorTest.php | 3 +- tests/UtilsTests/VariableTypeUtilsTest.php | 69 ++++--- 7 files changed, 331 insertions(+), 196 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index e1d5edf6..f822044b 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -380,44 +380,44 @@ public function getForcedVariation($experimentKey, $userId) * @param string Feature flag key * @param string User ID * @param array Associative array of user attributes - * + * * @return boolean */ - public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ - + public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) + { if (!$this->_isValid) { $this->_logger->log(Logger::ERROR, "Datafile has invalid format. Failing '".__FUNCTION__."'."); return null; } - if(!$featureFlagKey){ + if (!$featureFlagKey) { $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty."); return null; } - if(!$userId){ + if (!$userId) { $this->_logger->log(Logger::ERROR, "User ID cannot be empty."); return null; } $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); - if($feature_flag == new FeatureFlag){ + if ($feature_flag == new FeatureFlag) { // Error logged in ProjectConfig - getFeatureFlagFromKey return null; } //validate feature flag - if(!Validator::isFeatureFlagValid($this->_config, $feature_flag)){ + if (!Validator::isFeatureFlagValid($this->_config, $feature_flag)) { return null; } $decision = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); - if(!$decision){ - $this->_logger->log(Logger::INFO,"Feature Flag '{$featureFlagKey}' is not enabled for user '{$userId}'."); + if (!$decision) { + $this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is not enabled for user '{$userId}'."); return false; } - if($decision->getSource() == Decision::DECISION_SOURCE_EXPERIMENT){ + if ($decision->getSource() == Decision::DECISION_SOURCE_EXPERIMENT) { $experiment_id = $decision->getExperimentId(); $variation_id = $decision->getVariationId(); $experiment = $this->_config->getExperimentFromId($experiment_id); @@ -425,11 +425,11 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ $this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes); } else { - $this->_logger->log(Logger::INFO,"The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); + $this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); } - $this->_logger->log(Logger::INFO,"Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'."); - return true; + $this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'."); + return true; } /** @@ -439,50 +439,56 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null){ * @param string User ID * @param array Associative array of user attributes * @param string Variable type - * + * * @return string Feature variable value / null */ - public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $userId, - $attributes = null, $variableType = null) - { - if(!$featureFlagKey){ + public function getFeatureVariableValueForType( + $featureFlagKey, + $variableKey, + $userId, + $attributes = null, + $variableType = null + ) { + if (!$featureFlagKey) { $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty."); return null; } - if(!$variableKey){ + if (!$variableKey) { $this->_logger->log(Logger::ERROR, "Variable key cannot be empty."); return null; } - if(!$userId){ + if (!$userId) { $this->_logger->log(Logger::ERROR, "User ID cannot be empty."); return null; } $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); - if($feature_flag == new FeatureFlag){ + if ($feature_flag == new FeatureFlag) { // Error logged in ProjectConfig - getFeatureFlagFromKey return null; } $variable = $this->_config->getFeatureVariableFromKey($featureFlagKey, $variableKey); - if(!$variable){ + if (!$variable) { // Error message logged in ProjectConfig- getFeatureVariableFromKey return null; } - if($variableType != $variable->getType()){ + if ($variableType != $variable->getType()) { $this->_logger->log( - Logger::ERROR,"Variable is of type '{$variable->getType()}', but you requested it as type '{$variableType}'."); + Logger::ERROR, + "Variable is of type '{$variable->getType()}', but you requested it as type '{$variableType}'." + ); return null; } $decision = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); $variable_value = $variable->getDefaultValue(); - if(!$decision){ - $this->_logger->log(Logger::INFO,"User '{$userId}'is not in any variation, ". + if (!$decision) { + $this->_logger->log(Logger::INFO, "User '{$userId}'is not in any variation, ". "returning default value '{$variable_value}'."); } else { $experiment_id = $decision->getExperimentId(); @@ -490,15 +496,19 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u $experiment = $this->_config->getExperimentFromId($experiment_id); $variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id); $variable_usage = $variation->getVariableUsageById($variable->getId()); - if($variable_usage){ + if ($variable_usage) { $variable_value = $variable_usage->getValue(); - $this->_logger->log(Logger::INFO, + $this->_logger->log( + Logger::INFO, "Returning variable value '{$variable_value}' for variation '{$variation->getKey()}' ". - "of feature flag '{$featureFlagKey}'"); + "of feature flag '{$featureFlagKey}'" + ); } else { - $this->_logger->log(Logger::INFO, - "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}' ". - "returning default value '{$variable_value}'."); + $this->_logger->log( + Logger::INFO, + "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}' ". + "returning default value '{$variable_value}'." + ); } } @@ -511,15 +521,22 @@ public function getFeatureVariableValueForType($featureFlagKey, $variableKey, $u * @param string Variable key * @param string User ID * @param array Associative array of user attributes - * + * * @return string boolean variable value / null */ - public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId, $attributes = null){ + public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId, $attributes = null) + { $variable_value = $this->getFeatureVariableValueForType( - $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::BOOLEAN_TYPE); + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::BOOLEAN_TYPE + ); - if(!is_null($variable_value)) + if (!is_null($variable_value)) { return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + } return $variable_value; } @@ -530,15 +547,22 @@ public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId * @param string Variable key * @param string User ID * @param array Associative array of user attributes - * + * * @return string integer variable value / null */ - public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId, $attributes = null){ + public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId, $attributes = null) + { $variable_value = $this->getFeatureVariableValueForType( - $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::INTEGER_TYPE); + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::INTEGER_TYPE + ); - if(!is_null($variable_value)) + if (!is_null($variable_value)) { return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::INTEGER_TYPE, $this->_logger); + } return $variable_value; } @@ -549,15 +573,22 @@ public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId * @param string Variable key * @param string User ID * @param array Associative array of user attributes - * + * * @return string double variable value / null */ - public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, $attributes = null){ + public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, $attributes = null) + { $variable_value = $this->getFeatureVariableValueForType( - $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::DOUBLE_TYPE); + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::DOUBLE_TYPE + ); - if(!is_null($variable_value)) + if (!is_null($variable_value)) { return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::DOUBLE_TYPE, $this->_logger); + } return $variable_value; } @@ -568,12 +599,18 @@ public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, * @param string Variable key * @param string User ID * @param array Associative array of user attributes - * + * * @return string variable value / null */ - public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, $attributes = null){ + public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, $attributes = null) + { $variable_value = $this->getFeatureVariableValueForType( - $featureFlagKey, $variableKey, $userId, $attributes, FeatureVariable::STRING_TYPE); + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::STRING_TYPE + ); return $variable_value; } @@ -584,27 +621,32 @@ public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, * @param string User ID * @param array Associative array of user attributes */ - public function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes){ + public function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes) + { $impressionEvent = $this->_eventBuilder ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); $this->_logger->log( Logger::DEBUG, - sprintf('Dispatching impression event to URL %s with params %s.', - $impressionEvent->getUrl(), http_build_query($impressionEvent->getParams()) + sprintf( + 'Dispatching impression event to URL %s with params %s.', + $impressionEvent->getUrl(), + http_build_query($impressionEvent->getParams()) ) ); try { $this->_eventDispatcher->dispatchEvent($impressionEvent); - } - catch (Throwable $exception) { + } catch (Throwable $exception) { $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', $exception->getMessage())); - } - catch (Exception $exception) { + 'Unable to dispatch impression event. Error %s', + $exception->getMessage() + )); + } catch (Exception $exception) { $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', $exception->getMessage())); + 'Unable to dispatch impression event. Error %s', + $exception->getMessage() + )); } } } diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index 085aa966..4abb830e 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -457,7 +457,7 @@ public function getVariationFromId($experimentKey, $variationId) public function getFeatureVariableFromKey($featureFlagKey, $variableKey) { $feature_flag = $this->getFeatureFlagFromKey($featureFlagKey); - if($feature_flag == new FeatureFlag()) + if($feature_flag && !($feature_flag->getKey())) return null; if(isset($this->_featureFlagVariableMap[$featureFlagKey]) && diff --git a/src/Optimizely/Utils/Validator.php b/src/Optimizely/Utils/Validator.php index 77532a19..98b87b8d 100644 --- a/src/Optimizely/Utils/Validator.php +++ b/src/Optimizely/Utils/Validator.php @@ -109,27 +109,30 @@ public static function isUserInExperiment($config, $experiment, $userAttributes) /** * Checks that if there are more than one experiment IDs * in the feature flag, they must belong to the same mutex group - * - * @param ProjectConfig $config The project config to verify against + * + * @param ProjectConfig $config The project config to verify against * @param FeatureFlag $featureFlag The feature to validate - * + * * @return boolean True if feature flag is valid */ public static function isFeatureFlagValid($config, $featureFlag) { $experimentIds = $featureFlag->getExperimentIds(); - if(empty($experimentIds)) + if (empty($experimentIds)) { return true; - if(sizeof($experimentIds) == 1) + } + if (sizeof($experimentIds) == 1) { return true; + } $groupId = $config->getExperimentFromId($experimentIds[0])->getGroupId(); - foreach($experimentIds as $id){ + foreach ($experimentIds as $id) { $experiment = $config->getExperimentFromId($id); $grpId = $experiment->getGroupId(); - if($groupId != $grpId) + if ($groupId != $grpId) { return false; + } } return true; diff --git a/src/Optimizely/Utils/VariableTypeUtils.php b/src/Optimizely/Utils/VariableTypeUtils.php index 9a5888d7..c5841966 100644 --- a/src/Optimizely/Utils/VariableTypeUtils.php +++ b/src/Optimizely/Utils/VariableTypeUtils.php @@ -24,34 +24,36 @@ class VariableTypeUtils { - public static function castStringToType($value, $variableType, LoggerInterface $logger = null){ - if($variableType == FeatureVariable::STRING_TYPE) - return $value; - - $return_value = null; - - switch($variableType){ - case FeatureVariable::BOOLEAN_TYPE : - $return_value = strtolower($value) == "true"; - break; - - case FeatureVariable::INTEGER_TYPE : - if(ctype_digit($value)){ - $return_value = (int) $value; - } - break; - - case FeatureVariable::DOUBLE_TYPE : - if(is_numeric($value)){ - $return_value = (float) $value; - } - break; - } - - if(is_null($return_value) && $logger) - $logger->log(Logger::ERROR, "Unable to cast variable value '{$value}' to type '{$variableType}'."); - - return $return_value; - } - -} \ No newline at end of file + public static function castStringToType($value, $variableType, LoggerInterface $logger = null) + { + if ($variableType == FeatureVariable::STRING_TYPE) { + return $value; + } + + $return_value = null; + + switch ($variableType) { + case FeatureVariable::BOOLEAN_TYPE: + $return_value = strtolower($value) == "true"; + break; + + case FeatureVariable::INTEGER_TYPE: + if (ctype_digit($value)) { + $return_value = (int) $value; + } + break; + + case FeatureVariable::DOUBLE_TYPE: + if (is_numeric($value)) { + $return_value = (float) $value; + } + break; + } + + if (is_null($return_value) && $logger) { + $logger->log(Logger::ERROR, "Unable to cast variable value '{$value}' to type '{$variableType}'."); + } + + return $return_value; + } +} diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index ce46a93f..1554226f 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -1732,44 +1732,47 @@ public function testGetVariationBucketingIdAttribute() $this->assertEquals(null, $variationKey, sprintf('Invalid variation key "%s" for getVariation with bucketing ID "%s".', $variationKey, $this->testBucketingIdControl)); } - public function testIsFeatureEnabledGivenInvalidDataFile(){ + public function testIsFeatureEnabledGivenInvalidDataFile() + { $optlyObject = new Optimizely('Random datafile', null, $this->loggerMock); $this->expectOutputRegex("/Datafile has invalid format. Failing 'isFeatureEnabled'./"); $optlyObject->isFeatureEnabled("boolean_feature", "user_id"); } - public function testIsFeatureEnabledGivenInvalidArguments(){ + public function testIsFeatureEnabledGivenInvalidArguments() + { // should return null and log a message when feature flag key is empty $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); - $this->assertSame($this->optimizelyObject->isFeatureEnabled("","user_id"), null); + $this->assertSame($this->optimizelyObject->isFeatureEnabled("", "user_id"), null); // should return null and log a message when feature flag key is null $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); - $this->assertSame($this->optimizelyObject->isFeatureEnabled(null,"user_id"), null); + $this->assertSame($this->optimizelyObject->isFeatureEnabled(null, "user_id"), null); // should return null and log a message when user id is empty $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "User ID cannot be empty."); + ->with(Logger::ERROR, "User ID cannot be empty."); $this->assertSame($this->optimizelyObject->isFeatureEnabled("boolean_feature", ""), null); // should return null and log a message when user id is null $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "User ID cannot be empty."); + ->with(Logger::ERROR, "User ID cannot be empty."); $this->assertSame($this->optimizelyObject->isFeatureEnabled("boolean_feature", null), null); } - public function testIsFeatureEnabledGivenFeatureFlagNotFound(){ + public function testIsFeatureEnabledGivenFeatureFlagNotFound() + { $feature_key = "abcd"; // Any string that is not a feature flag key in the data file //should return null and log a message when no feature flag found against a valid feature key @@ -1779,7 +1782,8 @@ public function testIsFeatureEnabledGivenFeatureFlagNotFound(){ $this->assertSame($this->optimizelyObject->isFeatureEnabled($feature_key, "user_id"), null); } - public function testIsFeatureEnabledGivenInvalidFeatureFlag(){ + public function testIsFeatureEnabledGivenInvalidFeatureFlag() + { // Create local config copy for this method to add error $projectConfig = new ProjectConfig($this->datafile, $this->loggerMock, new NoOpErrorHandler()); $optimizelyObj = new Optimizely($this->datafile); @@ -1798,7 +1802,8 @@ public function testIsFeatureEnabledGivenInvalidFeatureFlag(){ $this->assertSame($optimizelyObj->isFeatureEnabled('mutex_group_feature', "user_id"), null); } - public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser(){ + public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser() + { // should return false when no variation is returned for user $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) @@ -1829,10 +1834,12 @@ public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser(){ $this->assertSame( $optimizelyMock->isFeatureEnabled('double_single_variable_feature', 'user_id'), - false); + false + ); } - public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimented(){ + public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimented() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) @@ -1852,7 +1859,10 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); $expected_decision = new Decision( - $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); + $experiment->getId(), + $variation->getId(), + Decision::DECISION_SOURCE_EXPERIMENT + ); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -1867,12 +1877,14 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper ->method('log') ->with(Logger::INFO, "Feature Flag 'double_single_variable_feature' is enabled for user 'user_id'."); - $this->assertSame( + $this->assertSame( $optimizelyMock->isFeatureEnabled('double_single_variable_feature', 'user_id', []), - true); + true + ); } - public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperimented(){ + public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperimented() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) @@ -1892,7 +1904,10 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx $experiment = $rollout->getExperiments()[0]; $variation = $experiment->getVariations()[0]; $expected_decision = new Decision( - $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $experiment->getId(), + $variation->getId(), + Decision::DECISION_SOURCE_ROLLOUT + ); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -1904,69 +1919,92 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user_id' is not being experimented on Feature Flag 'boolean_single_variable_feature'."); + ->with( + Logger::INFO, + "The user 'user_id' is not being experimented on Feature Flag 'boolean_single_variable_feature'." + ); $this->loggerMock->expects($this->at(1)) ->method('log') ->with(Logger::INFO, "Feature Flag 'boolean_single_variable_feature' is enabled for user 'user_id'."); - $this->assertSame( + $this->assertSame( $optimizelyMock->isFeatureEnabled('boolean_single_variable_feature', 'user_id', []), - true); + true + ); } - public function testGetFeatureVariableValueForTypeGivenInvalidArguments(){ + public function testGetFeatureVariableValueForTypeGivenInvalidArguments() + { // should return null and log a message when feature flag key is empty $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - "", "double_variable", "user_id"), null); + "", + "double_variable", + "user_id" + ), null); // should return null and log a message when feature flag key is null $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - null, "double_variable", "user_id"), null); + null, + "double_variable", + "user_id" + ), null); // should return null and log a message when variable key is empty $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "Variable key cannot be empty."); + ->with(Logger::ERROR, "Variable key cannot be empty."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - "boolean_feature", "", "user_id"), null); + "boolean_feature", + "", + "user_id" + ), null); // should return null and log a message when variable key is null $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "Variable key cannot be empty."); + ->with(Logger::ERROR, "Variable key cannot be empty."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - "boolean_feature", null, "user_id"), null); + "boolean_feature", + null, + "user_id" + ), null); // should return null and log a message when user id is empty $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "User ID cannot be empty."); + ->with(Logger::ERROR, "User ID cannot be empty."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - "boolean_feature", "double_variable", ""), null); + "boolean_feature", + "double_variable", + "" + ), null); // should return null and log a message when user id is null $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, "User ID cannot be empty."); + ->with(Logger::ERROR, "User ID cannot be empty."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - "boolean_feature", "double_variable", null), null); + "boolean_feature", + "double_variable", + null + ), null); } - public function testGetFeatureVariableValueForTypeGivenFeatureFlagNotFound(){ + public function testGetFeatureVariableValueForTypeGivenFeatureFlagNotFound() + { $feature_key = "abcd"; // Any string that is not a feature flag key in the data file //should return null and log a message when no feature flag found against a valid feature key @@ -1975,10 +2013,14 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagNotFound(){ ->with(Logger::ERROR, "FeatureFlag Key \"{$feature_key}\" is not in datafile."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - $feature_key , "double_variable", 'user_id'), null); + $feature_key, + "double_variable", + 'user_id' + ), null); } - public function testGetFeatureVariableValueForTypeGivenFeatureVariableNotFound(){ + public function testGetFeatureVariableValueForTypeGivenFeatureVariableNotFound() + { $feature_key = "boolean_feature"; // Any exisiting feature key in the data file $variable_key = "abcd"; // Any string that is not a variable key in the data file @@ -1989,10 +2031,14 @@ public function testGetFeatureVariableValueForTypeGivenFeatureVariableNotFound() "for feature flag \"{$feature_key}\"."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - $feature_key , $variable_key, 'user_id'), null); + $feature_key, + $variable_key, + 'user_id' + ), null); } - public function testGetFeatureVariableValueForTypeGivenInvalidFeatureVariableType(){ + public function testGetFeatureVariableValueForTypeGivenInvalidFeatureVariableType() + { // should return null and log a message when a feature variable does exist but is // called for another type $this->loggerMock->expects($this->at(0)) @@ -2000,12 +2046,16 @@ public function testGetFeatureVariableValueForTypeGivenInvalidFeatureVariableTyp ->with(Logger::ERROR, "Variable is of type 'double', but you requested it as type 'string'."); $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( - "double_single_variable_feature" , "double_variable", "user_id", null, "string") - , null); - + "double_single_variable_feature", + "double_variable", + "user_id", + null, + "string" + ), null); } - public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUser(){ + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUser() + { // should return default value $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) @@ -2016,21 +2066,25 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledFo $decisionService->setAccessible(true); $decisionService->setValue($this->optimizelyObject, $decisionServiceMock); - $decisionServiceMock->expects($this->exactly(1)) + $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') ->will($this->returnValue(null)); $this->loggerMock->expects($this->exactly(1)) ->method('log') - ->with(Logger::INFO, - "User 'user_id'is not in any variation, returning default value '14.99'."); + ->with( + Logger::INFO, + "User 'user_id'is not in any variation, returning default value '14.99'." + ); $this->assertSame( $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), - '14.99'); + '14.99' + ); } - public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableIsInVariation(){ + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableIsInVariation() + { // should return specific value $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) @@ -2044,7 +2098,10 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); $expected_decision = new Decision( - $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); + $experiment->getId(), + $variation->getId(), + Decision::DECISION_SOURCE_EXPERIMENT + ); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -2052,16 +2109,20 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs $this->loggerMock->expects($this->exactly(1)) ->method('log') - ->with(Logger::INFO, + ->with( + Logger::INFO, "Returning variable value '42.42' for variation 'control' ". - "of feature flag 'double_single_variable_feature'"); + "of feature flag 'double_single_variable_feature'" + ); $this->assertSame( $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), - '42.42'); + '42.42' + ); } - public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableNotInVariation(){ + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableNotInVariation() + { // should return default value $decisionServiceMock = $this->getMockBuilder(DecisionService::class) @@ -2077,7 +2138,10 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_integer_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_integer_feature', 'control'); $expected_decision = new Decision( - $experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); + $experiment->getId(), + $variation->getId(), + Decision::DECISION_SOURCE_EXPERIMENT + ); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -2085,16 +2149,26 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs $this->loggerMock->expects($this->exactly(1)) ->method('log') - ->with(Logger::INFO, - "Variable 'double_variable' is not used in variation 'control' returning default value '14.99'."); + ->with( + Logger::INFO, + "Variable 'double_variable' is not used in variation 'control' returning default value '14.99'." + ); $this->assertSame( - $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), - '14.99'); + $this->optimizelyObject->getFeatureVariableValueForType( + 'double_single_variable_feature', + 'double_variable', + 'user_id', + [], + 'double' + ), + '14.99' + ); } - public function testGetFeatureVariableBooleanCaseTrue(){ + public function testGetFeatureVariableBooleanCaseTrue() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('getFeatureVariableValueForType')) @@ -2113,7 +2187,8 @@ public function testGetFeatureVariableBooleanCaseTrue(){ ); } - public function testGetFeatureVariableBooleanCaseFalse(){ + public function testGetFeatureVariableBooleanCaseFalse() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('getFeatureVariableValueForType')) @@ -2132,8 +2207,9 @@ public function testGetFeatureVariableBooleanCaseFalse(){ ); } - public function testGetFeatureVariableIntegerWhenCasted(){ - $optimizelyMock = $this->getMockBuilder(Optimizely::class) + public function testGetFeatureVariableIntegerWhenCasted() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('getFeatureVariableValueForType')) ->getMock(); @@ -2151,7 +2227,8 @@ public function testGetFeatureVariableIntegerWhenCasted(){ ); } - public function testGetFeatureVariableIntegerWhenNotCasted(){ + public function testGetFeatureVariableIntegerWhenNotCasted() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('getFeatureVariableValueForType')) @@ -2170,8 +2247,9 @@ public function testGetFeatureVariableIntegerWhenNotCasted(){ ); } - public function testGetFeatureVariableDoubleWhenCasted(){ - $optimizelyMock = $this->getMockBuilder(Optimizely::class) + public function testGetFeatureVariableDoubleWhenCasted() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('getFeatureVariableValueForType')) ->getMock(); @@ -2189,8 +2267,9 @@ public function testGetFeatureVariableDoubleWhenCasted(){ ); } - public function testGetFeatureVariableDoubleWhenNotCasted(){ - $optimizelyMock = $this->getMockBuilder(Optimizely::class) + public function testGetFeatureVariableDoubleWhenNotCasted() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('getFeatureVariableValueForType')) ->getMock(); @@ -2208,7 +2287,8 @@ public function testGetFeatureVariableDoubleWhenNotCasted(){ ); } - public function testGetFeatureVariableString(){ + public function testGetFeatureVariableString() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('getFeatureVariableValueForType')) diff --git a/tests/UtilsTests/ValidatorTest.php b/tests/UtilsTests/ValidatorTest.php index 64c23fda..6d89fee8 100644 --- a/tests/UtilsTests/ValidatorTest.php +++ b/tests/UtilsTests/ValidatorTest.php @@ -171,7 +171,8 @@ public function testIsUserInExperimentAudienceNoMatch() )); } - public function testIsFeatureFlagValid(){ + public function testIsFeatureFlagValid() + { $config = new ProjectConfig(DATAFILE, new NoOpLogger(), new NoOpErrorHandler()); $feature_flag_source = $config->getFeatureFlagFromKey('mutex_group_feature'); diff --git a/tests/UtilsTests/VariableTypeUtilsTest.php b/tests/UtilsTests/VariableTypeUtilsTest.php index ab6a25e4..1d52fd62 100644 --- a/tests/UtilsTests/VariableTypeUtilsTest.php +++ b/tests/UtilsTests/VariableTypeUtilsTest.php @@ -37,49 +37,56 @@ protected function setUp() $this->variableUtilObj = new VariableTypeUtils(); } - public function testValueCastingToBoolean(){ + public function testValueCastingToBoolean() + { $this->assertTrue($this->variableUtilObj->castStringToType('true', 'boolean')); $this->assertTrue($this->variableUtilObj->castStringToType('True', 'boolean')); $this->assertFalse($this->variableUtilObj->castStringToType('false', 'boolean')); $this->assertFalse($this->variableUtilObj->castStringToType('somestring', 'boolean')); } - public function testValueCastingToInteger(){ - $this->assertSame($this->variableUtilObj->castStringToType('1000', 'integer'), 1000); - $this->assertSame($this->variableUtilObj->castStringToType('123', 'integer'), 123); + public function testValueCastingToInteger() + { + $this->assertSame($this->variableUtilObj->castStringToType('1000', 'integer'), 1000); + $this->assertSame($this->variableUtilObj->castStringToType('123', 'integer'), 123); - // should return nulll and log a message if value can not be casted to an integer - $value = '123.5'; // any string with non-decimal digits - $type = 'integer'; - $this->loggerMock->expects($this->exactly(1)) + // should return nulll and log a message if value can not be casted to an integer + $value = '123.5'; // any string with non-decimal digits + $type = 'integer'; + $this->loggerMock->expects($this->exactly(1)) ->method('log') - ->with(Logger::ERROR, - "Unable to cast variable value '{$value}' to type '{$type}'."); + ->with( + Logger::ERROR, + "Unable to cast variable value '{$value}' to type '{$type}'." + ); - $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); - } + $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); + } - public function testValueCastingToDouble(){ - $this->assertSame($this->variableUtilObj->castStringToType('1000', 'double'), 1000.0); - $this->assertSame($this->variableUtilObj->castStringToType('3.0', 'double'), 3.0); - $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'double'), 13.37); + public function testValueCastingToDouble() + { + $this->assertSame($this->variableUtilObj->castStringToType('1000', 'double'), 1000.0); + $this->assertSame($this->variableUtilObj->castStringToType('3.0', 'double'), 3.0); + $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'double'), 13.37); - // should return nil and log a message if value can not be casted to a double - $value = 'any-non-numeric-string'; - $type = 'double'; - $this->loggerMock->expects($this->exactly(1)) + // should return nil and log a message if value can not be casted to a double + $value = 'any-non-numeric-string'; + $type = 'double'; + $this->loggerMock->expects($this->exactly(1)) ->method('log') - ->with(Logger::ERROR, - "Unable to cast variable value '{$value}' to type '{$type}'."); + ->with( + Logger::ERROR, + "Unable to cast variable value '{$value}' to type '{$type}'." + ); - $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); - } + $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); + } - public function testValueCastingToString(){ - $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'string'), '13.37'); - $this->assertSame($this->variableUtilObj->castStringToType('a string', 'string'), 'a string'); - $this->assertSame($this->variableUtilObj->castStringToType('3', 'string'), '3'); - $this->assertSame($this->variableUtilObj->castStringToType('false', 'string'), 'false'); - } + public function testValueCastingToString() + { + $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'string'), '13.37'); + $this->assertSame($this->variableUtilObj->castStringToType('a string', 'string'), 'a string'); + $this->assertSame($this->variableUtilObj->castStringToType('3', 'string'), '3'); + $this->assertSame($this->variableUtilObj->castStringToType('false', 'string'), 'false'); + } } - From eb54a9dba4c6cdaf2d1c936546b498d4df9c802b Mon Sep 17 00:00:00 2001 From: Owais Date: Thu, 9 Nov 2017 11:00:32 +0500 Subject: [PATCH 16/20] :memo: Addressed all nits, renamed DecisionService Decision to Feature Decision, repositioned getBucketingId --- .../DecisionService/DecisionService.php | 66 ++++++++++--------- .../{Decision.php => FeatureDecision.php} | 4 +- .../DecisionServiceTest.php | 22 +++---- 3 files changed, 47 insertions(+), 45 deletions(-) rename src/Optimizely/DecisionService/{Decision.php => FeatureDecision.php} (96%) diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 63ceac2e..ffb40eb3 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -25,6 +25,7 @@ use Optimizely\Entity\Variation; use Optimizely\Logger\LoggerInterface; use Optimizely\ProjectConfig; +use Optimizely\UserProfile\Decision; use Optimizely\UserProfile\UserProfileServiceInterface; use Optimizely\UserProfile\UserProfile; use Optimizely\UserProfile\UserProfileUtils; @@ -84,6 +85,27 @@ public function __construct(LoggerInterface $logger, ProjectConfig $projectConfi $this->_userProfileService = $userProfileService; } + /** + * Gets the ID for Bucketing + * @param string $userId user ID + * @param array $userAttributes user attributes + * + * @return string the bucketing ID assigned to user + */ + private function getBucketingId($userId, $userAttributes) + { + // By default, the bucketing ID should be the user ID + $bucketingId = $userId; + + // If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key + if (!empty($userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) { + $bucketingId = $userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]; + $this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId)); + } + + return $bucketingId; + } + /** * Determine which variation to show the user. * @@ -143,27 +165,6 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null return $variation; } - /** - * Gets the Bucketing ID for Bucketing - * @param string $userId user ID - * @param array $userAttributes user attributes - * - * @return string the bucketing ID assigned to user - */ - private function getBucketingId($userId, $userAttributes) - { - // By default, the bucketing ID should be the user ID - $bucketingId = $userId; - - // If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key - if (!empty($userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) { - $bucketingId = $userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]; - $this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId)); - } - - return $bucketingId; - } - /** * Get the variation the user is bucketed into for the given FeatureFlag * @param FeatureFlag $featureFlag The feature flag the user wants to access @@ -193,14 +194,14 @@ public function getVariationForFeature(FeatureFlag $featureFlag, $userId, $userA ); return $decision; - } else { - $this->_logger->log( - Logger::INFO, - "User '{$userId}' is not bucketed into rollout for feature flag '{$featureFlag->getKey()}'." - ); + } + + $this->_logger->log( + Logger::INFO, + "User '{$userId}' is not bucketed into rollout for feature flag '{$featureFlag->getKey()}'." + ); - return null; - } + return null; } /** @@ -215,6 +216,7 @@ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $user { $feature_flag_key = $featureFlag->getKey(); $experimentIds = $featureFlag->getExperimentIds(); + //Check if there are any experiment ids inside feature flag if (empty($experimentIds)) { $this->_logger->log( @@ -239,7 +241,7 @@ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $user "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$feature_flag_key}'." ); - return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_EXPERIMENT); + return new FeatureDecision($experiment->getId(), $variation->getId(), FeatureDecision::DECISION_SOURCE_EXPERIMENT); } } @@ -308,7 +310,7 @@ public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId, // Evaluate if user satisfies the traffic allocation for this rollout rule $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); if ($variation && $variation->getKey()) { - return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); + return new FeatureDecision($experiment->getId(), $variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); } else { $this->_logger->log( Logger::DEBUG, @@ -321,7 +323,7 @@ public function getVariationForFeatureRollout(FeatureFlag $featureFlag, $userId, $experiment = $rolloutRules[sizeof($rolloutRules)-1]; $variation = $this->_bucketer->bucket($this->_projectConfig, $experiment, $bucketing_id, $userId); if ($variation && $variation->getKey()) { - return new Decision($experiment->getId(), $variation->getId(), DECISION::DECISION_SOURCE_ROLLOUT); + return new FeatureDecision($experiment->getId(), $variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); } else { $this->_logger->log( Logger::DEBUG, @@ -452,7 +454,7 @@ private function saveVariation(Experiment $experiment, Variation $variation, Use $decision = $userProfile->getDecisionForExperiment($experimentId); $variationId = $variation->getId(); if (is_null($decision)) { - $decision = new \Optimizely\UserProfile\Decision($variationId); + $decision = new Decision($variationId); } else { $decision->setVariationId($variationId); } diff --git a/src/Optimizely/DecisionService/Decision.php b/src/Optimizely/DecisionService/FeatureDecision.php similarity index 96% rename from src/Optimizely/DecisionService/Decision.php rename to src/Optimizely/DecisionService/FeatureDecision.php index 6782f923..e458bb30 100644 --- a/src/Optimizely/DecisionService/Decision.php +++ b/src/Optimizely/DecisionService/FeatureDecision.php @@ -16,7 +16,7 @@ */ namespace Optimizely\DecisionService; -class Decision +class FeatureDecision { const DECISION_SOURCE_EXPERIMENT = 'experiment'; const DECISION_SOURCE_ROLLOUT = 'rollout'; @@ -38,7 +38,7 @@ class Decision private $_source; /** - * Decision constructor. + * FeatureDecision constructor. * * @param $experimentId * @param $variationId diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 77902590..1efcfa3b 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -19,8 +19,8 @@ use Exception; use Monolog\Logger; use Optimizely\Bucketer; -use Optimizely\DecisionService\Decision; use Optimizely\DecisionService\DecisionService; +use Optimizely\DecisionService\FeatureDecision; use Optimizely\Entity\Experiment; use Optimizely\Entity\Variation; use Optimizely\ErrorHandler\NoOpErrorHandler; @@ -675,7 +675,7 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsB ->will($this->returnValue($variation)); $feature_flag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); - $expected_decision = new Decision($experiment->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); + $expected_decision = new FeatureDecision($experiment->getId(), $variation->getId(), FeatureDecision::DECISION_SOURCE_EXPERIMENT); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -698,7 +698,7 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBuck $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); $variation = $mutex_exp->getVariations()[0]; - $expected_decision = new Decision($mutex_exp->getId(), $variation->getId(), Decision::DECISION_SOURCE_EXPERIMENT); + $expected_decision = new FeatureDecision($mutex_exp->getId(), $variation->getId(), FeatureDecision::DECISION_SOURCE_EXPERIMENT); $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); $this->loggerMock->expects($this->at(0)) @@ -772,8 +772,8 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $rollout = $this->config->getRolloutFromId($rollout_id); $experiment = $rollout->getExperiments()[0]; $expected_variation = $experiment->getVariations()[0]; - $expected_decision = new Decision( - $experiment->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $expected_decision = new FeatureDecision( + $experiment->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); $decisionServiceMock ->method('getVariationForFeatureExperiment') @@ -892,8 +892,8 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $experiment = $rollout->getExperiments()[0]; $expected_variation = $experiment->getVariations()[0]; - $expected_decision = new Decision( - $experiment->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $expected_decision = new FeatureDecision( + $experiment->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -927,8 +927,8 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $experiment2 = $rollout->getExperiments()[2]; $expected_variation = $experiment2->getVariations()[0]; - $expected_decision = new Decision( - $experiment2->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $expected_decision = new FeatureDecision( + $experiment2->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -1021,8 +1021,8 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $experiment2 = $rollout->getExperiments()[2]; $expected_variation = $experiment2->getVariations()[0]; - $expected_decision = new Decision( - $experiment2->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $expected_decision = new FeatureDecision( + $experiment2->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); // Provide null attributes so that user does not qualify for audience $user_attributes = []; From e331e42dd06f2fc623b9a0c2c20175746e780476 Mon Sep 17 00:00:00 2001 From: Owais Date: Fri, 10 Nov 2017 12:44:33 +0500 Subject: [PATCH 17/20] :memo: Final styling issues addressed --- src/Optimizely/DecisionService/DecisionService.php | 9 +++++---- src/Optimizely/Utils/Validator.php | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index ffb40eb3..4913f4a1 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -97,7 +97,8 @@ private function getBucketingId($userId, $userAttributes) // By default, the bucketing ID should be the user ID $bucketingId = $userId; - // If the bucketing ID key is defined in userAttributes, then use that in place of the userID for the murmur hash key + // If the bucketing ID key is defined in userAttributes, then use that in + // place of the userID for the murmur hash key if (!empty($userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID])) { $bucketingId = $userAttributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]; $this->_logger->log(Logger::DEBUG, sprintf('Setting the bucketing ID to "%s".', $bucketingId)); @@ -217,7 +218,7 @@ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $user $feature_flag_key = $featureFlag->getKey(); $experimentIds = $featureFlag->getExperimentIds(); - //Check if there are any experiment ids inside feature flag + // Check if there are any experiment IDs inside feature flag if (empty($experimentIds)) { $this->_logger->log( Logger::DEBUG, @@ -226,7 +227,7 @@ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $user return null; } - // Evaluate each experiment id and return the first bucketed experiment variation + // Evaluate each experiment ID and return the first bucketed experiment variation foreach ($experimentIds as $experiment_id) { $experiment = $this->_projectConfig->getExperimentFromId($experiment_id); if ($experiment && !($experiment->getKey())) { @@ -254,7 +255,7 @@ public function getVariationForFeatureExperiment(FeatureFlag $featureFlag, $user } /** - * Get the variation if the user is bucketed into rollout on this feature flag + * Get the variation if the user is bucketed into rollout for this feature flag * Evaluate the user for rules in priority order by seeing if the user satisfies the audience. * Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation. * @param FeatureFlag $featureFlag The feature flag the user wants to access diff --git a/src/Optimizely/Utils/Validator.php b/src/Optimizely/Utils/Validator.php index e5f42207..1e892c45 100644 --- a/src/Optimizely/Utils/Validator.php +++ b/src/Optimizely/Utils/Validator.php @@ -44,7 +44,6 @@ public static function validateJsonSchema($datafile, LoggerInterface $logger = n $logger->log(Logger::DEBUG, "JSON does not validate. Violations:\n");; foreach ($validator->getErrors() as $error) { $logger->log(Logger::DEBUG, "[%s] %s\n", $error['property'], $error['message']); - } } From c2e5147d56f85c833fb91b929a8703912d2c6a6a Mon Sep 17 00:00:00 2001 From: Owais Date: Fri, 10 Nov 2017 13:35:45 +0500 Subject: [PATCH 18/20] :memo: Styling fixes and made sendImpression protected --- src/Optimizely/Optimizely.php | 74 ++-- .../DecisionServiceTest.php | 334 ++++++++++-------- tests/OptimizelyTest.php | 2 +- 3 files changed, 234 insertions(+), 176 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index f822044b..8110772a 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -205,6 +205,41 @@ private function getValidExperimentsForEvent($event, $userId, $attributes = null return $validExperiments; } + /** + * @param string Experiment key + * @param string Variation key + * @param string User ID + * @param array Associative array of user attributes + */ + protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes) + { + $impressionEvent = $this->_eventBuilder + ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); + $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); + $this->_logger->log( + Logger::DEBUG, + sprintf( + 'Dispatching impression event to URL %s with params %s.', + $impressionEvent->getUrl(), + http_build_query($impressionEvent->getParams()) + ) + ); + + try { + $this->_eventDispatcher->dispatchEvent($impressionEvent); + } catch (Throwable $exception) { + $this->_logger->log(Logger::ERROR, sprintf( + 'Unable to dispatch impression event. Error %s', + $exception->getMessage() + )); + } catch (Exception $exception) { + $this->_logger->log(Logger::ERROR, sprintf( + 'Unable to dispatch impression event. Error %s', + $exception->getMessage() + )); + } + } + /** * Buckets visitor and sends impression event to Optimizely. * @@ -465,7 +500,7 @@ public function getFeatureVariableValueForType( } $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); - if ($feature_flag == new FeatureFlag) { + if ($feature_flag && (!$feature_flag->getId())) { // Error logged in ProjectConfig - getFeatureFlagFromKey return null; } @@ -506,7 +541,7 @@ public function getFeatureVariableValueForType( } else { $this->_logger->log( Logger::INFO, - "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}' ". + "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}', ". "returning default value '{$variable_value}'." ); } @@ -614,39 +649,4 @@ public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, return $variable_value; } - - /** - * @param string Experiment key - * @param string Variation key - * @param string User ID - * @param array Associative array of user attributes - */ - public function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes) - { - $impressionEvent = $this->_eventBuilder - ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); - $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); - $this->_logger->log( - Logger::DEBUG, - sprintf( - 'Dispatching impression event to URL %s with params %s.', - $impressionEvent->getUrl(), - http_build_query($impressionEvent->getParams()) - ) - ); - - try { - $this->_eventDispatcher->dispatchEvent($impressionEvent); - } catch (Throwable $exception) { - $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', - $exception->getMessage() - )); - } catch (Exception $exception) { - $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', - $exception->getMessage() - )); - } - } } diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 77902590..5cc35684 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -73,9 +73,9 @@ public function setUp() $this->userProvideServiceMock = $this->getMockBuilder(UserProfileServiceInterface::class) ->getMock(); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); + $this->decisionService = new DecisionService($this->loggerMock, $this->config); - $this->decisionServiceMock = $this->getMockBuilder(DecisionService::class) + $this->decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->config)) ->setMethods(array('getVariation')) ->getMock(); @@ -126,9 +126,9 @@ public function testGetVariationReturnsWhitelistedVariation() $callIndex = 0; $this->bucketerMock->expects($this->never()) ->method('bucket'); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); + ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'User "user1" is forced in variation "control" of experiment "test_experiment".'); @@ -158,9 +158,9 @@ public function testGetVariationReturnsWhitelistedVariationForGroupedExperiment( $callIndex = 0; $this->bucketerMock->expects($this->never()) ->method('bucket'); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); + ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'User "user1" is forced in variation "group_exp_1_var_1" of experiment "group_experiment_1".'); @@ -277,9 +277,9 @@ public function testGetVariationReturnsStoredVariationIfAvailable() $callIndex = 0; $this->bucketerMock->expects($this->never()) ->method('bucket'); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, 'User "not_whitelisted_user" is not in the forced variation map.'); + ->with(Logger::DEBUG, 'User "not_whitelisted_user" is not in the forced variation map.'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'Returning previously activated variation "control" of experiment "test_experiment" for user "not_whitelisted_user" from user profile.'); @@ -315,9 +315,9 @@ public function testGetVariationBucketsIfNoStoredVariation() $this->bucketerMock->expects($this->once()) ->method('bucket') ->willReturn($expectedVariation); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'No previously activated variation of experiment "test_experiment" for user "testUserId" found in user profile.'); @@ -365,7 +365,7 @@ public function testGetVariationBucketsIfStoredVariationIsInvalid() ->willReturn($expectedVariation); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'User "testUserId" was previously bucketed into variation with ID "invalid" for experiment "test_experiment", but no matching variation was found for that user. We will re-bucket the user.'); @@ -417,7 +417,7 @@ public function testGetVariationBucketsIfUserProfileServiceLookupThrows() ->willReturn($expectedVariation); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::ERROR, 'The User Profile Service lookup method failed: I am error.'); @@ -469,7 +469,7 @@ public function testGetVariationBucketsIfUserProfileServiceSaveThrows() ->willReturn($expectedVariation); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'No user profile found for user with ID "testUserId".'); @@ -580,11 +580,11 @@ public function testGetVariationWithBucketingId() // check forced variation $this->assertTrue($optlyObject->setForcedVariation($this->experimentKey, $userId, $this->variationKeyControl), sprintf('Set variation to "%s" failed.', $this->variationKeyControl)); $variationKey = $optlyObject->getVariation($this->experimentKey, $userId, $userAttributesWithBucketingId); - $this->assertEquals( $this->variationKeyControl, $variationKey); + $this->assertEquals($this->variationKeyControl, $variationKey); // check whitelisted variation $variationKey = $optlyObject->getVariation($this->experimentKey, $this->testUserIdWhitelisted, $userAttributesWithBucketingId); - $this->assertEquals( $this->variationKeyControl, $variationKey); + $this->assertEquals($this->variationKeyControl, $variationKey); // check user profile $storedUserProfile = array( @@ -608,23 +608,23 @@ public function testGetVariationWithBucketingId() $this->assertEquals($this->variationKeyControl, $variationKey, sprintf('Variation "%s" does not match expected user profile variation "%s".', $variationKey, $this->variationKeyControl)); } - //should return nil and log a message when the feature flag's experiment ids array is empty - public function testGetVariationForFeatureExperimentGivenNullExperimentIds(){ - + // should return nil and log a message when the feature flag's experiment ids array is empty + public function testGetVariationForFeatureExperimentGivenNullExperimentIds() + { $feature_flag = $this->config->getFeatureFlagFromKey('empty_feature'); $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::DEBUG, "The feature flag 'empty_feature' is not used in any experiments."); $this->assertSame( - $this->decisionService->getVariationForFeatureExperiment($feature_flag,'user1',[]), - null + null, + $this->decisionService->getVariationForFeatureExperiment($feature_flag, 'user1', []) ); } - //should return nil and log a message when the experiment is not in the datafile - public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile(){ - + // should return nil and log a message when the experiment is not in the datafile + public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile() + { $boolean_feature = $this->config->getFeatureFlagFromKey('boolean_feature'); $feature_flag = clone $boolean_feature; // Use any string that is not an experiment id in the data file @@ -636,40 +636,47 @@ public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::INFO, - "The user 'user1' is not bucketed into any of the experiments using the feature 'boolean_feature'."); + ->with( + Logger::INFO, + "The user 'user1' is not bucketed into any of the experiments using the feature 'boolean_feature'." + ); $this->assertSame( - $this->decisionService->getVariationForFeatureExperiment($feature_flag,'user1',[]), - null + null, + $this->decisionService->getVariationForFeatureExperiment($feature_flag, 'user1', []) ); } - //should return nil and log when the user is not bucketed into the feature flag's experiments - public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBucketed(){ + // should return nil and log when the user is not bucketed into the feature flag's experiments + public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBucketed() + { $multivariate_experiment = $this->config->getExperimentFromKey('test_experiment_multivariate'); $map = [ [$multivariate_experiment, 'user1', [], null] ]; - //make sure the user is not bucketed into the feature experiment + // make sure the user is not bucketed into the feature experiment $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') ->will($this->returnValueMap($map)); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user1' is not bucketed into any of the experiments using the feature 'multi_variate_feature'."); + ->with( + Logger::INFO, + "The user 'user1' is not bucketed into any of the experiments using the feature 'multi_variate_feature'." + ); $feature_flag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); $this->assertSame( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []), - null); + null, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []) + ); } // should return the variation when the user is bucketed into a variation for the experiment on the feature flag - public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucketed(){ + public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucketed() + { // return the first variation of the `test_experiment_multivariate` experiment, which is attached to the `multi_variate_feature` $experiment = $this->config->getExperimentFromKey('test_experiment_multivariate'); - $variation = $this->config->getVariationFromId('test_experiment_multivariate','122231'); + $variation = $this->config->getVariationFromId('test_experiment_multivariate', '122231'); $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') ->will($this->returnValue($variation)); @@ -679,17 +686,20 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsB $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'."); + ->with( + Logger::INFO, + "The user 'user1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []), - $expected_decision + $expected_decision, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []) ); } // should return the variation the user is bucketed into when the user is bucketed into one of the experiments - public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed(){ + public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed() + { $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); $variation = $mutex_exp->getVariations()[0]; $this->decisionServiceMock->expects($this->at(0)) @@ -703,16 +713,19 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBuck $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user_1' is bucketed into experiment 'group_experiment_1' of feature 'boolean_feature'."); + ->with( + Logger::INFO, + "The user 'user_1' is bucketed into experiment 'group_experiment_1' of feature 'boolean_feature'." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []), - $expected_decision + $expected_decision, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []) ); } - // should return nil and log a message when the user is not bucketed into any of the mutex experiments - public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed(){ + // should return nil and log a message when the user is not bucketed into any of the mutex experiments + public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() + { $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); $variation = $mutex_exp->getVariations()[0]; $this->decisionServiceMock->expects($this->at(0)) @@ -724,27 +737,29 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuc $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user_1' is not bucketed into any of the experiments using the feature 'boolean_feature'."); + ->with( + Logger::INFO, + "The user 'user_1' is not bucketed into any of the experiments using the feature 'boolean_feature'." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []), - null + null, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []) ); } - //should return the bucketed experiment and variation - public function testGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment(){ - - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock, $this->config)) - ->setMethods(array('getVariationForFeatureExperiment')) - ->getMock(); + // should return the bucketed experiment and variation + public function testGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment() + { + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->config)) + ->setMethods(array('getVariationForFeatureExperiment')) + ->getMock(); - $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); - $expected_experiment_id = $feature_flag->getExperimentIds()[0]; - $expected_experiment = $this->config->getExperimentFromId($expected_experiment_id); - $expected_variation = $expected_experiment->getVariations()[0]; - $expected_decision = [ + $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); + $expected_experiment_id = $feature_flag->getExperimentIds()[0]; + $expected_experiment = $this->config->getExperimentFromId($expected_experiment_id); + $expected_variation = $expected_experiment->getVariations()[0]; + $expected_decision = [ 'experiment' => $expected_experiment, 'variation' => $expected_variation ]; @@ -754,14 +769,14 @@ public function testGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperi ->will($this->returnValue($expected_decision)); $this->assertEquals( - $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), - $expected_decision + $expected_decision, + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []) ); } // should return the bucketed variation and null experiment - public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ - + public function testGetVariationForFeatureWhenBucketedToFeatureRollout() + { $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->config)) ->setMethods(array('getVariationForFeatureExperiment','getVariationForFeatureRollout')) @@ -773,7 +788,10 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $experiment = $rollout->getExperiments()[0]; $expected_variation = $experiment->getVariations()[0]; $expected_decision = new Decision( - $experiment->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $experiment->getId(), + $expected_variation->getId(), + Decision::DECISION_SOURCE_ROLLOUT + ); $decisionServiceMock ->method('getVariationForFeatureExperiment') @@ -785,17 +803,20 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "User 'user_1' is bucketed into rollout for feature flag 'string_single_variable_feature'."); + ->with( + Logger::INFO, + "User 'user_1' is bucketed into rollout for feature flag 'string_single_variable_feature'." + ); $this->assertEquals( - $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), - $expected_decision + $expected_decision, + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExperimentNorToFeatureRollout(){ + public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExperimentNorToFeatureRollout() + { $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->config)) ->setMethods(array('getVariationForFeatureExperiment','getVariationForFeatureRollout')) @@ -803,7 +824,7 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); - $decisionServiceMock + $decisionServiceMock ->method('getVariationForFeatureExperiment') ->will($this->returnValue(null)); @@ -813,56 +834,65 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "User 'user_1' is not bucketed into rollout for feature flag 'string_single_variable_feature'."); + ->with( + Logger::INFO, + "User 'user_1' is not bucketed into rollout for feature flag 'string_single_variable_feature'." + ); $this->assertEquals( - $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), - null + null, + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureRolloutWhenNoRolloutIsAssociatedToFeatureFlag(){ + public function testGetVariationForFeatureRolloutWhenNoRolloutIsAssociatedToFeatureFlag() + { // No rollout id is associated to boolean_feature - $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); - $this->loggerMock->expects($this->at(0)) + $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Feature flag 'boolean_feature' is not used in a rollout."); + ->with( + Logger::DEBUG, + "Feature flag 'boolean_feature' is not used in a rollout." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []), - null + null, + $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile(){ - $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); - $feature_flag = clone $feature_flag; - // Set any string which is not a rollout id in the data file - $feature_flag->setRolloutId('invalid_rollout_id'); + public function testGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() + { + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + $feature_flag = clone $feature_flag; + // Set any string which is not a rollout id in the data file + $feature_flag->setRolloutId('invalid_rollout_id'); - $this->loggerMock->expects($this->at(0)) + $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, - 'Rollout with ID "invalid_rollout_id" is not in the datafile.'); + ->with( + Logger::ERROR, + 'Rollout with ID "invalid_rollout_id" is not in the datafile.' + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []), - null + null, + $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperiment(){ + public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperiment() + { // Mock Project Config $configMock = $this->getMockBuilder(ProjectConfig::class) ->setConstructorArgs(array(DATAFILE, $this->loggerMock, new NoOpErrorHandler())) ->setMethods(array('getRolloutFromId')) - ->getMock(); + ->getMock(); $this->decisionService = new DecisionService($this->loggerMock, $configMock); @@ -877,15 +907,16 @@ public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperimen ->will($this->returnValue($experiment_less_rollout)); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', []), - null + null, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', []) ); } // ============== when the user qualifies for targeting rule (audience match) ====================== // should return the variation the user is bucketed into when the user is bucketed into the targeting rule - public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRule(){ + public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); @@ -893,7 +924,10 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $expected_variation = $experiment->getVariations()[0]; $expected_decision = new Decision( - $experiment->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $experiment->getId(), + $expected_variation->getId(), + Decision::DECISION_SOURCE_ROLLOUT + ); // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -907,28 +941,34 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Attempting to bucket user 'user_1' into rollout rule '{$experiment->getKey()}'."); + ->with( + Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment->getKey()}'." + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_decision + $expected_decision, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } // should return the variation the user is bucketed into when the user is bucketed into the "Everyone Else" rule' // and the user is not bucketed into the targeting rule - public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargetingRuleButBucketedToEveryoneElseRule(){ + public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargetingRuleButBucketedToEveryoneElseRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); $experiment0 = $rollout->getExperiments()[0]; // Everyone Else Rule $experiment2 = $rollout->getExperiments()[2]; - $expected_variation = $experiment2->getVariations()[0]; + $expected_variation = $experiment2->getVariations()[0]; $expected_decision = new Decision( - $experiment2->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $experiment2->getId(), + $expected_variation->getId(), + Decision::DECISION_SOURCE_ROLLOUT + ); // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -940,30 +980,35 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $this->bucketerMock->expects($this->at(0)) ->method('bucket') ->willReturn(null); - //Make bucket return expected variation when called second time for everyone else + // Make bucket return expected variation when called second time for everyone else $this->bucketerMock->expects($this->at(1)) ->method('bucket') ->willReturn($expected_variation); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'."); + ->with( + Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'." + ); $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now."); + ->with( + Logger::DEBUG, + "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now." + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_decision + $expected_decision, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } - // should log and return nil when the user is not bucketed into the targeting rule and + // should log and return nil when the user is not bucketed into the targeting rule and // the user is not bucketed into the "Everyone Else" rule' - public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTargetingRuleNorToEveryoneElseRule(){ + public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTargetingRuleNorToEveryoneElseRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); @@ -981,27 +1026,33 @@ public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheT $this->bucketerMock->expects($this->at(0)) ->method('bucket') ->willReturn(null); - //Make bucket return null when called second time for everyone else + // Make bucket return null when called second time for everyone else $this->bucketerMock->expects($this->at(1)) ->method('bucket') ->willReturn(null); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'."); + ->with( + Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'." + ); $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now."); + ->with( + Logger::DEBUG, + "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now." + ); $this->loggerMock->expects($this->at(2)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' was excluded from the 'Everyone Else' rule for feature flag"); + ->with( + Logger::DEBUG, + "User 'user_1' was excluded from the 'Everyone Else' rule for feature flag" + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - null + null, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } @@ -1011,18 +1062,22 @@ public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheT // should return expected variation when the user is attempted to be bucketed into all targeting rules // including Everyone Else rule - public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargetingRule(){ + public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargetingRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); $experiment0 = $rollout->getExperiments()[0]; $experiment1 = $rollout->getExperiments()[1]; // Everyone Else Rule - $experiment2 = $rollout->getExperiments()[2]; - $expected_variation = $experiment2->getVariations()[0]; + $experiment2 = $rollout->getExperiments()[2]; + $expected_variation = $experiment2->getVariations()[0]; $expected_decision = new Decision( - $experiment2->getId(), $expected_variation->getId(), Decision::DECISION_SOURCE_ROLLOUT); + $experiment2->getId(), + $expected_variation->getId(), + Decision::DECISION_SOURCE_ROLLOUT + ); // Provide null attributes so that user does not qualify for audience $user_attributes = []; @@ -1031,7 +1086,7 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - // Expect bucket to be called exactly once for the everyone else/last rule. + // Expect bucket to be called exactly once for the everyone else/last rule. // As we ignore Audience check only for thelast rule $this->bucketerMock->expects($this->exactly(1)) ->method('bucket') @@ -1039,18 +1094,21 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment0->getKey()}'."); + ->with( + Logger::DEBUG, + "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment0->getKey()}'." + ); $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment1->getKey()}'."); + ->with( + Logger::DEBUG, + "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment1->getKey()}'." + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_decision + $expected_decision, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } } - diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 1554226f..7b670d4e 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -2151,7 +2151,7 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs ->method('log') ->with( Logger::INFO, - "Variable 'double_variable' is not used in variation 'control' returning default value '14.99'." + "Variable 'double_variable' is not used in variation 'control', returning default value '14.99'." ); $this->assertSame( From e1cd2ca432636d1aab837d385efacd9aac077eb5 Mon Sep 17 00:00:00 2001 From: Owais Date: Mon, 13 Nov 2017 17:55:41 +0500 Subject: [PATCH 19/20] :ear: Stacked PRs mess --- src/Optimizely/Optimizely.php | 4 ++-- tests/OptimizelyTest.php | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 8110772a..c8a1b5eb 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -21,8 +21,8 @@ use Optimizely\Exceptions\InvalidEventTagException; use Throwable; use Monolog\Logger; -use Optimizely\DecisionService\Decision; use Optimizely\DecisionService\DecisionService; +use Optimizely\DecisionService\FeatureDecision; use Optimizely\Entity\Experiment; use Optimizely\Entity\FeatureFlag; use Optimizely\Entity\FeatureVariable; @@ -452,7 +452,7 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) return false; } - if ($decision->getSource() == Decision::DECISION_SOURCE_EXPERIMENT) { + if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_EXPERIMENT) { $experiment_id = $decision->getExperimentId(); $variation_id = $decision->getVariationId(); $experiment = $this->_config->getExperimentFromId($experiment_id); diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 7b670d4e..f9c554a9 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -18,8 +18,8 @@ use Exception; use Monolog\Logger; -use Optimizely\DecisionService\Decision; use Optimizely\DecisionService\DecisionService; +use Optimizely\DecisionService\FeatureDecision; use Optimizely\ErrorHandler\NoOpErrorHandler; use Optimizely\Event\LogEvent; use Optimizely\Exceptions\InvalidAttributeException; @@ -1858,10 +1858,10 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - $expected_decision = new Decision( + $expected_decision = new FeatureDecision( $experiment->getId(), $variation->getId(), - Decision::DECISION_SOURCE_EXPERIMENT + FeatureDecision::DECISION_SOURCE_EXPERIMENT ); $decisionServiceMock->expects($this->exactly(1)) @@ -1903,10 +1903,10 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx $rollout = $this->projectConfig->getRolloutFromId('166660'); $experiment = $rollout->getExperiments()[0]; $variation = $experiment->getVariations()[0]; - $expected_decision = new Decision( + $expected_decision = new FeatureDecision( $experiment->getId(), $variation->getId(), - Decision::DECISION_SOURCE_ROLLOUT + FeatureDecision::DECISION_SOURCE_ROLLOUT ); $decisionServiceMock->expects($this->exactly(1)) @@ -2097,10 +2097,10 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - $expected_decision = new Decision( + $expected_decision = new FeatureDecision( $experiment->getId(), $variation->getId(), - Decision::DECISION_SOURCE_EXPERIMENT + FeatureDecision::DECISION_SOURCE_EXPERIMENT ); $decisionServiceMock->expects($this->exactly(1)) @@ -2137,10 +2137,10 @@ public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUs // Mock getVariationForFeature to return experiment/variation from a different feature $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_integer_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_integer_feature', 'control'); - $expected_decision = new Decision( + $expected_decision = new FeatureDecision( $experiment->getId(), $variation->getId(), - Decision::DECISION_SOURCE_EXPERIMENT + FeatureDecision::DECISION_SOURCE_EXPERIMENT ); $decisionServiceMock->expects($this->exactly(1)) From 508f782ad9c796a2109a4c8570d09b008cff9c59 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 21 Nov 2017 01:41:33 +0500 Subject: [PATCH 20/20] :pen: nits addressed --- src/Optimizely/Optimizely.php | 4 ++-- tests/UtilsTests/VariableTypeUtilsTest.php | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index c8a1b5eb..9abf37cd 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -180,7 +180,7 @@ private function validateUserInputs($attributes, $eventTags = null) { * is one that is in "Running" state and into which the user has been bucketed. * * @param $event string Event key representing the event which needs to be recorded. - * @param $user string ID for user. + * @param $userId string ID for user. * @param $attributes array Attributes of the user. * * @return Array Of objects where each object contains the ID of the experiment to track and the ID of the variation the user is bucketed into. @@ -436,7 +436,7 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) } $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); - if ($feature_flag == new FeatureFlag) { + if ($feature_flag && (!$feature_flag->getId())) { // Error logged in ProjectConfig - getFeatureFlagFromKey return null; } diff --git a/tests/UtilsTests/VariableTypeUtilsTest.php b/tests/UtilsTests/VariableTypeUtilsTest.php index 1d52fd62..1e0374ca 100644 --- a/tests/UtilsTests/VariableTypeUtilsTest.php +++ b/tests/UtilsTests/VariableTypeUtilsTest.php @@ -47,8 +47,8 @@ public function testValueCastingToBoolean() public function testValueCastingToInteger() { - $this->assertSame($this->variableUtilObj->castStringToType('1000', 'integer'), 1000); - $this->assertSame($this->variableUtilObj->castStringToType('123', 'integer'), 123); + $this->assertSame(1000, $this->variableUtilObj->castStringToType('1000', 'integer')); + $this->assertSame(123, $this->variableUtilObj->castStringToType('123', 'integer')); // should return nulll and log a message if value can not be casted to an integer $value = '123.5'; // any string with non-decimal digits @@ -65,9 +65,9 @@ public function testValueCastingToInteger() public function testValueCastingToDouble() { - $this->assertSame($this->variableUtilObj->castStringToType('1000', 'double'), 1000.0); - $this->assertSame($this->variableUtilObj->castStringToType('3.0', 'double'), 3.0); - $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'double'), 13.37); + $this->assertSame(1000.0, $this->variableUtilObj->castStringToType('1000', 'double')); + $this->assertSame(3.0, $this->variableUtilObj->castStringToType('3.0', 'double')); + $this->assertSame(13.37, $this->variableUtilObj->castStringToType('13.37', 'double')); // should return nil and log a message if value can not be casted to a double $value = 'any-non-numeric-string'; @@ -84,9 +84,9 @@ public function testValueCastingToDouble() public function testValueCastingToString() { - $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'string'), '13.37'); - $this->assertSame($this->variableUtilObj->castStringToType('a string', 'string'), 'a string'); - $this->assertSame($this->variableUtilObj->castStringToType('3', 'string'), '3'); - $this->assertSame($this->variableUtilObj->castStringToType('false', 'string'), 'false'); + $this->assertSame('13.37', $this->variableUtilObj->castStringToType('13.37', 'string')); + $this->assertSame('a string', $this->variableUtilObj->castStringToType('a string', 'string')); + $this->assertSame('3', $this->variableUtilObj->castStringToType('3', 'string')); + $this->assertSame('false', $this->variableUtilObj->castStringToType('false', 'string')); } }