From bfb6bc478d2f7e24ed837f74f91cce30164b1af8 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 18 Nov 2020 19:04:52 +0500 Subject: [PATCH 01/21] init decide --- .../Decide/OptimizelyDecideOption.php | 27 +++++++++ src/Optimizely/Decide/OptimizelyDecision.php | 46 +++++++++++++++ .../Decide/OptimizelyDecisionMessage.php | 25 ++++++++ .../Enums/DecisionNotificationTypes.php | 1 + src/Optimizely/OptimizelyUserContext.php | 57 +++++++++++++++++++ 5 files changed, 156 insertions(+) create mode 100644 src/Optimizely/Decide/OptimizelyDecideOption.php create mode 100644 src/Optimizely/Decide/OptimizelyDecision.php create mode 100644 src/Optimizely/Decide/OptimizelyDecisionMessage.php create mode 100644 src/Optimizely/OptimizelyUserContext.php diff --git a/src/Optimizely/Decide/OptimizelyDecideOption.php b/src/Optimizely/Decide/OptimizelyDecideOption.php new file mode 100644 index 00000000..b0e21436 --- /dev/null +++ b/src/Optimizely/Decide/OptimizelyDecideOption.php @@ -0,0 +1,27 @@ +variationKey = $variationKey; + $this->enabled = $enabled; + $this->variables = $variables; + $this->ruleKey = $ruleKey; + $this->flagKey = $flagKey; + $this->userContext = $userContext; + $this->reasons = $reasons; + } + + public function jsonSerialize() + { + return get_object_vars($this); + } +} diff --git a/src/Optimizely/Decide/OptimizelyDecisionMessage.php b/src/Optimizely/Decide/OptimizelyDecisionMessage.php new file mode 100644 index 00000000..635a204a --- /dev/null +++ b/src/Optimizely/Decide/OptimizelyDecisionMessage.php @@ -0,0 +1,25 @@ +userId = $userId; + $this->userAttributes = $userAttributes; + + // todo: check cloning and if null can be passed with [] as default param + if ($userAttributes === null) { + $userAttributes = []; + } + } + + public function setAttribute($key, $value) + { + $this->userAttributes[$key] = $value; + } + + public function decide($key, $options = []) + { + } + + public function decideForKeys(array $keys, $options = []) + { + } + + public function decideAll($options = []) + { + } + + public function trackEvent($eventKey, $eventTags = []) + { + } +} From 7045bc53e911afd5d58630a2f9c3d086827349b1 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 19 Nov 2020 11:59:49 +0500 Subject: [PATCH 02/21] Added tests for UserContext and createUserContext --- src/Optimizely/Optimizely.php | 31 ++++++- src/Optimizely/OptimizelyUserContext.php | 30 +++++-- tests/OptimizelyTest.php | 65 +++++++++++++++ tests/OptimizelyUserContextTests.php | 102 +++++++++++++++++++++++ 4 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 tests/OptimizelyUserContextTests.php diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 3a84909a..6c19f96c 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -38,6 +38,7 @@ use Optimizely\Notification\NotificationCenter; use Optimizely\Notification\NotificationType; use Optimizely\OptimizelyConfig\OptimizelyConfigService; +use Optimizely\OptimizelyUserContext; use Optimizely\ProjectConfigManager\HTTPProjectConfigManager; use Optimizely\ProjectConfigManager\ProjectConfigManagerInterface; use Optimizely\ProjectConfigManager\StaticProjectConfigManager; @@ -95,6 +96,8 @@ class Optimizely */ private $_logger; + private $defaultDecideOptions; + /** * @var ProjectConfigManagerInterface */ @@ -127,7 +130,8 @@ public function __construct( UserProfileServiceInterface $userProfileService = null, ProjectConfigManagerInterface $configManager = null, NotificationCenter $notificationCenter = null, - $sdkKey = null + $sdkKey = null, + array $defaultDecideOptions = [] ) { $this->_isValid = true; $this->_eventDispatcher = $eventDispatcher ?: new DefaultEventDispatcher(); @@ -145,6 +149,8 @@ public function __construct( $this->configManager = new StaticProjectConfigManager($datafile, $skipJsonValidation, $this->_logger, $this->_errorHandler); } } + + $this->defaultDecideOptions = $defaultDecideOptions; } /** @@ -241,6 +247,29 @@ protected function sendImpressionEvent($config, $experimentKey, $variationKey, $ ); } + + public function createUserContext($userId, array $userAttributes = []) + { + // We do not check if config is ready as UserContext can be created evne when SDK is not ready. + + // validate userId + if (!$this->validateInputs( + [ + self::USER_ID => $userId + ] + ) + ) { + return null; + } + + // validate attributes + if (!$this->validateUserInputs($userAttributes)) { + return null; + } + + return new OptimizelyUserContext($this, $userId, $userAttributes); + } + /** * Buckets visitor and sends impression event to Optimizely. * diff --git a/src/Optimizely/OptimizelyUserContext.php b/src/Optimizely/OptimizelyUserContext.php index ab62db59..6e08799b 100644 --- a/src/Optimizely/OptimizelyUserContext.php +++ b/src/Optimizely/OptimizelyUserContext.php @@ -19,24 +19,21 @@ class OptimizelyUserContext { + private $optimizelyClient; private $userId; - private $userAttributes; + private $attributes; - public function __construct($userId, array $userAttributes = []) + public function __construct(Optimizely $optimizelyClient, $userId, array $attributes = []) { + $this->optimizelyClient = $optimizelyClient; $this->userId = $userId; - $this->userAttributes = $userAttributes; - - // todo: check cloning and if null can be passed with [] as default param - if ($userAttributes === null) { - $userAttributes = []; - } + $this->attributes = $attributes; } public function setAttribute($key, $value) { - $this->userAttributes[$key] = $value; + $this->attributes[$key] = $value; } public function decide($key, $options = []) @@ -54,4 +51,19 @@ public function decideAll($options = []) public function trackEvent($eventKey, $eventTags = []) { } + + public function getUserId() + { + return $this->userId; + } + + public function getAttributes() + { + return $this->attributes; + } + + public function getOptimizely() + { + return $this->optimizelyClient; + } } diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 39f3f45b..540c7838 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -16,6 +16,8 @@ */ namespace Optimizely\Tests; +// require(dirname(__FILE__).'/TestData.php'); + use Exception; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; @@ -45,6 +47,7 @@ use Optimizely\Event\Builder\EventBuilder; use Optimizely\Logger\DefaultLogger; use Optimizely\Optimizely; +use Optimizely\OptimizelyUserContext; class OptimizelyTest extends \PHPUnit_Framework_TestCase { @@ -310,6 +313,68 @@ public function testInitWithBothSdkKeyAndDatafile() $this->assertEquals('3', $optimizelyClient->configManager->getConfig()->getRevision()); } + public function testCreateUserContextInvalidOptimizelyObject() + { + $optimizely = new Optimizely('Random datafile'); + + $userCxt = $optimizely->createUserContext('test_user'); + $this->assertInstanceof(OptimizelyUserContext::class, $userCxt); + } + + public function testCreateUserContextCallsValidateInputsWithUserId() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile)) + ->setMethods(array('validateInputs')) + ->getMock(); + + $userId = 'test_user'; + $inputArray = [ + Optimizely::USER_ID => $userId + ]; + + // assert that validateInputs gets called with exactly same keys + $optimizelyMock->expects($this->once()) + ->method('validateInputs') + ->with($inputArray) + ->willReturn(false); + + $this->assertNull($optimizelyMock->createUserContext($userId)); + } + + public function testCreateUserContextWithNonArrayAttributes() + { + try { + $this->optimizelyObject->createUserContext('test_user', 42); + } catch (Exception $exception) { + return; + } catch (TypeError $exception) { + return; + } + + $this->fail('Unexpected behavior. UserContext should have thrown an error.'); + } + + public function testCreateUserContextInvalidAttributes() + { + $this->loggerMock->expects($this->exactly(1)) + ->method('log'); + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, 'Provided attributes are in an invalid format.'); + + $errorHandlerMock = $this->getMockBuilder(NoOpErrorHandler::class) + ->setMethods(array('handleError')) + ->getMock(); + $errorHandlerMock->expects($this->once()) + ->method('handleError') + ->with(new InvalidAttributeException('Provided attributes are in an invalid format.')); + + $optimizely = new Optimizely($this->datafile, null, $this->loggerMock, $errorHandlerMock); + + $this->assertNull($optimizely->createUserContext('test_user', [5,6,7])); + } + public function testActivateInvalidOptimizelyObject() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) diff --git a/tests/OptimizelyUserContextTests.php b/tests/OptimizelyUserContextTests.php new file mode 100644 index 00000000..95213583 --- /dev/null +++ b/tests/OptimizelyUserContextTests.php @@ -0,0 +1,102 @@ +datafile = DATAFILE; + + // Mock Logger + $this->loggerMock = $this->getMockBuilder(NoOpLogger::class) + ->setMethods(array('log')) + ->getMock(); + + + $this->optimizelyObject = new Optimizely($this->datafile, null, $this->loggerMock); + } + public function testOptimizelyUserContextIsCreatedWithExpectedValues() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + $optUserContext = new OptimizelyUserContext($this->optimizelyObject, $userId, $attributes); + + $this->assertEquals($userId, $optUserContext->getUserId()); + $this->assertEquals($attributes, $optUserContext->getAttributes()); + $this->assertSame($this->optimizelyObject, $optUserContext->getOptimizely()); + } + + public function testOptimizelyUserContextThrowsErrorWhenNonArrayPassedAsAttributes() + { + $userId = 'test_user'; + + try { + $optUserContext = new OptimizelyUserContext($this->optimizelyObject, $userId, 'HelloWorld'); + } catch (Exception $exception) { + return; + } catch (TypeError $exception) { + return; + } + + $this->fail('Unexpected behavior. UserContext should have thrown an error.'); + } + + public function testSetAttribute() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + $optUserContext = new OptimizelyUserContext($this->optimizelyObject, $userId, $attributes); + + $this->assertEquals($attributes, $optUserContext->getAttributes()); + + $optUserContext->setAttribute('color', 'red'); + $this->assertEquals([ + "browser" => "chrome", + "color" => "red" + ], $optUserContext->getAttributes()); + } + + public function testSetAttributeOverridesValueOfExistingKey() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + $optUserContext = new OptimizelyUserContext($this->optimizelyObject, $userId, $attributes); + + $this->assertEquals($attributes, $optUserContext->getAttributes()); + + $optUserContext->setAttribute('browser', 'firefox'); + $this->assertEquals(["browser" => "firefox"], $optUserContext->getAttributes()); + } +} From cf08f7a9cac31cc707ece74678b4857e6ee25340 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 19 Nov 2020 12:29:19 +0500 Subject: [PATCH 03/21] changes in bucketer --- src/Optimizely/Bucketer.php | 44 ++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Optimizely/Bucketer.php b/src/Optimizely/Bucketer.php index 1cc6a864..51f1871c 100644 --- a/src/Optimizely/Bucketer.php +++ b/src/Optimizely/Bucketer.php @@ -112,12 +112,14 @@ protected function generateBucketValue($bucketingKey) * * @return string ID representing experiment or variation. */ - private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations) + private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations, &$decideReasons = null) { // Generate the bucketing key based on combination of user ID and experiment ID or group ID. $bucketingKey = $bucketingId.$parentId; $bucketingNumber = $this->generateBucketValue($bucketingKey); - $this->_logger->log(Logger::DEBUG, sprintf('Assigned bucket %s to user "%s" with bucketing ID "%s".', $bucketingNumber, $userId, $bucketingId)); + $message = sprintf('Assigned bucket %s to user "%s" with bucketing ID "%s".', $bucketingNumber, $userId, $bucketingId); + $this->_logger->log(Logger::DEBUG, $message); + $decideReasons[] = $message; foreach ($trafficAllocations as $trafficAllocation) { $currentEnd = $trafficAllocation->getEndOfRange(); @@ -139,7 +141,7 @@ private function findBucket($bucketingId, $userId, $parentId, $trafficAllocation * * @return Variation Variation which will be shown to the user. */ - public function bucket(ProjectConfigInterface $config, Experiment $experiment, $bucketingId, $userId) + public function bucket(ProjectConfigInterface $config, Experiment $experiment, $bucketingId, $userId, &$decideReasons = null) { if (is_null($experiment->getKey())) { return null; @@ -156,32 +158,34 @@ public function bucket(ProjectConfigInterface $config, Experiment $experiment, $ $userExperimentId = $this->findBucket($bucketingId, $userId, $group->getId(), $group->getTrafficAllocation()); if (empty($userExperimentId)) { - $this->_logger->log(Logger::INFO, sprintf('User "%s" is in no experiment.', $userId)); + $message = sprintf('User "%s" is in no experiment.', $userId); + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; return null; } if ($userExperimentId != $experiment->getId()) { - $this->_logger->log( - Logger::INFO, - sprintf( - 'User "%s" is not in experiment %s of group %s.', - $userId, - $experiment->getKey(), - $experiment->getGroupId() - ) + $message = sprintf( + 'User "%s" is not in experiment %s of group %s.', + $userId, + $experiment->getKey(), + $experiment->getGroupId() ); + + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; return null; } - $this->_logger->log( - Logger::INFO, - sprintf( - 'User "%s" is in experiment %s of group %s.', - $userId, - $experiment->getKey(), - $experiment->getGroupId() - ) + $message = sprintf( + 'User "%s" is in experiment %s of group %s.', + $userId, + $experiment->getKey(), + $experiment->getGroupId() ); + + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; } // Bucket user if not in whitelist and in group (if any). From b62b1b232cac1b10fbf9636265c5f75d87b4d8c7 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 19 Nov 2020 14:27:41 +0500 Subject: [PATCH 04/21] changes in DS --- src/Optimizely/Bucketer.php | 4 +- .../DecisionService/DecisionService.php | 182 +++++++++++------- 2 files changed, 111 insertions(+), 75 deletions(-) diff --git a/src/Optimizely/Bucketer.php b/src/Optimizely/Bucketer.php index 51f1871c..414f2429 100644 --- a/src/Optimizely/Bucketer.php +++ b/src/Optimizely/Bucketer.php @@ -112,7 +112,7 @@ protected function generateBucketValue($bucketingKey) * * @return string ID representing experiment or variation. */ - private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations, &$decideReasons = null) + private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations, &$decideReasons = []) { // Generate the bucketing key based on combination of user ID and experiment ID or group ID. $bucketingKey = $bucketingId.$parentId; @@ -141,7 +141,7 @@ private function findBucket($bucketingId, $userId, $parentId, $trafficAllocation * * @return Variation Variation which will be shown to the user. */ - public function bucket(ProjectConfigInterface $config, Experiment $experiment, $bucketingId, $userId, &$decideReasons = null) + public function bucket(ProjectConfigInterface $config, Experiment $experiment, $bucketingId, $userId, &$decideReasons = []) { if (is_null($experiment->getKey())) { return null; diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index f4c7d410..4b7403c5 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -95,7 +95,7 @@ public function __construct(LoggerInterface $logger, UserProfileServiceInterface * * @return String representing bucketing ID if it is a String type in attributes else return user ID. */ - protected function getBucketingId($userId, $userAttributes) + protected function getBucketingId($userId, $userAttributes, &$decideReasons = []) { $bucketingIdKey = ControlAttributes::BUCKETING_ID; @@ -103,7 +103,10 @@ protected function getBucketingId($userId, $userAttributes) if (is_string($userAttributes[$bucketingIdKey])) { return $userAttributes[$bucketingIdKey]; } - $this->_logger->log(Logger::WARNING, 'Bucketing ID attribute is not a string. Defaulted to user ID.'); + + $message = 'Bucketing ID attribute is not a string. Defaulted to user ID.'; + $this->_logger->log(Logger::WARNING, $message); + $decideReasons[] = $message; } return $userId; } @@ -118,34 +121,37 @@ protected function getBucketingId($userId, $userAttributes) * * @return Variation Variation which the user is bucketed into. */ - public function getVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId, $attributes = null) + public function getVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId, $attributes = null, $decideOptions = [], &$decideReasons = []) { - $bucketingId = $this->getBucketingId($userId, $attributes); + $bucketingId = $this->getBucketingId($userId, $attributes, $decideReasons); if (!$experiment->isExperimentRunning()) { - $this->_logger->log(Logger::INFO, sprintf('Experiment "%s" is not running.', $experiment->getKey())); + $message = sprintf('Experiment "%s" is not running.', $experiment->getKey()); + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; return null; } // check if a forced variation is set - $forcedVariation = $this->getForcedVariation($projectConfig, $experiment->getKey(), $userId); + $forcedVariation = $this->getForcedVariation($projectConfig, $experiment->getKey(), $userId, $decideReasons); if (!is_null($forcedVariation)) { return $forcedVariation; } // check if the user has been whitelisted - $variation = $this->getWhitelistedVariation($projectConfig, $experiment, $userId); + $variation = $this->getWhitelistedVariation($projectConfig, $experiment, $userId, $decideReasons); if (!is_null($variation)) { return $variation; } // check for sticky bucketing + // todo: Add logic for IGNORE_USER_PROFILE $userProfile = new UserProfile($userId); if (!is_null($this->_userProfileService)) { - $storedUserProfile = $this->getStoredUserProfile($userId); + $storedUserProfile = $this->getStoredUserProfile($userId, $decideReasons); if (!is_null($storedUserProfile)) { $userProfile = $storedUserProfile; - $variation = $this->getStoredVariation($projectConfig, $experiment, $userProfile); + $variation = $this->getStoredVariation($projectConfig, $experiment, $userProfile, $decideReasons); if (!is_null($variation)) { return $variation; } @@ -153,27 +159,33 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ } if (!Validator::doesUserMeetAudienceConditions($projectConfig, $experiment, $attributes, $this->_logger)) { + $message = sprintf('User "%s" does not meet conditions to be in experiment "%s".', $userId, $experiment->getKey()); $this->_logger->log( Logger::INFO, - sprintf('User "%s" does not meet conditions to be in experiment "%s".', $userId, $experiment->getKey()) + $message ); + $decideReasons[] = $message; return null; } - $variation = $this->_bucketer->bucket($projectConfig, $experiment, $bucketingId, $userId); + $variation = $this->_bucketer->bucket($projectConfig, $experiment, $bucketingId, $userId, $decideReasons); if ($variation === null) { - $this->_logger->log(Logger::INFO, sprintf('User "%s" is in no variation.', $userId)); + $message = sprintf('User "%s" is in no variation.', $userId); + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; } else { - $this->saveVariation($experiment, $variation, $userProfile); + $this->saveVariation($experiment, $variation, $userProfile, $decideReasons); + $message = sprintf( + 'User "%s" is in variation %s of experiment %s.', + $userId, + $variation->getKey(), + $experiment->getKey() + ); $this->_logger->log( Logger::INFO, - sprintf( - 'User "%s" is in variation %s of experiment %s.', - $userId, - $variation->getKey(), - $experiment->getKey() - ) + $message ); + $decideReasons[] = $message; } return $variation; @@ -189,33 +201,37 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ * @return Decision if getVariationForFeatureExperiment or getVariationForFeatureRollout returns a Decision * null otherwise */ - public function getVariationForFeature(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes) + public function getVariationForFeature(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = [], &$decideReasons = []) { //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($projectConfig, $featureFlag, $userId, $userAttributes); + $decision = $this->getVariationForFeatureExperiment($projectConfig, $featureFlag, $userId, $userAttributes, $decideOptions, $decideReasons); 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($projectConfig, $featureFlag, $userId, $userAttributes); + $decision = $this->getVariationForFeatureRollout($projectConfig, $featureFlag, $userId, $userAttributes, $decideReasons); if ($decision) { + $message = "User '{$userId}' is bucketed into rollout for feature flag '{$featureFlag->getKey()}'."; $this->_logger->log( Logger::INFO, - "User '{$userId}' is bucketed into rollout for feature flag '{$featureFlag->getKey()}'." + $message ); + $decideReasons[] = $message; return $decision; } + $message = "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()}'." + $message ); + $decideReasons[] = $message; return new FeatureDecision(null, null, FeatureDecision::DECISION_SOURCE_ROLLOUT); } @@ -230,17 +246,19 @@ public function getVariationForFeature(ProjectConfigInterface $projectConfig, Fe * @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(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes) + public function getVariationForFeatureExperiment(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = [], &$decideReasons = []) { $featureFlagKey = $featureFlag->getKey(); $experimentIds = $featureFlag->getExperimentIds(); // Check if there are any experiment IDs inside feature flag if (empty($experimentIds)) { + $message = "The feature flag '{$featureFlagKey}' is not used in any experiments."; $this->_logger->log( Logger::DEBUG, - "The feature flag '{$featureFlagKey}' is not used in any experiments." + $message ); + $decideReasons[] = $message; return null; } @@ -252,21 +270,25 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project continue; } - $variation = $this->getVariation($projectConfig, $experiment, $userId, $userAttributes); + $variation = $this->getVariation($projectConfig, $experiment, $userId, $userAttributes, $decideOptions, $decideReasons); if ($variation && $variation->getKey()) { + $message = "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$featureFlagKey}'."; $this->_logger->log( Logger::INFO, - "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$featureFlagKey}'." + $message ); + $decideReasons[] = $message; return new FeatureDecision($experiment, $variation, FeatureDecision::DECISION_SOURCE_FEATURE_TEST); } } + $message = "The user '{$userId}' is not bucketed into any of the experiments using the feature '{$featureFlagKey}'."; $this->_logger->log( Logger::INFO, - "The user '{$userId}' is not bucketed into any of the experiments using the feature '{$featureFlagKey}'." + $message ); + $decideReasons[] = $message; return null; } @@ -285,16 +307,18 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project * no rollout found against the rollout ID or * no variation is returned for the user */ - public function getVariationForFeatureRollout(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes) + public function getVariationForFeatureRollout(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, &$decideReasons = []) { - $bucketing_id = $this->getBucketingId($userId, $userAttributes); + $bucketing_id = $this->getBucketingId($userId, $userAttributes, $decideReasons); $featureFlagKey = $featureFlag->getKey(); $rollout_id = $featureFlag->getRolloutId(); if (empty($rollout_id)) { + $message = "Feature flag '{$featureFlagKey}' is not used in a rollout."; $this->_logger->log( Logger::DEBUG, - "Feature flag '{$featureFlagKey}' is not used in a rollout." + $message ); + $decideReasons[] = $message; return null; } $rollout = $projectConfig->getRolloutFromId($rollout_id); @@ -314,16 +338,18 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon // Evaluate if user meets the audience condition of this rollout rule if (!Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', $i + 1)) { + $message = sprintf("User '%s' does not meet conditions for targeting rule %s.", $userId, $i+1); $this->_logger->log( Logger::DEBUG, - sprintf("User '%s' does not meet conditions for targeting rule %s.", $userId, $i+1) + $message ); + $decideReasons[] = $message; // Evaluate this user for the next rule continue; } // Evaluate if user satisfies the traffic allocation for this rollout rule - $variation = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId); + $variation = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId, $decideReasons); if ($variation && $variation->getKey()) { return new FeatureDecision($rolloutRule, $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT); } @@ -334,14 +360,16 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon // Evaluate if user meets the audience condition of Everyone Else Rule / Last Rule now if (!Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', 'Everyone Else')) { + $message = sprintf("User '%s' does not meet conditions for targeting rule 'Everyone Else'.", $userId); $this->_logger->log( Logger::DEBUG, - sprintf("User '%s' does not meet conditions for targeting rule 'Everyone Else'.", $userId) + $message ); + $decideReasons[] = $message; return null; } - $variation = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId); + $variation = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId, $decideReasons); if ($variation && $variation->getKey()) { return new FeatureDecision($rolloutRule, $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT); } @@ -358,7 +386,7 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon * * @return Variation The variation which the given user and experiment should be forced into. */ - public function getForcedVariation(ProjectConfigInterface $projectConfig, $experimentKey, $userId) + public function getForcedVariation(ProjectConfigInterface $projectConfig, $experimentKey, $userId, &$decideReasons = []) { if (!isset($this->_forcedVariationMap[$userId])) { $this->_logger->log(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); @@ -375,7 +403,9 @@ public function getForcedVariation(ProjectConfigInterface $projectConfig, $exper } if (!isset($experimentToVariationMap[$experimentId])) { - $this->_logger->log(Logger::DEBUG, sprintf('No experiment "%s" mapped to user "%s" in the forced variation map.', $experimentKey, $userId)); + $message = sprintf('No experiment "%s" mapped to user "%s" in the forced variation map.', $experimentKey, $userId); + $this->_logger->log(Logger::DEBUG, $message); + $decideReasons[] = $message; return null; } @@ -383,7 +413,9 @@ public function getForcedVariation(ProjectConfigInterface $projectConfig, $exper $variation = $projectConfig->getVariationFromId($experimentKey, $variationId); $variationKey = $variation->getKey(); - $this->_logger->log(Logger::DEBUG, sprintf('Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map', $variationKey, $experimentKey, $userId)); + $message = sprintf('Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map', $variationKey, $experimentKey, $userId); + $this->_logger->log(Logger::DEBUG, $message); + $decideReasons[] = $message; return $variation; } @@ -445,7 +477,7 @@ public function setForcedVariation(ProjectConfigInterface $projectConfig, $exper * * @return null|Variation Representing the variation the user is forced into. */ - private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId) + private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId, &$decideReasons = []) { // Check if user is whitelisted for a variation. $forcedVariations = $experiment->getForcedVariations(); @@ -453,10 +485,10 @@ private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, $variationKey = $forcedVariations[$userId]; $variation = $projectConfig->getVariationFromKey($experiment->getKey(), $variationKey); if ($variationKey && !empty($variation->getKey())) { - $this->_logger->log( - Logger::INFO, - sprintf('User "%s" is forced in variation "%s" of experiment "%s".', $userId, $variationKey, $experiment->getKey()) - ); + $message = sprintf('User "%s" is forced in variation "%s" of experiment "%s".', $userId, $variationKey, $experiment->getKey()); + + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; } else { return null; } @@ -472,7 +504,7 @@ private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, * * @return null|UserProfile the stored user profile. */ - private function getStoredUserProfile($userId) + private function getStoredUserProfile($userId, &$decideReasons = []) { if (is_null($this->_userProfileService)) { return null; @@ -494,10 +526,9 @@ private function getStoredUserProfile($userId) ); } } catch (Exception $e) { - $this->_logger->log( - Logger::ERROR, - sprintf('The User Profile Service lookup method failed: %s.', $e->getMessage()) - ); + $message = sprintf('The User Profile Service lookup method failed: %s.', $e->getMessage()); + $this->_logger->log(Logger::ERROR, $message); + $decideReasons[] = $message; } return null; @@ -512,7 +543,7 @@ private function getStoredUserProfile($userId) * * @return null|Variation the stored variation or null if not found. */ - private function getStoredVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, UserProfile $userProfile) + private function getStoredVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, UserProfile $userProfile, &$decideReasons = []) { $experimentKey = $experiment->getKey(); $userId = $userProfile->getUserId(); @@ -528,15 +559,20 @@ private function getStoredVariation(ProjectConfigInterface $projectConfig, Exper $variation = $projectConfig->getVariationFromId($experimentKey, $variationId); if (!($variation->getId())) { + $message = sprintf( + 'User "%s" was previously bucketed into variation with ID "%s" for experiment "%s", but no matching variation was found for that user. We will re-bucket the user.', + $userId, + $variationId, + $experimentKey + ); + $this->_logger->log( Logger::INFO, - sprintf( - 'User "%s" was previously bucketed into variation with ID "%s" for experiment "%s", but no matching variation was found for that user. We will re-bucket the user.', - $userId, - $variationId, - $experimentKey - ) + $message ); + + $decideReasons[] = $message; + return null; } @@ -559,7 +595,7 @@ private function getStoredVariation(ProjectConfigInterface $projectConfig, Exper * @param $variation Variation Variation the user is bucketed into. * @param $userProfile UserProfile User profile object to which we are persisting the variation assignment. */ - private function saveVariation(Experiment $experiment, Variation $variation, UserProfile $userProfile) + private function saveVariation(Experiment $experiment, Variation $variation, UserProfile $userProfile, &$decideReasons = []) { if (is_null($this->_userProfileService)) { return; @@ -579,25 +615,25 @@ private function saveVariation(Experiment $experiment, Variation $variation, Use try { $this->_userProfileService->save($userProfileMap); - $this->_logger->log( - Logger::INFO, - sprintf( - 'Saved variation "%s" of experiment "%s" for user "%s".', - $variation->getKey(), - $experiment->getKey(), - $userProfile->getUserId() - ) + $message = sprintf( + 'Saved variation "%s" of experiment "%s" for user "%s".', + $variation->getKey(), + $experiment->getKey(), + $userProfile->getUserId() ); + + $this->_logger->log(Logger::INFO, $message); + $decideReasons[] = $message; } catch (Exception $e) { - $this->_logger->log( - Logger::WARNING, - sprintf( - 'Failed to save variation "%s" of experiment "%s" for user "%s".', - $variation->getKey(), - $experiment->getKey(), - $userProfile->getUserId() - ) + $message = sprintf( + 'Failed to save variation "%s" of experiment "%s" for user "%s".', + $variation->getKey(), + $experiment->getKey(), + $userProfile->getUserId() ); + + $this->_logger->log(Logger::WARNING, $message); + $decideReasons[] = $message; } } } From e93d8b831e9445eba5dc4fa6a8e4a308eb2b6875 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 19 Nov 2020 15:46:21 +0500 Subject: [PATCH 05/21] WIP --- src/Optimizely/Decide/OptimizelyDecision.php | 17 +++- src/Optimizely/Optimizely.php | 97 ++++++++++++++++++++ src/Optimizely/OptimizelyUserContext.php | 12 ++- 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/src/Optimizely/Decide/OptimizelyDecision.php b/src/Optimizely/Decide/OptimizelyDecision.php index 097c9290..7e6795ee 100644 --- a/src/Optimizely/Decide/OptimizelyDecision.php +++ b/src/Optimizely/Decide/OptimizelyDecision.php @@ -28,15 +28,22 @@ class OptimizelyDecision private $reasons; - public function __construct($variationKey, $enabled, $variables, $ruleKey, $flagKey, $userContext, $reasons) - { + public function __construct( + $variationKey = null, + $enabled = null, + $variables = null, + $ruleKey = null, + $flagKey = null, + $userContext = null, + $reasons = null + ) { $this->variationKey = $variationKey; - $this->enabled = $enabled; - $this->variables = $variables; + $this->enabled = $enabled === null ? false : $enabled; + $this->variables = $variables === null ? [] : $variables; $this->ruleKey = $ruleKey; $this->flagKey = $flagKey; $this->userContext = $userContext; - $this->reasons = $reasons; + $this->reasons = $reasons === null ? [] : $reasons; } public function jsonSerialize() diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 6c19f96c..0930fc49 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -23,6 +23,9 @@ use Optimizely\Exceptions\InvalidEventTagException; use Throwable; use Monolog\Logger; +use Optimizely\Decide\OptimizelyDecideOption; +use Optimizely\Decide\OptimizelyDecision; +use Optimizely\Decide\OptimizelyDecisionMessage; use Optimizely\DecisionService\DecisionService; use Optimizely\DecisionService\FeatureDecision; use Optimizely\Entity\Experiment; @@ -270,6 +273,100 @@ public function createUserContext($userId, array $userAttributes = []) return new OptimizelyUserContext($this, $userId, $userAttributes); } + + public function decide(OptimizelyUserContext $userContext, $key, array $decideOptions = []) + { + $decideReasons = []; + + // check if SDK is ready + $config = $this->getConfig(); + if ($config === null) { + $this->_logger->log(Logger::ERROR, sprintf(Errors::INVALID_DATAFILE, __FUNCTION__)); + $decideReasons[] = OptimizelyDecisionMessage::SDK_NOT_READY; + return new OptimizelyDecision(null, null, null, null, key, $userContext, reasons); + } + + // validate that key is a string + if (!$this->validateInputs( + [ + self::FEATURE_FLAG_KEY =>$key + ] + ) + ) { + $errorMessage = sprintf(OptimizelyDecisionMessage::FLAG_KEY_INVALID, $key); + $this->_logger->log(Logger::ERROR, $errorMessage); + $decideReasons[] = $errorMessage; + return new OptimizelyDecision(null, null, null, null, key, $userContext, reasons); + } + + + // validate that key maps to a feature flag + $featureFlag = $config->getFeatureFlagFromKey($key); + if ($featureFlag && (!$featureFlag->getId())) { + // Error logged in DatafileProjectConfig - getFeatureFlagFromKey + $decideReasons[] = sprintf(OptimizelyDecisionMessage::FLAG_KEY_INVALID, $key); + return new OptimizelyDecision(null, null, null, null, key, $userContext, reasons); + } + + // merge decide options and default decide options + $decideOptions += $this->defaultDecideOptions; + + // create optimizely decision result + $userId = $userContext->getUserId(); + $userAttributes = $userContext->getAttributes(); + $variationKey = null; + $featureEnabled = false; + $ruleKey = null; + $flagKey = key; + $allVariables = []; + $decisionEventDispatched = false; + + + // Copied from is FeatureEnabled + + $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes); + $variation = $decision->getVariation(); + + if ($config->getSendFlagDecisions() && ($decision->getSource() == FeatureDecision::DECISION_SOURCE_ROLLOUT || !$variation)) { + if ($variation) { + $featureEnabled = $variation->getFeatureEnabled(); + } + $ruleKey = $decision->getExperiment() ? $decision->getExperiment()->getKey() : ''; + $this->sendImpressionEvent($config, $ruleKey, $variation ? $variation->getKey() : '', $featureFlagKey, $ruleKey, $decision->getSource(), $featureEnabled, $userId, $attributes); + } + + if ($variation) { + $experimentKey = $decision->getExperiment()->getKey(); + $featureEnabled = $variation->getFeatureEnabled(); + if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_FEATURE_TEST) { + $sourceInfo = (object) array( + 'experimentKey'=> $experimentKey, + 'variationKey'=> $variation->getKey() + ); + + $this->sendImpressionEvent($config, $experimentKey, $variation->getKey(), $featureFlagKey, $experimentKey, $decision->getSource(), $featureEnabled, $userId, $attributes); + } else { + $this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); + } + } + + $attributes = is_null($attributes) ? [] : $attributes; + $this->notificationCenter->sendNotifications( + NotificationType::DECISION, + array( + DecisionNotificationTypes::FEATURE, + $userId, + $attributes, + (object) array( + 'featureKey'=>$featureFlagKey, + 'featureEnabled'=> $featureEnabled, + 'source'=> $decision->getSource(), + 'sourceInfo'=> isset($sourceInfo) ? $sourceInfo : (object) array() + ) + ) + ); + } + /** * Buckets visitor and sends impression event to Optimizely. * diff --git a/src/Optimizely/OptimizelyUserContext.php b/src/Optimizely/OptimizelyUserContext.php index 6e08799b..8df393b7 100644 --- a/src/Optimizely/OptimizelyUserContext.php +++ b/src/Optimizely/OptimizelyUserContext.php @@ -36,20 +36,24 @@ public function setAttribute($key, $value) $this->attributes[$key] = $value; } - public function decide($key, $options = []) + public function decide($key, array $options = []) { + return $optimizelyClient->decide($this, $key, $options) } - public function decideForKeys(array $keys, $options = []) + public function decideForKeys(array $keys, array $options = []) { + return $optimizelyClient->decideForKeys($this, $keys, $options); } - public function decideAll($options = []) + public function decideAll(array $options = []) { + return $this->optimizelyClient->decideAll($this, $keys); } - public function trackEvent($eventKey, $eventTags = []) + public function trackEvent($eventKey, array $eventTags = []) { + return $this->optimizelyClient->track($eventKey, $this->userId, $this->attributes, $eventTags); } public function getUserId() From e9a281ce885e4d171d3c661d8a9f9d04cc269e06 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 23 Nov 2020 14:24:52 +0500 Subject: [PATCH 06/21] Decide verified with FSC PR --- src/Optimizely/Decide/OptimizelyDecision.php | 37 +++++- .../DecisionService/DecisionService.php | 24 ++-- .../DecisionService/FeatureDecision.php | 2 +- src/Optimizely/Optimizely.php | 108 ++++++++++++------ src/Optimizely/OptimizelyUserContext.php | 14 ++- tests/OptimizelyTest.php | 97 ++++++++++++++++ 6 files changed, 234 insertions(+), 48 deletions(-) diff --git a/src/Optimizely/Decide/OptimizelyDecision.php b/src/Optimizely/Decide/OptimizelyDecision.php index 7e6795ee..85ed10cb 100644 --- a/src/Optimizely/Decide/OptimizelyDecision.php +++ b/src/Optimizely/Decide/OptimizelyDecision.php @@ -17,7 +17,7 @@ namespace Optimizely\Decide; -class OptimizelyDecision +class OptimizelyDecision implements \JsonSerializable { private $variationKey; private $enabled; @@ -46,6 +46,41 @@ public function __construct( $this->reasons = $reasons === null ? [] : $reasons; } + public function getVariationKey() + { + return $this->variationKey; + } + + public function getEnabled() + { + return $this->enabled; + } + + public function getVariables() + { + return $this->variables; + } + + public function getRuleKey() + { + return $this->ruleKey; + } + + public function getFlagKey() + { + return $this->flagKey; + } + + public function getUserContext() + { + return $this->userContext; + } + + public function getReasons() + { + return $this->reasons; + } + public function jsonSerialize() { return get_object_vars($this); diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 4b7403c5..5fa4425b 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -20,6 +20,7 @@ use Monolog\Logger; use Optimizely\Bucketer; use Optimizely\Config\ProjectConfigInterface; +use Optimizely\Decide\OptimizelyDecideOption; use Optimizely\Entity\Experiment; use Optimizely\Entity\FeatureFlag; use Optimizely\Entity\Rollout; @@ -145,15 +146,16 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ } // check for sticky bucketing - // todo: Add logic for IGNORE_USER_PROFILE - $userProfile = new UserProfile($userId); - if (!is_null($this->_userProfileService)) { - $storedUserProfile = $this->getStoredUserProfile($userId, $decideReasons); - if (!is_null($storedUserProfile)) { - $userProfile = $storedUserProfile; - $variation = $this->getStoredVariation($projectConfig, $experiment, $userProfile, $decideReasons); - if (!is_null($variation)) { - return $variation; + if (!in_array(OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE, $decideOptions)) { + $userProfile = new UserProfile($userId); + if (!is_null($this->_userProfileService)) { + $storedUserProfile = $this->getStoredUserProfile($userId, $decideReasons); + if (!is_null($storedUserProfile)) { + $userProfile = $storedUserProfile; + $variation = $this->getStoredVariation($projectConfig, $experiment, $userProfile, $decideReasons); + if (!is_null($variation)) { + return $variation; + } } } } @@ -174,7 +176,9 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ $this->_logger->log(Logger::INFO, $message); $decideReasons[] = $message; } else { - $this->saveVariation($experiment, $variation, $userProfile, $decideReasons); + if (!in_array(OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE, $decideOptions)) { + $this->saveVariation($experiment, $variation, $userProfile, $decideReasons); + } $message = sprintf( 'User "%s" is in variation %s of experiment %s.', $userId, diff --git a/src/Optimizely/DecisionService/FeatureDecision.php b/src/Optimizely/DecisionService/FeatureDecision.php index 9aac1305..d100c585 100644 --- a/src/Optimizely/DecisionService/FeatureDecision.php +++ b/src/Optimizely/DecisionService/FeatureDecision.php @@ -20,7 +20,7 @@ class FeatureDecision { const DECISION_SOURCE_FEATURE_TEST = 'feature-test'; const DECISION_SOURCE_ROLLOUT = 'rollout'; - const DECITION_SOURCE_EXPERIMENT = 'experiment'; + const DECISION_SOURCE_EXPERIMENT = 'experiment'; /** * The experiment in this decision. diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 0930fc49..96c76b1a 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -273,7 +273,6 @@ public function createUserContext($userId, array $userAttributes = []) return new OptimizelyUserContext($this, $userId, $userAttributes); } - public function decide(OptimizelyUserContext $userContext, $key, array $decideOptions = []) { $decideReasons = []; @@ -283,20 +282,20 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp if ($config === null) { $this->_logger->log(Logger::ERROR, sprintf(Errors::INVALID_DATAFILE, __FUNCTION__)); $decideReasons[] = OptimizelyDecisionMessage::SDK_NOT_READY; - return new OptimizelyDecision(null, null, null, null, key, $userContext, reasons); + return new OptimizelyDecision(null, null, null, null, $key, $userContext, $decideReasons); } // validate that key is a string if (!$this->validateInputs( [ - self::FEATURE_FLAG_KEY =>$key + self::FEATURE_FLAG_KEY => $key ] ) ) { $errorMessage = sprintf(OptimizelyDecisionMessage::FLAG_KEY_INVALID, $key); $this->_logger->log(Logger::ERROR, $errorMessage); $decideReasons[] = $errorMessage; - return new OptimizelyDecision(null, null, null, null, key, $userContext, reasons); + return new OptimizelyDecision(null, null, null, null, $key, $userContext, $decideReasons); } @@ -305,7 +304,7 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp if ($featureFlag && (!$featureFlag->getId())) { // Error logged in DatafileProjectConfig - getFeatureFlagFromKey $decideReasons[] = sprintf(OptimizelyDecisionMessage::FLAG_KEY_INVALID, $key); - return new OptimizelyDecision(null, null, null, null, key, $userContext, reasons); + return new OptimizelyDecision(null, null, null, null, $key, $userContext, $decideReasons); } // merge decide options and default decide options @@ -317,54 +316,97 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp $variationKey = null; $featureEnabled = false; $ruleKey = null; - $flagKey = key; + $flagKey = $key; $allVariables = []; $decisionEventDispatched = false; - - // Copied from is FeatureEnabled - - $decision = $this->_decisionService->getVariationForFeature($config, $featureFlag, $userId, $attributes); + // get decision + $decision = $this->_decisionService->getVariationForFeature( + $config, + $featureFlag, + $userId, + $userAttributes, + $decideOptions, + $decideReasons + ); $variation = $decision->getVariation(); - if ($config->getSendFlagDecisions() && ($decision->getSource() == FeatureDecision::DECISION_SOURCE_ROLLOUT || !$variation)) { - if ($variation) { - $featureEnabled = $variation->getFeatureEnabled(); - } - $ruleKey = $decision->getExperiment() ? $decision->getExperiment()->getKey() : ''; - $this->sendImpressionEvent($config, $ruleKey, $variation ? $variation->getKey() : '', $featureFlagKey, $ruleKey, $decision->getSource(), $featureEnabled, $userId, $attributes); - } - if ($variation) { - $experimentKey = $decision->getExperiment()->getKey(); + $variationKey = $variation->getKey(); $featureEnabled = $variation->getFeatureEnabled(); - if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_FEATURE_TEST) { - $sourceInfo = (object) array( - 'experimentKey'=> $experimentKey, - 'variationKey'=> $variation->getKey() + $ruleKey = $decision->getExperiment()->getKey(); + } else { + $variationKey = null; + $ruleKey = null; + } + + // send impression only if decide options do not contain DISABLE_DECISION_EVENT + if (!in_array(OptimizelyDecideOption::DISABLE_DECISION_EVENT, $decideOptions)) { + $sendFlagDecisions = $config->getSendFlagDecisions(); + $source = $decision->getSource(); + + // send impression for rollout when sendFlagDecisions is enabled. + if ($source == FeatureDecision::DECISION_SOURCE_FEATURE_TEST || $sendFlagDecisions) { + $this->sendImpressionEvent( + $config, + $ruleKey, + $variationKey, + $flagKey, + $ruleKey, + $source, + $featureEnabled, + $userId, + $userAttributes ); - $this->sendImpressionEvent($config, $experimentKey, $variation->getKey(), $featureFlagKey, $experimentKey, $decision->getSource(), $featureEnabled, $userId, $attributes); - } else { - $this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); + $decisionEventDispatched = true; } } - $attributes = is_null($attributes) ? [] : $attributes; + // Generate all variables map if decide options doesn't include excludeVariables + if (!in_array(OptimizelyDecideOption::EXCLUDE_VARIABLES, $decideOptions)) { + foreach ($featureFlag->getVariables() as $variable) { + $allVariables[$variable->getKey()] = $this->getFeatureVariableValueFromVariation( + $flagKey, + $variable->getKey(), + null, + $featureEnabled, + $variation, + $userId + ); + } + } + + // send notification $this->notificationCenter->sendNotifications( NotificationType::DECISION, array( - DecisionNotificationTypes::FEATURE, + DecisionNotificationTypes::FLAG, $userId, - $attributes, + $userAttributes, (object) array( - 'featureKey'=>$featureFlagKey, + 'flagKey'=> $flagKey, 'featureEnabled'=> $featureEnabled, - 'source'=> $decision->getSource(), - 'sourceInfo'=> isset($sourceInfo) ? $sourceInfo : (object) array() + 'variables' => $allVariables, + 'variation' => $variationKey, + 'ruleKey' => $ruleKey, + 'reasons' => $decideReasons, + 'decisionEventDispatched' => $decisionEventDispatched ) ) ); + + $shouldIncludeReasons = in_array(OptimizelyDecideOption::INCLUDE_REASONS, $decideOptions); + + return new OptimizelyDecision( + $variationKey, + $featureEnabled, + $allVariables, + $ruleKey, + $flagKey, + $userContext, + $shouldIncludeReasons ? $decideReasons:[] + ); } /** @@ -400,7 +442,7 @@ public function activate($experimentKey, $userId, $attributes = null) return null; } - $this->sendImpressionEvent($config, $experimentKey, $variationKey, '', $experimentKey, FeatureDecision::DECITION_SOURCE_EXPERIMENT, true, $userId, $attributes); + $this->sendImpressionEvent($config, $experimentKey, $variationKey, '', $experimentKey, FeatureDecision::DECISION_SOURCE_EXPERIMENT, true, $userId, $attributes); return $variationKey; } diff --git a/src/Optimizely/OptimizelyUserContext.php b/src/Optimizely/OptimizelyUserContext.php index 8df393b7..d4a7d733 100644 --- a/src/Optimizely/OptimizelyUserContext.php +++ b/src/Optimizely/OptimizelyUserContext.php @@ -17,7 +17,7 @@ namespace Optimizely; -class OptimizelyUserContext +class OptimizelyUserContext implements \JsonSerializable { private $optimizelyClient; private $userId; @@ -38,12 +38,12 @@ public function setAttribute($key, $value) public function decide($key, array $options = []) { - return $optimizelyClient->decide($this, $key, $options) + return $this->optimizelyClient->decide($this, $key, $options); } public function decideForKeys(array $keys, array $options = []) { - return $optimizelyClient->decideForKeys($this, $keys, $options); + return $this->optimizelyClient->decideForKeys($this, $keys, $options); } public function decideAll(array $options = []) @@ -70,4 +70,12 @@ public function getOptimizely() { return $this->optimizelyClient; } + + public function jsonSerialize() + { + return [ + 'userId' => $this->userId, + 'attributes' => $this->attributes + ]; + } } diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 540c7838..08968872 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -26,6 +26,7 @@ use GuzzleHttp\Psr7\Response; use Monolog\Logger; use Optimizely\Config\DatafileProjectConfig; +use Optimizely\Decide\OptimizelyDecision; use Optimizely\DecisionService\DecisionService; use Optimizely\DecisionService\FeatureDecision; use Optimizely\Entity\FeatureVariable; @@ -375,6 +376,102 @@ public function testCreateUserContextInvalidAttributes() $this->assertNull($optimizely->createUserContext('test_user', [5,6,7])); } + public function testDecide() + { + $optimizely = new Optimizely($this->datafile); + $userContext = $optimizely->createUserContext('test_user', ['device_type' => 'iPhone']); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + $this->assertEquals($expectedOptimizelyDecision->getVariationKey(), $optimizelyDecision->getVariationKey()); + $this->assertEquals($expectedOptimizelyDecision->getEnabled(), $optimizelyDecision->getEnabled()); + $this->assertEquals($expectedOptimizelyDecision->getVariables(), $optimizelyDecision->getVariables()); + $this->assertEquals($expectedOptimizelyDecision->getRuleKey(), $optimizelyDecision->getRuleKey()); + $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); + $this->assertEquals($expectedOptimizelyDecision->getUserContext(), $optimizelyDecision->getUserContext()); + $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); + } + public function testActivateInvalidOptimizelyObject() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) From 141192d1ecba9c35ead19204a2fc1e16db096277 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 24 Nov 2020 18:37:09 +0500 Subject: [PATCH 07/21] WIP --- src/Optimizely/Optimizely.php | 41 ++++++++++++++++++++++++ src/Optimizely/OptimizelyUserContext.php | 3 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 96c76b1a..20ca5754 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -409,6 +409,47 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp ); } + public function decideAll(OptimizelyUserContext $userContext, array $decideOptions = []) + { + // check if SDK is ready + $config = $this->getConfig(); + if ($config === null) { + $this->_logger->log(Logger::ERROR, sprintf(Errors::INVALID_DATAFILE, __FUNCTION__)); + return []; + } + + // get all feature keys + $keys = []; + $featureFlags = $config->getFeatureFlags(); + foreach ($featureFlags as $feature) { + $keys [] = $feature->getKey(); + } + + return $this->decideForKeys($userContext, $keys, $decideOptions); + } + + public function decideForKeys(OptimizelyUserContext $userContext, array $keys, array $decideOptions = []) + { + // check if SDK is ready + $config = $this->getConfig(); + if ($config === null) { + $this->_logger->log(Logger::ERROR, sprintf(Errors::INVALID_DATAFILE, __FUNCTION__)); + return []; + } + + $enabledFlagsOnly = in_array(OptimizelyDecideOption::ENABLED_FLAGS_ONLY, $decideOptions); + $decisions = []; + + foreach ($keys as $key) { + $decision = $this->decide($userContext, $key, $decideOptions); + if (!$enabledFlagsOnly || $decision->getEnabled() === true) { + $decisions [$key] = $decision; + } + } + + return $decisions; + } + /** * Buckets visitor and sends impression event to Optimizely. * diff --git a/src/Optimizely/OptimizelyUserContext.php b/src/Optimizely/OptimizelyUserContext.php index d4a7d733..48952d76 100644 --- a/src/Optimizely/OptimizelyUserContext.php +++ b/src/Optimizely/OptimizelyUserContext.php @@ -48,11 +48,12 @@ public function decideForKeys(array $keys, array $options = []) public function decideAll(array $options = []) { - return $this->optimizelyClient->decideAll($this, $keys); + return $this->optimizelyClient->decideAll($this, $options); } public function trackEvent($eventKey, array $eventTags = []) { + $eventTags = $eventTags ?: null; return $this->optimizelyClient->track($eventKey, $this->userId, $this->attributes, $eventTags); } From bc47a636ebcf5286d17281dba9662547692e8c44 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 24 Nov 2020 18:51:00 +0500 Subject: [PATCH 08/21] WIP --- src/Optimizely/DecisionService/DecisionService.php | 6 ++---- src/Optimizely/DecisionService/FeatureDecision.php | 2 +- src/Optimizely/Enums/DecisionNotificationTypes.php | 2 +- src/Optimizely/Optimizely.php | 2 +- tests/OptimizelyTest.php | 2 -- tests/OptimizelyUserContextTests.php | 3 --- 6 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 5fa4425b..803c898b 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -177,7 +177,7 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ $decideReasons[] = $message; } else { if (!in_array(OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE, $decideOptions)) { - $this->saveVariation($experiment, $variation, $userProfile, $decideReasons); + $this->saveVariation($experiment, $variation, $userProfile); } $message = sprintf( 'User "%s" is in variation %s of experiment %s.', @@ -599,7 +599,7 @@ private function getStoredVariation(ProjectConfigInterface $projectConfig, Exper * @param $variation Variation Variation the user is bucketed into. * @param $userProfile UserProfile User profile object to which we are persisting the variation assignment. */ - private function saveVariation(Experiment $experiment, Variation $variation, UserProfile $userProfile, &$decideReasons = []) + private function saveVariation(Experiment $experiment, Variation $variation, UserProfile $userProfile) { if (is_null($this->_userProfileService)) { return; @@ -627,7 +627,6 @@ private function saveVariation(Experiment $experiment, Variation $variation, Use ); $this->_logger->log(Logger::INFO, $message); - $decideReasons[] = $message; } catch (Exception $e) { $message = sprintf( 'Failed to save variation "%s" of experiment "%s" for user "%s".', @@ -637,7 +636,6 @@ private function saveVariation(Experiment $experiment, Variation $variation, Use ); $this->_logger->log(Logger::WARNING, $message); - $decideReasons[] = $message; } } } diff --git a/src/Optimizely/DecisionService/FeatureDecision.php b/src/Optimizely/DecisionService/FeatureDecision.php index d100c585..85554a4c 100644 --- a/src/Optimizely/DecisionService/FeatureDecision.php +++ b/src/Optimizely/DecisionService/FeatureDecision.php @@ -1,6 +1,6 @@ validateInputs( diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 08968872..78a07bea 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -16,8 +16,6 @@ */ namespace Optimizely\Tests; -// require(dirname(__FILE__).'/TestData.php'); - use Exception; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; diff --git a/tests/OptimizelyUserContextTests.php b/tests/OptimizelyUserContextTests.php index 95213583..103756c1 100644 --- a/tests/OptimizelyUserContextTests.php +++ b/tests/OptimizelyUserContextTests.php @@ -18,7 +18,6 @@ require(dirname(__FILE__).'/TestData.php'); - use Exception; use TypeError; @@ -29,8 +28,6 @@ class OptimizelyUserContextTest extends \PHPUnit_Framework_TestCase { - const OUTPUT_STREAM = 'output'; - private $datafile; private $loggerMock; private $optimizelyObject; From 4218774dfb012f8567dd22ad16107ff549cbcdf1 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 24 Nov 2020 19:10:10 +0500 Subject: [PATCH 09/21] add new unit tests --- tests/OptimizelyTest.php | 20 +++++++----- tests/OptimizelyUserContextTests.php | 46 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 78a07bea..1d2e8c59 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -118,6 +118,17 @@ public function setUp() $this->staticConfigManager = new StaticProjectConfigManager($this->datafile, true, $this->loggerMock, new NoOpErrorHandler); } + public function compareOptimizelyDecisions(OptimizelyDecision $expectedOptimizelyDecision, OptimizelyDecision $optimizelyDecision) + { + $this->assertEquals($expectedOptimizelyDecision->getVariationKey(), $optimizelyDecision->getVariationKey()); + $this->assertEquals($expectedOptimizelyDecision->getEnabled(), $optimizelyDecision->getEnabled()); + $this->assertEquals($expectedOptimizelyDecision->getVariables(), $optimizelyDecision->getVariables()); + $this->assertEquals($expectedOptimizelyDecision->getRuleKey(), $optimizelyDecision->getRuleKey()); + $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); + $this->assertEquals($expectedOptimizelyDecision->getUserContext(), $optimizelyDecision->getUserContext()); + $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); + } + public function testIsValidForInvalidOptimizelyObject() { $optlyObject = new Optimizely('Random datafile'); @@ -461,13 +472,8 @@ public function testDecide() $userContext, [] ); - $this->assertEquals($expectedOptimizelyDecision->getVariationKey(), $optimizelyDecision->getVariationKey()); - $this->assertEquals($expectedOptimizelyDecision->getEnabled(), $optimizelyDecision->getEnabled()); - $this->assertEquals($expectedOptimizelyDecision->getVariables(), $optimizelyDecision->getVariables()); - $this->assertEquals($expectedOptimizelyDecision->getRuleKey(), $optimizelyDecision->getRuleKey()); - $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); - $this->assertEquals($expectedOptimizelyDecision->getUserContext(), $optimizelyDecision->getUserContext()); - $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } public function testActivateInvalidOptimizelyObject() diff --git a/tests/OptimizelyUserContextTests.php b/tests/OptimizelyUserContextTests.php index 103756c1..2801fb89 100644 --- a/tests/OptimizelyUserContextTests.php +++ b/tests/OptimizelyUserContextTests.php @@ -96,4 +96,50 @@ public function testSetAttributeOverridesValueOfExistingKey() $optUserContext->setAttribute('browser', 'firefox'); $this->assertEquals(["browser" => "firefox"], $optUserContext->getAttributes()); } + + public function testDecideCallsAndReturnsOptimizelyDecideAPI() + { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('decide')) + ->getMock(); + + + $optUserContext = new OptimizelyUserContext($optimizelyMock, $userId, $attributes); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('decide') + ->with( + $optUserContext, + 'test_feature', + ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY'] + ) + ->will($this->returnValue('Mocked return value')); + + $this->assertEquals( + 'Mocked return value', + $optUserContext->decide('test_feature', ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY']) + ); + } + + public function testDecideAllCallsAndReturnsOptimizelyDecideAPI() + { + } + + public function testDecideForKeysCallsAndReturnsOptimizelyDecideAPI() + { + } + + public function testTrackEventCallsAndReturnsOptimizelyDecideAPI() + { + } + + public function testJsonSerialize() + { + } } From 8cc49df3204626e4d3f9b87c1a9c7595c3c7a1a6 Mon Sep 17 00:00:00 2001 From: ozayr-zaviar Date: Wed, 2 Dec 2020 22:21:39 +0500 Subject: [PATCH 10/21] testcases for decide api --- tests/OptimizelyTest.php | 1316 ++++++++++++++++++++++++++ tests/OptimizelyUserContextTests.php | 61 +- 2 files changed, 1376 insertions(+), 1 deletion(-) diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 1d2e8c59..882aeb8c 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -462,7 +462,292 @@ public function testDecide() $optimizelyMock->notificationCenter = $this->notificationCenterMock; $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testDecideWhenSdkNotReady() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array('invalid', null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + //Verify that sendNotifications is called with expected params + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + + $this->assertEquals($optimizelyDecision->getReasons(), ['Optimizely SDK not configured properly yet.']); + } + + public function testDecideWhenFlagKeyIsNotValid() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + //Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->never()) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->never()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 123); + $this->assertEquals($optimizelyDecision->getFlagKey(), 123); + $this->assertEquals($optimizelyDecision->getReasons(), ['No flag was found for key "123".']); + } + + public function testDecideWhenFlagKeyIsNotAvailable() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + //Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->never()) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->never()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'not_found_key'); + $this->assertEquals($optimizelyDecision->getFlagKey(), 'not_found_key'); + $this->assertEquals($optimizelyDecision->getReasons(), ['No flag was found for key "not_found_key".']); + } + + public function testwhenUserIsBucketedIntoFeatureExperiment() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); $expectedOptimizelyDecision = new OptimizelyDecision( 'control', true, @@ -476,6 +761,1037 @@ public function testDecide() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } + public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonTrue() + { + $datafileWithSendFlagDecisionsTrue = json_decode($this->datafile); + $datafileWithSendFlagDecisionsTrue->sendFlagDecisions = true; + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsTrue), null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->anything(), + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_ROLLOUT, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonFalse() + { + $datafileWithSendFlagDecisionsFalse = json_decode($this->datafile); + $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = false; + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsFalse), null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => false + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent') + ->with( + $this->anything(), + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_ROLLOUT, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testwhenDecisionServiceReturnNullAndSendFlagDecisonFalse() + { + $datafileWithSendFlagDecisionsFalse = json_decode($this->datafile); + $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = false; + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsFalse), null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $expected_decision = new FeatureDecision( + null, + null, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> false, + 'variables'=> ["double_variable" => 14.99], + 'variation' => null, + 'ruleKey' => null, + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->anything(), + null, + null, + 'double_single_variable_feature', + null, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + false, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + null, + false, + ['double_variable' => 14.99], + null, + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testwhenDecisionServiceReturnNullAndSendFlagDecisonTrue() + { + $datafileWithSendFlagDecisionsFalse = json_decode($this->datafile); + $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = true; + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsFalse), null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->anything(), + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testSendEventWhenOptionIsNotSet() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent', 'dispatchEvent')) + ->getMock(); + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + //Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + } + + public function testNotSendEventWhenOptionIsSet() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent', 'dispatchEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => false + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['DISABLE_DECISION_EVENT']); + } + + public function testExcludeVariableIfOptionIsSet() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent', 'dispatchEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> [], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + [], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['EXCLUDE_VARIABLES']); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision);$this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testIncludeVariableIfOptionIsNotSet() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testAllDecideOptionsToInternalMethods() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'featureEnabled'=> true, + 'variables'=> [], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => false + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + [], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY', 'IGNORE_USER_PROFILE_SERVICE', 'INCLUDE_REASONS', 'EXCLUDE_VARIABLES']); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testGetAllDecisions() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(9)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->notificationCenterMock->expects($this->exactly(9)) + ->method('sendNotifications'); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(9)) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decideAll($userContext); + $expectedOptimizelyDecision1 = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $expectedOptimizelyDecision2 = new OptimizelyDecision( + 'control', + true, + [], + 'test_experiment_double_feature', + 'boolean_feature', + $userContext, + [] + ); + + $this->assertEquals(count($optimizelyDecision), 9); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision1, $optimizelyDecision['double_single_variable_feature']); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecision['boolean_feature']); + } + + public function testGetEnabledDecisionOnly() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(9)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->notificationCenterMock->expects($this->exactly(9)) + ->method('sendNotifications'); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(9)) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decideAll($userContext, ['ENABLED_FLAGS_ONLY']); + + $optimizelyDecision = array_filter($optimizelyDecision, function ($decision){ return $decision->getEnabled(); }); + + $this->assertEquals(count($optimizelyDecision), 9); + + } + + public function testDecideForKeys() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(2)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->notificationCenterMock->expects($this->exactly(2)) + ->method('sendNotifications'); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(2)) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature']); + + $expectedOptimizelyDecision1 = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $expectedOptimizelyDecision2 = new OptimizelyDecision( + 'control', + true, + [], + 'test_experiment_double_feature', + 'boolean_feature', + $userContext, + [] + ); + + $this->assertEquals(count($optimizelyDecision), 2); + $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature']); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision1, $optimizelyDecision['double_single_variable_feature']); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecision['boolean_feature']); + + $optimizelyDecision = array_filter($optimizelyDecision, function ($decision){ return $decision->getEnabled(); }); + } + + public function testDecideForKeysForEnabledFlagsOnly() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(2)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->notificationCenterMock->expects($this->exactly(2)) + ->method('sendNotifications'); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(2)) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature'] ,['ENABLED_FLAGS_ONLY']); + + $expectedOptimizelyDecision1 = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $expectedOptimizelyDecision2 = new OptimizelyDecision( + 'control', + true, + [], + 'test_experiment_double_feature', + 'boolean_feature', + $userContext, + [] + ); + + $optimizelyDecision = array_filter($optimizelyDecision, function ($decision){ return $decision->getEnabled(); }); + + $this->assertEquals(count($optimizelyDecision), 2); + } + public function testActivateInvalidOptimizelyObject() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) diff --git a/tests/OptimizelyUserContextTests.php b/tests/OptimizelyUserContextTests.php index 2801fb89..97fcb4f4 100644 --- a/tests/OptimizelyUserContextTests.php +++ b/tests/OptimizelyUserContextTests.php @@ -129,10 +129,61 @@ public function testDecideCallsAndReturnsOptimizelyDecideAPI() public function testDecideAllCallsAndReturnsOptimizelyDecideAPI() { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('decideAll')) + ->getMock(); + + + $optUserContext = new OptimizelyUserContext($optimizelyMock, $userId, $attributes); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('decideAll') + ->with( + $optUserContext, + ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY'] + ) + ->will($this->returnValue('Mocked return value')); + + $this->assertEquals( + 'Mocked return value', + $optUserContext->decideAll(['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY']) + ); } public function testDecideForKeysCallsAndReturnsOptimizelyDecideAPI() { + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('decideForKeys')) + ->getMock(); + + + $optUserContext = new OptimizelyUserContext($optimizelyMock, $userId, $attributes); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('decideForKeys') + ->with( + $optUserContext, + ['test_feature', 'test_experiment'], + ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY'] + ) + ->will($this->returnValue('Mocked return value')); + + $this->assertEquals( + 'Mocked return value', + $optUserContext->decideForKeys(['test_feature', 'test_experiment'], ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY']) + ); } public function testTrackEventCallsAndReturnsOptimizelyDecideAPI() @@ -141,5 +192,13 @@ public function testTrackEventCallsAndReturnsOptimizelyDecideAPI() public function testJsonSerialize() { - } + $userId = 'test_user'; + $attributes = [ "browser" => "chrome"]; + $optUserContext = new OptimizelyUserContext($this->optimizelyObject, $userId, $attributes); + + $this->assertEquals([ + 'userId' => $this->userId, + 'attributes' => $this->attributes + ], $optUserContext->jsonSerialize()); + } } From 22e42de8cb06312b9f16de8655bb1946addd53f3 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 8 Dec 2020 21:35:22 +0500 Subject: [PATCH 11/21] unit tests WIP --- tests/OptimizelyTest.php | 760 ++++++++------------------- tests/OptimizelyUserContextTests.php | 59 ++- tests/TestData.php | 4 +- 3 files changed, 272 insertions(+), 551 deletions(-) diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 882aeb8c..c0e972e2 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -16,6 +16,8 @@ */ namespace Optimizely\Tests; +// require(dirname(__FILE__).'/TestData.php'); + use Exception; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; @@ -444,20 +446,20 @@ public function testDecide() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); $optimizelyMock->notificationCenter = $this->notificationCenterMock; @@ -476,205 +478,80 @@ public function testDecide() } public function testDecideWhenSdkNotReady() - { + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array('invalid', null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) ->getMock(); - - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - //assert that sendImpressionEvent is called with expected params + //assert that sendImpressionEvent is not called $optimizelyMock->expects($this->never()) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - //Verify that sendNotifications is called with expected params + ->method('sendImpressionEvent'); - $optimizelyMock->notificationCenter = $this->notificationCenterMock; + $expectedOptimizelyDecision = new OptimizelyDecision( + null, + null, + null, + null, + 'double_single_variable_feature', + $userContext, + [] + ); $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); $this->assertEquals($optimizelyDecision->getReasons(), ['Optimizely SDK not configured properly yet.']); } public function testDecideWhenFlagKeyIsNotValid() - { + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) ->getMock(); - - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - - //Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); - - $decisionServiceMock->expects($this->never()) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); - - //Verify that sendNotifications is called with expected params - $arrayParam = array( - DecisionNotificationTypes::FLAG, - 'test_user', - ['device_type' => 'iPhone'], - (object) array( - 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, - 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', - 'ruleKey' => 'test_experiment_double_feature', - 'reasons' => [], - 'decisionEventDispatched' => true - ) - ); + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); $this->notificationCenterMock->expects($this->never()) - ->method('sendNotifications') - ->with( - NotificationType::DECISION, - $arrayParam - ); + ->method('sendNotifications'); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->never()) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; $optimizelyDecision = $optimizelyMock->decide($userContext, 123); - $this->assertEquals($optimizelyDecision->getFlagKey(), 123); $this->assertEquals($optimizelyDecision->getReasons(), ['No flag was found for key "123".']); } public function testDecideWhenFlagKeyIsNotAvailable() - { + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) ->getMock(); - - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - - //Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); - - $decisionServiceMock->expects($this->never()) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); - - //Verify that sendNotifications is called with expected params - $arrayParam = array( - DecisionNotificationTypes::FLAG, - 'test_user', - ['device_type' => 'iPhone'], - (object) array( - 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, - 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', - 'ruleKey' => 'test_experiment_double_feature', - 'reasons' => [], - 'decisionEventDispatched' => true - ) - ); + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); $this->notificationCenterMock->expects($this->never()) - ->method('sendNotifications') - ->with( - NotificationType::DECISION, - $arrayParam - ); + ->method('sendNotifications'); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->never()) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is not called. + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; - $optimizelyDecision = $optimizelyMock->decide($userContext, 'not_found_key'); - $this->assertEquals($optimizelyDecision->getFlagKey(), 'not_found_key'); - $this->assertEquals($optimizelyDecision->getReasons(), ['No flag was found for key "not_found_key".']); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'unknown_key'); + $this->assertEquals($optimizelyDecision->getReasons(), ['No flag was found for key "unknown_key".']); } - public function testwhenUserIsBucketedIntoFeatureExperiment() - { + public function testDecidewhenUserIsBucketedIntoFeatureExperiment() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) @@ -729,20 +606,20 @@ public function testwhenUserIsBucketedIntoFeatureExperiment() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); $optimizelyMock->notificationCenter = $this->notificationCenterMock; @@ -761,15 +638,15 @@ public function testwhenUserIsBucketedIntoFeatureExperiment() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonTrue() - { + public function testDecidewhenUserIsBucketedIntoRolloutAndSendFlagDecisionIsTrue() + { $datafileWithSendFlagDecisionsTrue = json_decode($this->datafile); $datafileWithSendFlagDecisionsTrue->sendFlagDecisions = true; $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsTrue), null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) - ->getMock(); + ->getMock(); $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock)) @@ -820,20 +697,20 @@ public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonTrue() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->anything(), - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_ROLLOUT, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->anything(), + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_ROLLOUT, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); $optimizelyMock->notificationCenter = $this->notificationCenterMock; @@ -852,15 +729,15 @@ public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonTrue() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonFalse() - { + public function testDecidewhenUserIsBucketedIntoRolloutAndSendFlagDecisionIsFalse() + { $datafileWithSendFlagDecisionsFalse = json_decode($this->datafile); $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = false; $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsFalse), null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) - ->getMock(); + ->getMock(); $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock)) @@ -911,20 +788,9 @@ public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonFalse() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->never()) - ->method('sendImpressionEvent') - ->with( - $this->anything(), - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_ROLLOUT, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is not called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; @@ -943,15 +809,15 @@ public function testwhenUserIsBucketedIntoRolloutAndSendFlagDecisonFalse() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testwhenDecisionServiceReturnNullAndSendFlagDecisonFalse() - { + public function testDecidewhenDecisionServiceReturnsNullAndSendFlagDecisionIsTrue() + { $datafileWithSendFlagDecisionsFalse = json_decode($this->datafile); - $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = false; + $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = true; $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsFalse), null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) - ->getMock(); + ->getMock(); $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock)) @@ -962,10 +828,17 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisonFalse() $decisionService->setAccessible(true); $decisionService->setValue($optimizelyMock, $decisionServiceMock); + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + // assert that featureEnabled for $variation is true + $this->assertTrue($variation->getFeatureEnabled()); + $expected_decision = new FeatureDecision( null, null, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST + FeatureDecision::DECISION_SOURCE_ROLLOUT ); $decisionServiceMock->expects($this->exactly(1)) @@ -995,24 +868,24 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisonFalse() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->anything(), - null, - null, - 'double_single_variable_feature', - null, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - false, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->anything(), + null, + null, + 'double_single_variable_feature', + null, + FeatureDecision::DECISION_SOURCE_ROLLOUT, + false, + 'test_user', + ['device_type' => 'iPhone'] + ); $optimizelyMock->notificationCenter = $this->notificationCenterMock; - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); $expectedOptimizelyDecision = new OptimizelyDecision( null, @@ -1027,10 +900,10 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisonFalse() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testwhenDecisionServiceReturnNullAndSendFlagDecisonTrue() - { + public function testwhenDecisionServiceReturnNullAndSendFlagDecisionIsFalse() + { $datafileWithSendFlagDecisionsFalse = json_decode($this->datafile); - $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = true; + $datafileWithSendFlagDecisionsFalse->sendFlagDecisions = false; $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array(json_encode($datafileWithSendFlagDecisionsFalse), null, $this->loggerMock)) @@ -1046,17 +919,10 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisonTrue() $decisionService->setAccessible(true); $decisionService->setValue($optimizelyMock, $decisionServiceMock); - // Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - // assert that featureEnabled for $variation is true - $this->assertTrue($variation->getFeatureEnabled()); - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST + null, + null, + FeatureDecision::DECISION_SOURCE_ROLLOUT ); $decisionServiceMock->expects($this->exactly(1)) @@ -1070,12 +936,12 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisonTrue() ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, - 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', - 'ruleKey' => 'test_experiment_double_feature', + 'featureEnabled'=> false, + 'variables'=> ["double_variable" => 14.99], + 'variation' => null, + 'ruleKey' => null, 'reasons' => [], - 'decisionEventDispatched' => true + 'decisionEventDispatched' => false ) ); @@ -1086,30 +952,19 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisonTrue() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->anything(), - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); $expectedOptimizelyDecision = new OptimizelyDecision( - 'control', - true, - ['double_variable' => 42.42], - 'test_experiment_double_feature', + null, + false, + ['double_variable' => 14.99], + null, 'double_single_variable_feature', $userContext, [] @@ -1118,90 +973,15 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisonTrue() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testSendEventWhenOptionIsNotSet() - { - $optimizelyMock = $this->getMockBuilder(Optimizely::class) - ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('sendImpressionEvent', 'dispatchEvent')) - ->getMock(); - - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - - //Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); - - $decisionServiceMock->expects($this->exactly(1)) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); - - //Verify that sendNotifications is called with expected params - $arrayParam = array( - DecisionNotificationTypes::FLAG, - 'test_user', - ['device_type' => 'iPhone'], - (object) array( - 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, - 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', - 'ruleKey' => 'test_experiment_double_feature', - 'reasons' => [], - 'decisionEventDispatched' => true - ) - ); - - $this->notificationCenterMock->expects($this->exactly(1)) - ->method('sendNotifications') - ->with( - NotificationType::DECISION, - $arrayParam - ); - - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); - - $optimizelyMock->notificationCenter = $this->notificationCenterMock; - - $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); - } - - public function testNotSendEventWhenOptionIsSet() - { + public function testDecideOptionswithDISABLE_DECISION_EVENT() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('sendImpressionEvent', 'dispatchEvent')) + ->setMethods(array('sendImpressionEvent')) ->getMock(); - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock)) @@ -1248,35 +1028,24 @@ public function testNotSendEventWhenOptionIsSet() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->never()) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is not called. + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['DISABLE_DECISION_EVENT']); } - public function testExcludeVariableIfOptionIsSet() - { + public function testDecideOptionswithEXCLUDE_VARIABLES() + { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent', 'dispatchEvent')) ->getMock(); - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock)) @@ -1323,20 +1092,21 @@ public function testExcludeVariableIfOptionIsSet() $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + $expectedOptimizelyDecision = new OptimizelyDecision( 'control', true, @@ -1345,22 +1115,23 @@ public function testExcludeVariableIfOptionIsSet() 'double_single_variable_feature', $userContext, [] - ); + ); $optimizelyMock->notificationCenter = $this->notificationCenterMock; $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['EXCLUDE_VARIABLES']); - $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision);$this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testIncludeVariableIfOptionIsNotSet() + public function testDecidewithAllDecideOptionsSet() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) ->getMock(); - + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); $decisionServiceMock = $this->getMockBuilder(DecisionService::class) @@ -1372,13 +1143,9 @@ public function testIncludeVariableIfOptionIsNotSet() $decisionService->setAccessible(true); $decisionService->setValue($optimizelyMock, $decisionServiceMock); - // Mock getVariationForFeature to return a valid decision with experiment and variation keys $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - // assert that featureEnabled for $variation is true - $this->assertTrue($variation->getFeatureEnabled()); - $expected_decision = new FeatureDecision( $experiment, $variation, @@ -1389,7 +1156,7 @@ public function testIncludeVariableIfOptionIsNotSet() ->method('getVariationForFeature') ->will($this->returnValue($expected_decision)); - // Verify that sendNotifications is called with expected params + //Verify that sendNotifications is called with expected params $arrayParam = array( DecisionNotificationTypes::FLAG, 'test_user', @@ -1397,145 +1164,58 @@ public function testIncludeVariableIfOptionIsNotSet() (object) array( 'flagKey'=>'double_single_variable_feature', 'featureEnabled'=> true, - 'variables'=> ["double_variable" => 42.42], + 'variables'=> [], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], - 'decisionEventDispatched' => true + 'decisionEventDispatched' => false ) ); - $this->notificationCenterMock->expects($this->once()) + $this->notificationCenterMock->expects($this->exactly(1)) ->method('sendNotifications') ->with( NotificationType::DECISION, $arrayParam ); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(1)) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); - - $optimizelyMock->notificationCenter = $this->notificationCenterMock; - - $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); $expectedOptimizelyDecision = new OptimizelyDecision( 'control', true, - ['double_variable' => 42.42], + [], 'test_experiment_double_feature', 'double_single_variable_feature', $userContext, [] ); - $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); - } - - public function testAllDecideOptionsToInternalMethods() - { - $optimizelyMock = $this->getMockBuilder(Optimizely::class) - ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('sendImpressionEvent')) - ->getMock(); - - - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); - - $decisionServiceMock->expects($this->exactly(1)) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); - - //Verify that sendNotifications is called with expected params - $arrayParam = array( - DecisionNotificationTypes::FLAG, - 'test_user', - ['device_type' => 'iPhone'], - (object) array( - 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, - 'variables'=> [], - 'variation' => 'control', - 'ruleKey' => 'test_experiment_double_feature', - 'reasons' => [], - 'decisionEventDispatched' => false - ) - ); - - $this->notificationCenterMock->expects($this->exactly(1)) - ->method('sendNotifications') - ->with( - NotificationType::DECISION, - $arrayParam - ); - - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->never()) - ->method('sendImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment_double_feature', - 'control', - 'double_single_variable_feature', - 'test_experiment_double_feature', - FeatureDecision::DECISION_SOURCE_FEATURE_TEST, - true, - 'test_user', - ['device_type' => 'iPhone'] - ); - $expectedOptimizelyDecision = new OptimizelyDecision( - 'control', - true, - [], - 'test_experiment_double_feature', - 'double_single_variable_feature', - $userContext, - [] - ); - $optimizelyMock->notificationCenter = $this->notificationCenterMock; $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY', 'IGNORE_USER_PROFILE_SERVICE', 'INCLUDE_REASONS', 'EXCLUDE_VARIABLES']); $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testGetAllDecisions() + public function testDecideAll() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) ->getMock(); - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); $decisionServiceMock = $this->getMockBuilder(DecisionService::class) @@ -1600,54 +1280,66 @@ public function testGetAllDecisions() $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecision['boolean_feature']); } - public function testGetEnabledDecisionOnly() + public function testDecideOptionsWithENABLED_FLAGS_ONLY() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('sendImpressionEvent')) + ->setMethods(array('decide', 'sendImpressionEvent')) ->getMock(); - + $decideOptions = ['ENABLED_FLAGS_ONLY']; $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); + // $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + // ->setConstructorArgs(array($this->loggerMock)) + // ->setMethods(array('getVariationForFeature')) + // ->getMock(); - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); + // $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + // $decisionService->setAccessible(true); + // $decisionService->setValue($optimizelyMock, $decisionServiceMock); - // Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + $response1 = $this->optimizelyObject->decide($userContext, 'boolean_feature', $decideOptions); + $response2 = $this->optimizelyObject->decide($userContext, 'double_single_variable_feature', $decideOptions); + $response3 = $this->optimizelyObject->decide($userContext, 'integer_single_variable_feature', $decideOptions); + $response4 = $this->optimizelyObject->decide($userContext, 'boolean_single_variable_feature', $decideOptions); + $response5 = $this->optimizelyObject->decide($userContext, 'string_single_variable_feature', $decideOptions); + $response6 = $this->optimizelyObject->decide($userContext, 'multi_variate_feature', $decideOptions); + $response7 = $this->optimizelyObject->decide($userContext, 'mutex_group_feature', $decideOptions); + $response8 = $this->optimizelyObject->decide($userContext, 'empty_feature', $decideOptions); - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); + $map = [ + [$userContext, 'boolean_feature', $decideOptions, $response1], + [$userContext, 'double_single_variable_feature', $decideOptions, $response2], + [$userContext, 'integer_single_variable_feature', $decideOptions, $response3], + [$userContext, 'boolean_single_variable_feature', $decideOptions, $response4], + [$userContext, 'string_single_variable_feature', $decideOptions, $response5], + [$userContext, 'multi_variate_feature', $decideOptions, $response6], + [$userContext, 'mutex_group_feature', $decideOptions, $response7], + [$userContext, 'empty_feature', $decideOptions, $response8], + ]; - $decisionServiceMock->expects($this->exactly(9)) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); + // Mock isFeatureEnabled to return specific values + $optimizelyMock->expects($this->exactly(9)) + ->method('decide') + ->will($this->returnValueMap($map)); - $this->notificationCenterMock->expects($this->exactly(9)) - ->method('sendNotifications'); + // $this->notificationCenterMock->expects($this->exactly(9)) + // ->method('sendNotifications'); - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(9)) - ->method('sendImpressionEvent'); + // //assert that sendImpressionEvent is called with expected params + // $optimizelyMock->expects($this->exactly(9)) + // ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; - $optimizelyDecision = $optimizelyMock->decideAll($userContext, ['ENABLED_FLAGS_ONLY']); + $optimizelyDecision = $optimizelyMock->decideAll($userContext, $decideOptions); - $optimizelyDecision = array_filter($optimizelyDecision, function ($decision){ return $decision->getEnabled(); }); + $optimizelyDecision = array_filter($optimizelyDecision, function ($decision) { + return $decision->getEnabled(); + }); $this->assertEquals(count($optimizelyDecision), 9); - } public function testDecideForKeys() @@ -1715,12 +1407,14 @@ public function testDecideForKeys() ); $this->assertEquals(count($optimizelyDecision), 2); - $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature']); + // $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature']); $this->compareOptimizelyDecisions($expectedOptimizelyDecision1, $optimizelyDecision['double_single_variable_feature']); $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecision['boolean_feature']); - $optimizelyDecision = array_filter($optimizelyDecision, function ($decision){ return $decision->getEnabled(); }); + $optimizelyDecision = array_filter($optimizelyDecision, function ($decision) { + return $decision->getEnabled(); + }); } public function testDecideForKeysForEnabledFlagsOnly() @@ -1765,7 +1459,7 @@ public function testDecideForKeysForEnabledFlagsOnly() $optimizelyMock->notificationCenter = $this->notificationCenterMock; - $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature'] ,['ENABLED_FLAGS_ONLY']); + $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature'], ['ENABLED_FLAGS_ONLY']); $expectedOptimizelyDecision1 = new OptimizelyDecision( 'control', @@ -1787,7 +1481,9 @@ public function testDecideForKeysForEnabledFlagsOnly() [] ); - $optimizelyDecision = array_filter($optimizelyDecision, function ($decision){ return $decision->getEnabled(); }); + $optimizelyDecision = array_filter($optimizelyDecision, function ($decision) { + return $decision->getEnabled(); + }); $this->assertEquals(count($optimizelyDecision), 2); } diff --git a/tests/OptimizelyUserContextTests.php b/tests/OptimizelyUserContextTests.php index 97fcb4f4..e191cfba 100644 --- a/tests/OptimizelyUserContextTests.php +++ b/tests/OptimizelyUserContextTests.php @@ -21,7 +21,6 @@ use Exception; use TypeError; -use Optimizely\ErrorHandler\NoOpErrorHandler; use Optimizely\Logger\NoOpLogger; use Optimizely\Optimizely; use Optimizely\OptimizelyUserContext; @@ -111,7 +110,7 @@ public function testDecideCallsAndReturnsOptimizelyDecideAPI() $optUserContext = new OptimizelyUserContext($optimizelyMock, $userId, $attributes); - //assert that sendImpressionEvent is called with expected params + //assert that decide is called with expected params $optimizelyMock->expects($this->exactly(1)) ->method('decide') ->with( @@ -127,12 +126,11 @@ public function testDecideCallsAndReturnsOptimizelyDecideAPI() ); } - public function testDecideAllCallsAndReturnsOptimizelyDecideAPI() + public function testDecideAllCallsAndReturnsOptimizelyDecideAllAPI() { $userId = 'test_user'; $attributes = [ "browser" => "chrome"]; - $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('decideAll')) @@ -141,27 +139,26 @@ public function testDecideAllCallsAndReturnsOptimizelyDecideAPI() $optUserContext = new OptimizelyUserContext($optimizelyMock, $userId, $attributes); - //assert that sendImpressionEvent is called with expected params + //assert that decideAll is called with expected params $optimizelyMock->expects($this->exactly(1)) ->method('decideAll') ->with( $optUserContext, - ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY'] + ['ENABLED_FLAGS_ONLY'] ) ->will($this->returnValue('Mocked return value')); $this->assertEquals( 'Mocked return value', - $optUserContext->decideAll(['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY']) + $optUserContext->decideAll(['ENABLED_FLAGS_ONLY']) ); } - public function testDecideForKeysCallsAndReturnsOptimizelyDecideAPI() + public function testDecideForKeysCallsAndReturnsOptimizelyDecideForKeysAPI() { $userId = 'test_user'; $attributes = [ "browser" => "chrome"]; - $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('decideForKeys')) @@ -170,24 +167,52 @@ public function testDecideForKeysCallsAndReturnsOptimizelyDecideAPI() $optUserContext = new OptimizelyUserContext($optimizelyMock, $userId, $attributes); - //assert that sendImpressionEvent is called with expected params + //assert that decideForKeys is called with expected params $optimizelyMock->expects($this->exactly(1)) ->method('decideForKeys') ->with( $optUserContext, ['test_feature', 'test_experiment'], - ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY'] + ['DISABLE_DECISION_EVENT'] ) ->will($this->returnValue('Mocked return value')); $this->assertEquals( 'Mocked return value', - $optUserContext->decideForKeys(['test_feature', 'test_experiment'], ['DISABLE_DECISION_EVENT', 'ENABLED_FLAGS_ONLY']) + $optUserContext->decideForKeys(['test_feature', 'test_experiment'], ['DISABLE_DECISION_EVENT']) ); } - public function testTrackEventCallsAndReturnsOptimizelyDecideAPI() + public function testTrackEventCallsAndReturnsOptimizelyTrackAPI() { + $userId = 'test_user'; + $attributes = []; + $eventKey = "test_event"; + $eventTags = [ "revenue" => 50]; + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('track')) + ->getMock(); + + + $optUserContext = new OptimizelyUserContext($optimizelyMock, $userId, $attributes); + + //assert that track is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('track') + ->with( + $eventKey, + $userId, + $attributes, + $eventTags + ) + ->will($this->returnValue('Mocked return value')); + + $this->assertEquals( + 'Mocked return value', + $optUserContext->trackEvent($eventKey, $eventTags) + ); } public function testJsonSerialize() @@ -197,8 +222,8 @@ public function testJsonSerialize() $optUserContext = new OptimizelyUserContext($this->optimizelyObject, $userId, $attributes); $this->assertEquals([ - 'userId' => $this->userId, - 'attributes' => $this->attributes - ], $optUserContext->jsonSerialize()); - } + 'userId' => $userId, + 'attributes' => $attributes + ], json_decode(json_encode($optUserContext), true)); + } } diff --git a/tests/TestData.php b/tests/TestData.php index 1c5a78ab..51c08613 100644 --- a/tests/TestData.php +++ b/tests/TestData.php @@ -1768,9 +1768,9 @@ public function handleError(Exception $error) */ class DecisionTester extends DecisionService { - public function getBucketingId($userId, $userAttributes) + public function getBucketingId($userId, $userAttributes, &$decideReasons = []) { - return parent::getBucketingId($userId, $userAttributes); + return parent::getBucketingId($userId, $userAttributes, $decideReasons); } } From beaf8de3830ac8f3e533e5a335bafc6bc17b2ef0 Mon Sep 17 00:00:00 2001 From: ozayr-zaviar Date: Wed, 9 Dec 2020 13:44:39 +0500 Subject: [PATCH 12/21] decide doc string added --- src/Optimizely/Optimizely.php | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 0b78d59c..1e30e4e6 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -250,7 +250,17 @@ protected function sendImpressionEvent($config, $experimentKey, $variationKey, $ ); } - + /** + * Create a context of the user for which decision APIs will be called. + * + * A user context will be created successfully even when the SDK is not fully configured yet. + * + * @param $userId string The user ID to be used for bucketing. + * @param $userAttributes array A Hash representing user attribute names and values. + * + * @return OptimizelyUserContext|null An OptimizelyUserContext associated with this OptimizelyClient, + * or null If user attributes are not in valid format. + */ public function createUserContext($userId, array $userAttributes = []) { // We do not check if config is ready as UserContext can be created even when SDK is not ready. @@ -273,6 +283,17 @@ public function createUserContext($userId, array $userAttributes = []) return new OptimizelyUserContext($this, $userId, $userAttributes); } + /** + * Returns a decision result (OptimizelyDecision) for a given flag key and a user context, which contains all data required to deliver the flag. + * + * If the SDK finds an error, it'll return a `decision` with null for `variationKey`. The decision will include an error message in `reasons` + * + * @param $userContext OptimizelyUserContext context of the user for which decision will be called. + * @param $key string A flag key for which a decision will be made. + * @param $decideOptions array A list of options for decision making. + * + * @return OptimizelyDecision A decision result + */ public function decide(OptimizelyUserContext $userContext, $key, array $decideOptions = []) { $decideReasons = []; @@ -409,6 +430,14 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp ); } + /** + * Returns a hash of decision results (OptimizelyDecision) for all active flag keys. + * + * @param $userContext OptimizelyUserContext context of the user for which decision will be called. + * @param $decideOptions array A list of options for decision making. + * + * @return array Hash of decisions containing flag keys as hash keys and corresponding decisions as their values. + */ public function decideAll(OptimizelyUserContext $userContext, array $decideOptions = []) { // check if SDK is ready @@ -428,6 +457,19 @@ public function decideAll(OptimizelyUserContext $userContext, array $decideOptio return $this->decideForKeys($userContext, $keys, $decideOptions); } + /** + * Returns a hash of decision results (OptimizelyDecision) for multiple flag keys and a user context. + * + * If the SDK finds an error for a key, the response will include a decision for the key showing `reasons` for the error. + * + * The SDK will always return hash of decisions. When it can not process requests, it'll return an empty hash after logging the errors. + * + * @param $userContext OptimizelyUserContext context of the user for which decision will be called. + * @param $keys array A list of flag keys for which the decisions will be made. + * @param $decideOptions array A list of options for decision making. + * + * @return array Hash of decisions containing flag keys as hash keys and corresponding decisions as their values. + */ public function decideForKeys(OptimizelyUserContext $userContext, array $keys, array $decideOptions = []) { // check if SDK is ready From db2e453a7e34a25609b80d9e0cb10ea0e489785f Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 9 Dec 2020 19:30:57 +0500 Subject: [PATCH 13/21] revised all unit tests; --- src/Optimizely/Optimizely.php | 14 +- tests/OptimizelyTest.php | 260 ++++++---------------------------- 2 files changed, 53 insertions(+), 221 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 1e30e4e6..bcff2192 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -252,14 +252,14 @@ protected function sendImpressionEvent($config, $experimentKey, $variationKey, $ /** * Create a context of the user for which decision APIs will be called. - * + * * A user context will be created successfully even when the SDK is not fully configured yet. * * @param $userId string The user ID to be used for bucketing. * @param $userAttributes array A Hash representing user attribute names and values. * * @return OptimizelyUserContext|null An OptimizelyUserContext associated with this OptimizelyClient, - * or null If user attributes are not in valid format. + * or null If user attributes are not in valid format. */ public function createUserContext($userId, array $userAttributes = []) { @@ -285,14 +285,14 @@ public function createUserContext($userId, array $userAttributes = []) /** * Returns a decision result (OptimizelyDecision) for a given flag key and a user context, which contains all data required to deliver the flag. - * + * * If the SDK finds an error, it'll return a `decision` with null for `variationKey`. The decision will include an error message in `reasons` * * @param $userContext OptimizelyUserContext context of the user for which decision will be called. * @param $key string A flag key for which a decision will be made. * @param $decideOptions array A list of options for decision making. * - * @return OptimizelyDecision A decision result + * @return OptimizelyDecision A decision result */ public function decide(OptimizelyUserContext $userContext, $key, array $decideOptions = []) { @@ -432,7 +432,7 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp /** * Returns a hash of decision results (OptimizelyDecision) for all active flag keys. - * + * * @param $userContext OptimizelyUserContext context of the user for which decision will be called. * @param $decideOptions array A list of options for decision making. * @@ -459,9 +459,9 @@ public function decideAll(OptimizelyUserContext $userContext, array $decideOptio /** * Returns a hash of decision results (OptimizelyDecision) for multiple flag keys and a user context. - * + * * If the SDK finds an error for a key, the response will include a decision for the key showing `reasons` for the error. - * + * * The SDK will always return hash of decisions. When it can not process requests, it'll return an empty hash after logging the errors. * * @param $userContext OptimizelyUserContext context of the user for which decision will be called. diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index c0e972e2..4ebf012b 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -973,7 +973,7 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisionIsFalse() $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testDecideOptionswithDISABLE_DECISION_EVENT() + public function testDecideOptionDisableDecisionEvent() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) @@ -1037,7 +1037,7 @@ public function testDecideOptionswithDISABLE_DECISION_EVENT() $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['DISABLE_DECISION_EVENT']); } - public function testDecideOptionswithEXCLUDE_VARIABLES() + public function testDecideOptionExcludeVariable() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) @@ -1218,42 +1218,15 @@ public function testDecideAll() $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - - // Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - // assert that featureEnabled for $variation is true - $this->assertTrue($variation->getFeatureEnabled()); - - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); - - $decisionServiceMock->expects($this->exactly(9)) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); - $this->notificationCenterMock->expects($this->exactly(9)) ->method('sendNotifications'); //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(9)) + $optimizelyMock->expects($this->exactly(5)) ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; - $optimizelyDecision = $optimizelyMock->decideAll($userContext); $expectedOptimizelyDecision1 = new OptimizelyDecision( 'control', true, @@ -1265,227 +1238,86 @@ public function testDecideAll() ); $expectedOptimizelyDecision2 = new OptimizelyDecision( - 'control', + 'test_variation_2', true, [], - 'test_experiment_double_feature', + 'test_experiment_2', 'boolean_feature', $userContext, [] ); - $this->assertEquals(count($optimizelyDecision), 9); - - $this->compareOptimizelyDecisions($expectedOptimizelyDecision1, $optimizelyDecision['double_single_variable_feature']); - $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecision['boolean_feature']); - } - - public function testDecideOptionsWithENABLED_FLAGS_ONLY() - { - $optimizelyMock = $this->getMockBuilder(Optimizely::class) - ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('decide', 'sendImpressionEvent')) - ->getMock(); - - $decideOptions = ['ENABLED_FLAGS_ONLY']; - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - - // $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - // ->setConstructorArgs(array($this->loggerMock)) - // ->setMethods(array('getVariationForFeature')) - // ->getMock(); - // $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - // $decisionService->setAccessible(true); - // $decisionService->setValue($optimizelyMock, $decisionServiceMock); + $optimizelyDecisions = $optimizelyMock->decideAll($userContext); + $this->assertEquals(count($optimizelyDecisions), 9); - $response1 = $this->optimizelyObject->decide($userContext, 'boolean_feature', $decideOptions); - $response2 = $this->optimizelyObject->decide($userContext, 'double_single_variable_feature', $decideOptions); - $response3 = $this->optimizelyObject->decide($userContext, 'integer_single_variable_feature', $decideOptions); - $response4 = $this->optimizelyObject->decide($userContext, 'boolean_single_variable_feature', $decideOptions); - $response5 = $this->optimizelyObject->decide($userContext, 'string_single_variable_feature', $decideOptions); - $response6 = $this->optimizelyObject->decide($userContext, 'multi_variate_feature', $decideOptions); - $response7 = $this->optimizelyObject->decide($userContext, 'mutex_group_feature', $decideOptions); - $response8 = $this->optimizelyObject->decide($userContext, 'empty_feature', $decideOptions); - - $map = [ - [$userContext, 'boolean_feature', $decideOptions, $response1], - [$userContext, 'double_single_variable_feature', $decideOptions, $response2], - [$userContext, 'integer_single_variable_feature', $decideOptions, $response3], - [$userContext, 'boolean_single_variable_feature', $decideOptions, $response4], - [$userContext, 'string_single_variable_feature', $decideOptions, $response5], - [$userContext, 'multi_variate_feature', $decideOptions, $response6], - [$userContext, 'mutex_group_feature', $decideOptions, $response7], - [$userContext, 'empty_feature', $decideOptions, $response8], - ]; - - // Mock isFeatureEnabled to return specific values - $optimizelyMock->expects($this->exactly(9)) - ->method('decide') - ->will($this->returnValueMap($map)); - - // $this->notificationCenterMock->expects($this->exactly(9)) - // ->method('sendNotifications'); - - // //assert that sendImpressionEvent is called with expected params - // $optimizelyMock->expects($this->exactly(9)) - // ->method('sendImpressionEvent'); - - $optimizelyMock->notificationCenter = $this->notificationCenterMock; - - $optimizelyDecision = $optimizelyMock->decideAll($userContext, $decideOptions); - - $optimizelyDecision = array_filter($optimizelyDecision, function ($decision) { - return $decision->getEnabled(); - }); - - $this->assertEquals(count($optimizelyDecision), 9); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision1, $optimizelyDecisions['double_single_variable_feature']); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecisions['boolean_feature']); } - public function testDecideForKeys() + public function testDecideOptionEnabledFlagsOnly() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) ->setMethods(array('sendImpressionEvent')) ->getMock(); - + $decideOptions = ['ENABLED_FLAGS_ONLY']; $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - - // Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); - - $decisionServiceMock->expects($this->exactly(2)) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); - - $this->notificationCenterMock->expects($this->exactly(2)) + $this->notificationCenterMock->expects($this->exactly(9)) ->method('sendNotifications'); //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(2)) + $optimizelyMock->expects($this->exactly(5)) ->method('sendImpressionEvent'); $optimizelyMock->notificationCenter = $this->notificationCenterMock; - - $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature']); - - $expectedOptimizelyDecision1 = new OptimizelyDecision( - 'control', - true, - ['double_variable' => 42.42], - 'test_experiment_double_feature', - 'double_single_variable_feature', - $userContext, - [] - ); - - $expectedOptimizelyDecision2 = new OptimizelyDecision( - 'control', - true, - [], - 'test_experiment_double_feature', - 'boolean_feature', - $userContext, - [] - ); - - $this->assertEquals(count($optimizelyDecision), 2); - // $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature']); - - $this->compareOptimizelyDecisions($expectedOptimizelyDecision1, $optimizelyDecision['double_single_variable_feature']); - $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecision['boolean_feature']); - $optimizelyDecision = array_filter($optimizelyDecision, function ($decision) { - return $decision->getEnabled(); - }); + $keys = ['boolean_feature', + 'double_single_variable_feature', + 'integer_single_variable_feature', + 'boolean_single_variable_feature', + 'string_single_variable_feature', + 'multiple_variables_feature', + 'multi_variate_feature', + 'mutex_group_feature', + 'empty_feature' + ]; + + $optimizelyDecisions = $optimizelyMock->decideForKeys($userContext, $keys, $decideOptions); + $this->assertEquals(count($optimizelyDecisions), 6); } - public function testDecideForKeysForEnabledFlagsOnly() + public function testDecideAllCallsDecideForKeysAndReturnsItsResponse() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('sendImpressionEvent')) + ->setMethods(array('decideForKeys')) ->getMock(); + $decideOptions = ['ENABLED_FLAGS_ONLY']; + $keys = ['boolean_feature', + 'double_single_variable_feature', + 'integer_single_variable_feature', + 'boolean_single_variable_feature', + 'string_single_variable_feature', + 'multiple_variables_feature', + 'multi_variate_feature', + 'mutex_group_feature', + 'empty_feature' + ]; $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock)) - ->setMethods(array('getVariationForFeature')) - ->getMock(); - - $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); - $decisionService->setAccessible(true); - $decisionService->setValue($optimizelyMock, $decisionServiceMock); - - // Mock getVariationForFeature to return a valid decision with experiment and variation keys - $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); - $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); - - $expected_decision = new FeatureDecision( - $experiment, - $variation, - FeatureDecision::DECISION_SOURCE_FEATURE_TEST - ); - - $decisionServiceMock->expects($this->exactly(2)) - ->method('getVariationForFeature') - ->will($this->returnValue($expected_decision)); - - $this->notificationCenterMock->expects($this->exactly(2)) - ->method('sendNotifications'); - - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(2)) - ->method('sendImpressionEvent'); - - $optimizelyMock->notificationCenter = $this->notificationCenterMock; + //assert that decideForKeys is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('decideForKeys') + ->with($userContext, $keys, $decideOptions) + ->willReturn('Response from decideForKeys'); - $optimizelyDecision = $optimizelyMock->decideForKeys($userContext, ['double_single_variable_feature', 'boolean_feature'], ['ENABLED_FLAGS_ONLY']); - - $expectedOptimizelyDecision1 = new OptimizelyDecision( - 'control', - true, - ['double_variable' => 42.42], - 'test_experiment_double_feature', - 'double_single_variable_feature', - $userContext, - [] - ); - - $expectedOptimizelyDecision2 = new OptimizelyDecision( - 'control', - true, - [], - 'test_experiment_double_feature', - 'boolean_feature', - $userContext, - [] - ); - - $optimizelyDecision = array_filter($optimizelyDecision, function ($decision) { - return $decision->getEnabled(); - }); - - $this->assertEquals(count($optimizelyDecision), 2); + $optimizelyDecisions = $optimizelyMock->decideAll($userContext, $decideOptions); + $this->assertEquals('Response from decideForKeys', $optimizelyDecisions); } public function testActivateInvalidOptimizelyObject() From bc7ba1cb60597086544ed09ac2014f008c69cb08 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 9 Dec 2020 19:49:04 +0500 Subject: [PATCH 14/21] finalize --- src/Optimizely/Bucketer.php | 2 ++ src/Optimizely/DecisionService/DecisionService.php | 14 +++++++++++++- src/Optimizely/Optimizely.php | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Optimizely/Bucketer.php b/src/Optimizely/Bucketer.php index 414f2429..615b0d7f 100644 --- a/src/Optimizely/Bucketer.php +++ b/src/Optimizely/Bucketer.php @@ -109,6 +109,7 @@ protected function generateBucketValue($bucketingKey) * @param $userId string ID for user. * @param $parentId mixed ID representing Experiment or Group. * @param $trafficAllocations array Traffic allocations for variation or experiment. + * @param $decideReasons array Evaluation Logs. * * @return string ID representing experiment or variation. */ @@ -138,6 +139,7 @@ private function findBucket($bucketingId, $userId, $parentId, $trafficAllocation * @param $experiment Experiment Experiment or Rollout rule in which user is to be bucketed. * @param $bucketingId string A customer-assigned value used to create the key for the murmur hash. * @param $userId string User identifier. + * @param $decideReasons array Evaluation Logs. * * @return Variation Variation which will be shown to the user. */ diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 803c898b..3b5c418c 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -93,6 +93,7 @@ public function __construct(LoggerInterface $logger, UserProfileServiceInterface * * @param string $userId user ID * @param array $userAttributes user attributes + * @param array $decideReasons evaluation logs * * @return String representing bucketing ID if it is a String type in attributes else return user ID. */ @@ -119,6 +120,8 @@ protected function getBucketingId($userId, $userAttributes, &$decideReasons = [] * @param $experiment Experiment Experiment to get the variation for. * @param $userId string User identifier. * @param $attributes array Attributes of the user. + * @param $decideOptions array Options to customize evaluation. + * @param $decideReasons array Evaluation Logs. * * @return Variation Variation which the user is bucketed into. */ @@ -202,6 +205,8 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ * @param FeatureFlag $featureFlag The feature flag the user wants to access * @param string $userId user ID * @param array $userAttributes user attributes + * @param array $decideOptions Options to customize evaluation. + * @param array $decideReasons Evaluation Logs. * @return Decision if getVariationForFeatureExperiment or getVariationForFeatureRollout returns a Decision * null otherwise */ @@ -247,6 +252,8 @@ public function getVariationForFeature(ProjectConfigInterface $projectConfig, Fe * @param FeatureFlag $featureFlag The feature flag the user wants to access * @param string $userId user id * @param array $userAttributes user userAttributes + * @param array $decideOptions Options to customize evaluation. + * @param array $decideReasons Evaluation Logs. * @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 */ @@ -306,6 +313,7 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project * @param FeatureFlag $featureFlag The feature flag the user wants to access * @param string $userId user id * @param array $userAttributes user userAttributes + * @param array $decideReasons Evaluation Logs. * @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 @@ -387,6 +395,7 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. * @param $experimentKey string Key for experiment. * @param $userId string The user Id. + * @param $decideReasons array Evaluation Logs. * * @return Variation The variation which the given user and experiment should be forced into. */ @@ -478,6 +487,7 @@ public function setForcedVariation(ProjectConfigInterface $projectConfig, $exper * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. * @param $experiment Experiment Experiment in which user is to be bucketed. * @param $userId string string + * @param $decideReasons array Evaluation Logs. * * @return null|Variation Representing the variation the user is forced into. */ @@ -504,7 +514,8 @@ private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, /** * Get the stored user profile for the given user ID. * - * @param $userId string the ID of the user. + * @param $userId string ID of the user. + * @param $decideReasons array Evaluation Logs. * * @return null|UserProfile the stored user profile. */ @@ -544,6 +555,7 @@ private function getStoredUserProfile($userId, &$decideReasons = []) * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. * @param $experiment Experiment The experiment for which we are getting the stored variation. * @param $userProfile UserProfile The user profile from which we are getting the stored variation. + * @param $decideReasons array Evaluation Logs. * * @return null|Variation the stored variation or null if not found. */ diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index bcff2192..a2cb5567 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -99,6 +99,9 @@ class Optimizely */ private $_logger; + /** + * @var array A default list of options for decision making. + */ private $defaultDecideOptions; /** From 4681d87a6162741adee84729f4b3fd8246c9a9a9 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 9 Dec 2020 21:01:53 +0500 Subject: [PATCH 15/21] test on php 7.3.24 --- .travis.yml | 2 +- tests/OptimizelyTest.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e5a233da..4f0dc6ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ php: - '7.0' - '7.1' - '7.2' - - '7.3' + - '7.3.24' # revert it back to 7.3 once 7.3.25 latest build gets fixed. install: "composer install" script: - mkdir -p build/logs diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 4ebf012b..1a6631a8 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -16,8 +16,6 @@ */ namespace Optimizely\Tests; -// require(dirname(__FILE__).'/TestData.php'); - use Exception; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; From 10200dc39ef21f1a70419479e1fd8ebe211f470d Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 11 Dec 2020 18:48:50 +0500 Subject: [PATCH 16/21] Apply suggestions from code review Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> --- src/Optimizely/Optimizely.php | 2 +- tests/OptimizelyTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index a2cb5567..ac4d40d3 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -410,7 +410,7 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp $userAttributes, (object) array( 'flagKey'=> $flagKey, - 'featureEnabled'=> $featureEnabled, + 'enabled'=> $featureEnabled, 'variables' => $allVariables, 'variation' => $variationKey, 'ruleKey' => $ruleKey, diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 1a6631a8..51293c19 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -428,7 +428,7 @@ public function testDecide() ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, + 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', From 4bebeeff82cca9bce6ed8481446f03ffa68d5b1c Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 11 Dec 2020 21:03:30 +0500 Subject: [PATCH 17/21] address comments --- .../DecisionService/DecisionService.php | 1 - src/Optimizely/Optimizely.php | 5 +- tests/OptimizelyTest.php | 522 ++++++++++++++++-- tests/OptimizelyUserContextTests.php | 11 + 4 files changed, 487 insertions(+), 52 deletions(-) diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 3b5c418c..92381ef2 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -418,7 +418,6 @@ public function getForcedVariation(ProjectConfigInterface $projectConfig, $exper if (!isset($experimentToVariationMap[$experimentId])) { $message = sprintf('No experiment "%s" mapped to user "%s" in the forced variation map.', $experimentKey, $userId); $this->_logger->log(Logger::DEBUG, $message); - $decideReasons[] = $message; return null; } diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index ac4d40d3..4dd25f5e 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -332,7 +332,7 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp } // merge decide options and default decide options - $decideOptions += $this->defaultDecideOptions; + $decideOptions = array_merge($decideOptions, $this->defaultDecideOptions); // create optimizely decision result $userId = $userContext->getUserId(); @@ -482,6 +482,9 @@ public function decideForKeys(OptimizelyUserContext $userContext, array $keys, a return []; } + // merge decide options and default decide options + $decideOptions = array_merge($decideOptions, $this->defaultDecideOptions); + $enabledFlagsOnly = in_array(OptimizelyDecideOption::ENABLED_FLAGS_ONLY, $decideOptions); $decisions = []; diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 51293c19..6a8d88e5 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -126,7 +126,6 @@ public function compareOptimizelyDecisions(OptimizelyDecision $expectedOptimizel $this->assertEquals($expectedOptimizelyDecision->getRuleKey(), $optimizelyDecision->getRuleKey()); $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); $this->assertEquals($expectedOptimizelyDecision->getUserContext(), $optimizelyDecision->getUserContext()); - $this->assertEquals($expectedOptimizelyDecision->getFlagKey(), $optimizelyDecision->getFlagKey()); } public function testIsValidForInvalidOptimizelyObject() @@ -588,7 +587,7 @@ public function testDecidewhenUserIsBucketedIntoFeatureExperiment() ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, + 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', @@ -679,7 +678,7 @@ public function testDecidewhenUserIsBucketedIntoRolloutAndSendFlagDecisionIsTrue ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, + 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', @@ -770,7 +769,7 @@ public function testDecidewhenUserIsBucketedIntoRolloutAndSendFlagDecisionIsFals ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, + 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', @@ -850,7 +849,7 @@ public function testDecidewhenDecisionServiceReturnsNullAndSendFlagDecisionIsTru ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> false, + 'enabled'=> false, 'variables'=> ["double_variable" => 14.99], 'variation' => null, 'ruleKey' => null, @@ -934,7 +933,7 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisionIsFalse() ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> false, + 'enabled'=> false, 'variables'=> ["double_variable" => 14.99], 'variation' => null, 'ruleKey' => null, @@ -1010,7 +1009,7 @@ public function testDecideOptionDisableDecisionEvent() ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, + 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', @@ -1035,7 +1034,73 @@ public function testDecideOptionDisableDecisionEvent() $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['DISABLE_DECISION_EVENT']); } - public function testDecideOptionExcludeVariable() + public function testDecideOptionDisableDecisionEventWhenPassedInDefaultOptions() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array( + $this->datafile, null, $this->loggerMock, null, null, null, null, null, null, ['DISABLE_DECISION_EVENT'] + )) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => false + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is not called. + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + } + + public function testDecideOptionExcludeVariables() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) @@ -1074,7 +1139,7 @@ public function testDecideOptionExcludeVariable() ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, + 'enabled'=> true, 'variables'=> [], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', @@ -1119,14 +1184,15 @@ public function testDecideOptionExcludeVariable() $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['EXCLUDE_VARIABLES']); $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); - $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); } - public function testDecidewithAllDecideOptionsSet() + public function testDecideOptionExcludeVariablesWhenPassedInDefaultOptions() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) - ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('sendImpressionEvent')) + ->setConstructorArgs(array( + $this->datafile, null, $this->loggerMock, null, null, null, null, null, null, ['EXCLUDE_VARIABLES'] + )) + ->setMethods(array('sendImpressionEvent', 'dispatchEvent')) ->getMock(); @@ -1161,12 +1227,12 @@ public function testDecidewithAllDecideOptionsSet() ['device_type' => 'iPhone'], (object) array( 'flagKey'=>'double_single_variable_feature', - 'featureEnabled'=> true, + 'enabled'=> true, 'variables'=> [], 'variation' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], - 'decisionEventDispatched' => false + 'decisionEventDispatched' => true ) ); @@ -1178,7 +1244,145 @@ public function testDecidewithAllDecideOptionsSet() ); //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->never()) + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + [], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testDecideOptionEnabledFlagsOnly() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decideOptions = ['ENABLED_FLAGS_ONLY']; + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $this->notificationCenterMock->expects($this->exactly(9)) + ->method('sendNotifications'); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(5)) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $keys = ['boolean_feature', + 'double_single_variable_feature', + 'integer_single_variable_feature', + 'boolean_single_variable_feature', + 'string_single_variable_feature', + 'multiple_variables_feature', + 'multi_variate_feature', + 'mutex_group_feature', + 'empty_feature' + ]; + + $optimizelyDecisions = $optimizelyMock->decideForKeys($userContext, $keys, $decideOptions); + $this->assertEquals(count($optimizelyDecisions), 6); + } + + public function testDecideOptionEnabledFlagsOnlyWhenPassedInDefaultOptions() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array( + $this->datafile, null, $this->loggerMock, null, null, null, null, null, null, ['ENABLED_FLAGS_ONLY'] + )) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $this->notificationCenterMock->expects($this->exactly(9)) + ->method('sendNotifications'); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(5)) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $keys = ['boolean_feature', + 'double_single_variable_feature', + 'integer_single_variable_feature', + 'boolean_single_variable_feature', + 'string_single_variable_feature', + 'multiple_variables_feature', + 'multi_variate_feature', + 'mutex_group_feature', + 'empty_feature' + ]; + + $optimizelyDecisions = $optimizelyMock->decideForKeys($userContext, $keys); + $this->assertEquals(count($optimizelyDecisions), 6); + } + + public function testDecideOptionIncludeReasons() + { + $optimizely = new Optimizely($this->datafile); + $userContext = $optimizely->createUserContext('test_user', ['device_type' => 'iPhone']); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $expectedReasons = [ + 'User "test_user" is in variation control of experiment test_experiment_double_feature.', + "The user 'test_user' is bucketed into experiment 'test_experiment_double_feature' of feature 'double_single_variable_feature'." + ]; + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => $expectedReasons, + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) ->method('sendImpressionEvent') ->with( $this->projectConfig, @@ -1191,6 +1395,258 @@ public function testDecidewithAllDecideOptionsSet() 'test_user', ['device_type' => 'iPhone'] ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['INCLUDE_REASONS']); + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + ['double_variable' => 42.42], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + $this->assertEquals($expectedReasons, $optimizelyDecision->getReasons()); + } + + public function testDecideOptionIncludeReasonsWhenPassedInDefaultOptions() + { + $optimizely = new Optimizely($this->datafile); + $userContext = $optimizely->createUserContext('test_user', ['device_type' => 'iPhone']); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array( + $this->datafile, null, $this->loggerMock, null, null, null, null, null, null, ['INCLUDE_REASONS'] + )) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $expectedReasons = [ + "The feature flag 'empty_feature' is not used in any experiments.", + "Feature flag 'empty_feature' is not used in a rollout.", + "User 'test_user' is not bucketed into rollout for feature flag 'empty_feature'." + ]; + + // Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'empty_feature', + 'enabled'=> false, + 'variables'=> [], + 'variation' => null, + 'ruleKey' => null, + 'reasons' => $expectedReasons, + 'decisionEventDispatched' => false + ) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'empty_feature'); + $expectedOptimizelyDecision = new OptimizelyDecision( + null, + false, + [], + null, + 'empty_feature', + $userContext, + [] + ); + + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + $this->assertEquals($expectedReasons, $optimizelyDecision->getReasons()); + } + + public function testDecideParamOptionsWorkTogetherWithDefaultOptions() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array( + $this->datafile, null, $this->loggerMock, null, null, null, null, null, null, ['EXCLUDE_VARIABLES'] + )) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> true, + 'variables'=> [], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => false + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is not called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $expectedOptimizelyDecision = new OptimizelyDecision( + 'control', + true, + [], + 'test_experiment_double_feature', + 'double_single_variable_feature', + $userContext, + [] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['DISABLE_DECISION_EVENT']); + $this->compareOptimizelyDecisions($expectedOptimizelyDecision, $optimizelyDecision); + } + + public function testDecideForKeysParamOptionsWorkTogetherWithDefaultOptions() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array( + $this->datafile, null, $this->loggerMock, null, null, null, null, null, null, ['ENABLED_FLAGS_ONLY'] + )) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $this->notificationCenterMock->expects($this->exactly(9)) + ->method('sendNotifications'); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $keys = ['boolean_feature', + 'double_single_variable_feature', + 'integer_single_variable_feature', + 'boolean_single_variable_feature', + 'string_single_variable_feature', + 'multiple_variables_feature', + 'multi_variate_feature', + 'mutex_group_feature', + 'empty_feature' + ]; + + $optimizelyDecisions = $optimizelyMock->decideForKeys($userContext, $keys, ['DISABLE_DECISION_EVENT']); + $this->assertEquals(count($optimizelyDecisions), 6); + } + + public function testDecidewithAllDecideOptionsSet() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment, + $variation, + FeatureDecision::DECISION_SOURCE_FEATURE_TEST + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> true, + 'variables'=> [], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => false + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is not called. + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + $expectedOptimizelyDecision = new OptimizelyDecision( 'control', true, @@ -1253,40 +1709,6 @@ public function testDecideAll() $this->compareOptimizelyDecisions($expectedOptimizelyDecision2, $optimizelyDecisions['boolean_feature']); } - public function testDecideOptionEnabledFlagsOnly() - { - $optimizelyMock = $this->getMockBuilder(Optimizely::class) - ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) - ->setMethods(array('sendImpressionEvent')) - ->getMock(); - - $decideOptions = ['ENABLED_FLAGS_ONLY']; - $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); - - $this->notificationCenterMock->expects($this->exactly(9)) - ->method('sendNotifications'); - - //assert that sendImpressionEvent is called with expected params - $optimizelyMock->expects($this->exactly(5)) - ->method('sendImpressionEvent'); - - $optimizelyMock->notificationCenter = $this->notificationCenterMock; - - $keys = ['boolean_feature', - 'double_single_variable_feature', - 'integer_single_variable_feature', - 'boolean_single_variable_feature', - 'string_single_variable_feature', - 'multiple_variables_feature', - 'multi_variate_feature', - 'mutex_group_feature', - 'empty_feature' - ]; - - $optimizelyDecisions = $optimizelyMock->decideForKeys($userContext, $keys, $decideOptions); - $this->assertEquals(count($optimizelyDecisions), 6); - } - public function testDecideAllCallsDecideForKeysAndReturnsItsResponse() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) diff --git a/tests/OptimizelyUserContextTests.php b/tests/OptimizelyUserContextTests.php index e191cfba..2a3e3e6b 100644 --- a/tests/OptimizelyUserContextTests.php +++ b/tests/OptimizelyUserContextTests.php @@ -96,6 +96,17 @@ public function testSetAttributeOverridesValueOfExistingKey() $this->assertEquals(["browser" => "firefox"], $optUserContext->getAttributes()); } + public function testSetAttributeWhenNoAttributesProvidedInConstructor() + { + $userId = 'test_user'; + $optUserContext = new OptimizelyUserContext($this->optimizelyObject, $userId); + + $this->assertEquals([], $optUserContext->getAttributes()); + + $optUserContext->setAttribute('browser', 'firefox'); + $this->assertEquals(["browser" => "firefox"], $optUserContext->getAttributes()); + } + public function testDecideCallsAndReturnsOptimizelyDecideAPI() { $userId = 'test_user'; From af0fbc2763947535a7e1a46c9d9f12d2d8c85f51 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 14 Dec 2020 12:56:16 +0500 Subject: [PATCH 18/21] tests: ignore user profile --- src/Optimizely/Optimizely.php | 6 +- tests/OptimizelyTest.php | 298 ++++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+), 3 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 4dd25f5e..9d0dd1eb 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -401,6 +401,8 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp } } + $shouldIncludeReasons = in_array(OptimizelyDecideOption::INCLUDE_REASONS, $decideOptions); + // send notification $this->notificationCenter->sendNotifications( NotificationType::DECISION, @@ -414,14 +416,12 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp 'variables' => $allVariables, 'variation' => $variationKey, 'ruleKey' => $ruleKey, - 'reasons' => $decideReasons, + 'reasons' => $shouldIncludeReasons ? $decideReasons:[], 'decisionEventDispatched' => $decisionEventDispatched ) ) ); - $shouldIncludeReasons = in_array(OptimizelyDecideOption::INCLUDE_REASONS, $decideOptions); - return new OptimizelyDecision( $variationKey, $featureEnabled, diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 6a8d88e5..e4bc745f 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -47,6 +47,7 @@ use Optimizely\Logger\DefaultLogger; use Optimizely\Optimizely; use Optimizely\OptimizelyUserContext; +use Optimizely\UserProfile\UserProfileServiceInterface; class OptimizelyTest extends \PHPUnit_Framework_TestCase { @@ -1100,6 +1101,303 @@ public function testDecideOptionDisableDecisionEventWhenPassedInDefaultOptions() $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature'); } + public function testDecideRespectsUserProfileServiceLookup() + { + $userProfileServiceMock = $this->getMockBuilder(UserProfileServiceInterface::class) + ->getMock(); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock, null, null, $userProfileServiceMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + // mock userProfileServiceMock to return stored variation 'variation' rather than 'control'. + $storedUserProfile = array( + 'user_id' => 'test_user', + 'experiment_bucket_map' => array( + '122238' => array( + 'variation_id' => '122240' + ) + ) + ); + + $userProfileServiceMock->expects($this->once()) + ->method('lookup') + ->willReturn($storedUserProfile); + + + $userProfileServiceMock->expects($this->never()) + ->method('save'); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> false, + 'variables'=> ["double_variable" => 14.99], + 'variation' => 'variation', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'variation', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + false, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', []); + } + + public function testDecideRespectsUserProfileServiceSave() + { + $userProfileServiceMock = $this->getMockBuilder(UserProfileServiceInterface::class) + ->getMock(); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock, null, null, $userProfileServiceMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + // mock lookup to return null so that normal bucketing happens + $userProfileServiceMock->expects($this->once()) + ->method('lookup') + ->willReturn(null); + + + // assert that save is called with expected user profile map. + $storedUserProfile = array( + 'user_id' => 'test_user', + 'experiment_bucket_map' => array( + '122238' => array( + 'variation_id' => '122239' + ) + ) + ); + $userProfileServiceMock->expects($this->once()) + ->method('save') + ->with($storedUserProfile); + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', []); + } + + public function testDecideOptionIgnoreUserProfileService() + { + $userProfileServiceMock = $this->getMockBuilder(UserProfileServiceInterface::class) + ->getMock(); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock, null, null, $userProfileServiceMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + // mock userProfileServiceMock to return stored variation 'variation' rather than 'control'. + $storedUserProfile = array( + 'user_id' => 'test_user', + 'experiment_bucket_map' => array( + '122238' => array( + 'variation_id' => '122240' + ) + ) + ); + + // assert lookup isn't called. + $userProfileServiceMock->expects($this->never()) + ->method('lookup'); + + // assert save isn't called. + $userProfileServiceMock->expects($this->never()) + ->method('save'); + + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', ['IGNORE_USER_PROFILE_SERVICE']); + } + + public function testDecideOptionIgnoreUserProfileServiceWhenPassedInDefaultOptions() + { + $userProfileServiceMock = $this->getMockBuilder(UserProfileServiceInterface::class) + ->getMock(); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, + null, $this->loggerMock, null, null, $userProfileServiceMock, null, null, null, ['IGNORE_USER_PROFILE_SERVICE'] + )) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $userContext = $optimizelyMock->createUserContext('test_user', ['device_type' => 'iPhone']); + + // mock userProfileServiceMock to return stored variation 'variation' rather than 'control'. + $storedUserProfile = array( + 'user_id' => 'test_user', + 'experiment_bucket_map' => array( + '122238' => array( + 'variation_id' => '122240' + ) + ) + ); + + // assert lookup isn't called. + $userProfileServiceMock->expects($this->never()) + ->method('lookup'); + + // assert save isn't called. + $userProfileServiceMock->expects($this->never()) + ->method('save'); + + + //Verify that sendNotifications is called with expected params + $arrayParam = array( + DecisionNotificationTypes::FLAG, + 'test_user', + ['device_type' => 'iPhone'], + (object) array( + 'flagKey'=>'double_single_variable_feature', + 'enabled'=> true, + 'variables'=> ["double_variable" => 42.42], + 'variation' => 'control', + 'ruleKey' => 'test_experiment_double_feature', + 'reasons' => [], + 'decisionEventDispatched' => true + ) + ); + + $this->notificationCenterMock->expects($this->exactly(1)) + ->method('sendNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + //assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment_double_feature', + 'control', + 'double_single_variable_feature', + 'test_experiment_double_feature', + FeatureDecision::DECISION_SOURCE_FEATURE_TEST, + true, + 'test_user', + ['device_type' => 'iPhone'] + ); + + $optimizelyMock->notificationCenter = $this->notificationCenterMock; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'double_single_variable_feature', []); + } + public function testDecideOptionExcludeVariables() { $optimizelyMock = $this->getMockBuilder(Optimizely::class) From 7ebb70247927de159507ec5ccb13ee8dbbc3e73c Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Thu, 7 Jan 2021 16:27:30 +0500 Subject: [PATCH 19/21] update: headers --- src/Optimizely/Bucketer.php | 2 +- src/Optimizely/Decide/OptimizelyDecideOption.php | 2 +- src/Optimizely/Decide/OptimizelyDecision.php | 2 +- src/Optimizely/Decide/OptimizelyDecisionMessage.php | 2 +- src/Optimizely/DecisionService/DecisionService.php | 2 +- src/Optimizely/DecisionService/FeatureDecision.php | 2 +- src/Optimizely/Enums/DecisionNotificationTypes.php | 2 +- src/Optimizely/Optimizely.php | 2 +- src/Optimizely/OptimizelyUserContext.php | 2 +- tests/OptimizelyTest.php | 2 +- tests/OptimizelyUserContextTests.php | 2 +- tests/TestData.php | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Optimizely/Bucketer.php b/src/Optimizely/Bucketer.php index 615b0d7f..87b4a07a 100644 --- a/src/Optimizely/Bucketer.php +++ b/src/Optimizely/Bucketer.php @@ -1,6 +1,6 @@ Date: Fri, 8 Jan 2021 18:26:47 +0500 Subject: [PATCH 20/21] refact: return decide reasons instead of appending to a reference parameter (#221) * impact * fix Decision Service Tests * fix: test cases affected due to bucketer * fix: unit tests affected by reasons * fix: incompatible syntax for < php 7.0 * fix: some more tests * audience eval logs * tests: bucketer reasons * tests: Decision Service reasons * tests: decision service further * update: copyright headers --- src/Optimizely/Bucketer.php | 36 +-- .../DecisionService/DecisionService.php | 186 +++++++------ .../DecisionService/FeatureDecision.php | 15 +- src/Optimizely/Optimizely.php | 9 +- src/Optimizely/Utils/Validator.php | 24 +- tests/BucketerTest.php | 261 ++++++++++-------- .../DecisionServiceTest.php | 224 +++++++++------ tests/OptimizelyTest.php | 24 ++ tests/UtilsTests/ValidatorLoggingTest.php | 19 +- tests/UtilsTests/ValidatorTest.php | 40 +-- 10 files changed, 507 insertions(+), 331 deletions(-) diff --git a/src/Optimizely/Bucketer.php b/src/Optimizely/Bucketer.php index 87b4a07a..e6bd0dbe 100644 --- a/src/Optimizely/Bucketer.php +++ b/src/Optimizely/Bucketer.php @@ -109,12 +109,12 @@ protected function generateBucketValue($bucketingKey) * @param $userId string ID for user. * @param $parentId mixed ID representing Experiment or Group. * @param $trafficAllocations array Traffic allocations for variation or experiment. - * @param $decideReasons array Evaluation Logs. * - * @return string ID representing experiment or variation. + * @return [ string, array ] ID representing experiment or variation and array of log messages representing decision making. */ - private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations, &$decideReasons = []) + private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations) { + $decideReasons = []; // Generate the bucketing key based on combination of user ID and experiment ID or group ID. $bucketingKey = $bucketingId.$parentId; $bucketingNumber = $this->generateBucketValue($bucketingKey); @@ -125,11 +125,11 @@ private function findBucket($bucketingId, $userId, $parentId, $trafficAllocation foreach ($trafficAllocations as $trafficAllocation) { $currentEnd = $trafficAllocation->getEndOfRange(); if ($bucketingNumber < $currentEnd) { - return $trafficAllocation->getEntityId(); + return [$trafficAllocation->getEntityId(), $decideReasons]; } } - return null; + return [null, $decideReasons]; } /** @@ -139,14 +139,15 @@ private function findBucket($bucketingId, $userId, $parentId, $trafficAllocation * @param $experiment Experiment Experiment or Rollout rule in which user is to be bucketed. * @param $bucketingId string A customer-assigned value used to create the key for the murmur hash. * @param $userId string User identifier. - * @param $decideReasons array Evaluation Logs. * - * @return Variation Variation which will be shown to the user. + * @return [ Variation, array ] Variation which will be shown to the user and array of log messages representing decision making. */ - public function bucket(ProjectConfigInterface $config, Experiment $experiment, $bucketingId, $userId, &$decideReasons = []) + public function bucket(ProjectConfigInterface $config, Experiment $experiment, $bucketingId, $userId) { + $decideReasons = []; + if (is_null($experiment->getKey())) { - return null; + return [ null, $decideReasons ]; } // Determine if experiment is in a mutually exclusive group. @@ -155,15 +156,17 @@ public function bucket(ProjectConfigInterface $config, Experiment $experiment, $ $group = $config->getGroup($experiment->getGroupId()); if (is_null($group->getId())) { - return null; + return [ null, $decideReasons ]; } - $userExperimentId = $this->findBucket($bucketingId, $userId, $group->getId(), $group->getTrafficAllocation()); + list($userExperimentId, $reasons) = $this->findBucket($bucketingId, $userId, $group->getId(), $group->getTrafficAllocation()); + $decideReasons = array_merge($decideReasons, $reasons); + if (empty($userExperimentId)) { $message = sprintf('User "%s" is in no experiment.', $userId); $this->_logger->log(Logger::INFO, $message); $decideReasons[] = $message; - return null; + return [ null, $decideReasons ]; } if ($userExperimentId != $experiment->getId()) { @@ -176,7 +179,7 @@ public function bucket(ProjectConfigInterface $config, Experiment $experiment, $ $this->_logger->log(Logger::INFO, $message); $decideReasons[] = $message; - return null; + return [ null, $decideReasons ]; } $message = sprintf( @@ -191,13 +194,14 @@ public function bucket(ProjectConfigInterface $config, Experiment $experiment, $ } // Bucket user if not in whitelist and in group (if any). - $variationId = $this->findBucket($bucketingId, $userId, $experiment->getId(), $experiment->getTrafficAllocation()); + list($variationId, $reasons) = $this->findBucket($bucketingId, $userId, $experiment->getId(), $experiment->getTrafficAllocation()); + $decideReasons = array_merge($decideReasons, $reasons); if (!empty($variationId)) { $variation = $config->getVariationFromId($experiment->getKey(), $variationId); - return $variation; + return [ $variation, $decideReasons ]; } - return null; + return [ null, $decideReasons ]; } } diff --git a/src/Optimizely/DecisionService/DecisionService.php b/src/Optimizely/DecisionService/DecisionService.php index 69e8137b..72007503 100644 --- a/src/Optimizely/DecisionService/DecisionService.php +++ b/src/Optimizely/DecisionService/DecisionService.php @@ -93,24 +93,25 @@ public function __construct(LoggerInterface $logger, UserProfileServiceInterface * * @param string $userId user ID * @param array $userAttributes user attributes - * @param array $decideReasons evaluation logs * - * @return String representing bucketing ID if it is a String type in attributes else return user ID. + * @return [ String, array ] bucketing ID if it is a String type in attributes else return user ID and + * array of log messages representing decision making. */ - protected function getBucketingId($userId, $userAttributes, &$decideReasons = []) + protected function getBucketingId($userId, $userAttributes) { + $decideReasons = []; $bucketingIdKey = ControlAttributes::BUCKETING_ID; if (isset($userAttributes[$bucketingIdKey])) { if (is_string($userAttributes[$bucketingIdKey])) { - return $userAttributes[$bucketingIdKey]; + return [ $userAttributes[$bucketingIdKey], $decideReasons ]; } $message = 'Bucketing ID attribute is not a string. Defaulted to user ID.'; $this->_logger->log(Logger::WARNING, $message); $decideReasons[] = $message; } - return $userId; + return [ $userId, $decideReasons ]; } /** @@ -121,59 +122,68 @@ protected function getBucketingId($userId, $userAttributes, &$decideReasons = [] * @param $userId string User identifier. * @param $attributes array Attributes of the user. * @param $decideOptions array Options to customize evaluation. - * @param $decideReasons array Evaluation Logs. * - * @return Variation Variation which the user is bucketed into. + * @return [ Variation, array ] Variation which the user is bucketed into and array of log messages representing decision making. */ - public function getVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId, $attributes = null, $decideOptions = [], &$decideReasons = []) + public function getVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId, $attributes = null, $decideOptions = []) { - $bucketingId = $this->getBucketingId($userId, $attributes, $decideReasons); + $decideReasons = []; + list($bucketingId, $reasons) = $this->getBucketingId($userId, $attributes); + + $decideReasons = array_merge($decideReasons, $reasons); if (!$experiment->isExperimentRunning()) { $message = sprintf('Experiment "%s" is not running.', $experiment->getKey()); $this->_logger->log(Logger::INFO, $message); $decideReasons[] = $message; - return null; + return [ null, $decideReasons]; } // check if a forced variation is set - $forcedVariation = $this->getForcedVariation($projectConfig, $experiment->getKey(), $userId, $decideReasons); + list($forcedVariation, $reasons) = $this->getForcedVariation($projectConfig, $experiment->getKey(), $userId); + $decideReasons = array_merge($decideReasons, $reasons); if (!is_null($forcedVariation)) { - return $forcedVariation; + return [ $forcedVariation, $decideReasons]; } // check if the user has been whitelisted - $variation = $this->getWhitelistedVariation($projectConfig, $experiment, $userId, $decideReasons); + list($variation, $reasons) = $this->getWhitelistedVariation($projectConfig, $experiment, $userId); + $decideReasons = array_merge($decideReasons, $reasons); if (!is_null($variation)) { - return $variation; + return [ $variation, $decideReasons ]; } // check for sticky bucketing if (!in_array(OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE, $decideOptions)) { $userProfile = new UserProfile($userId); if (!is_null($this->_userProfileService)) { - $storedUserProfile = $this->getStoredUserProfile($userId, $decideReasons); + list($storedUserProfile, $reasons) = $this->getStoredUserProfile($userId); + $decideReasons = array_merge($decideReasons, $reasons); if (!is_null($storedUserProfile)) { $userProfile = $storedUserProfile; - $variation = $this->getStoredVariation($projectConfig, $experiment, $userProfile, $decideReasons); + list($variation, $reasons) = $this->getStoredVariation($projectConfig, $experiment, $userProfile); + $decideReasons = array_merge($decideReasons, $reasons); if (!is_null($variation)) { - return $variation; + return [ $variation, $decideReasons ]; } } } } - if (!Validator::doesUserMeetAudienceConditions($projectConfig, $experiment, $attributes, $this->_logger)) { + list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($projectConfig, $experiment, $attributes, $this->_logger); + $decideReasons = array_merge($decideReasons, $reasons); + if (!$evalResult) { $message = sprintf('User "%s" does not meet conditions to be in experiment "%s".', $userId, $experiment->getKey()); $this->_logger->log( Logger::INFO, $message ); $decideReasons[] = $message; - return null; + return [ null, $decideReasons]; } - $variation = $this->_bucketer->bucket($projectConfig, $experiment, $bucketingId, $userId, $decideReasons); + list($variation, $reasons) = $this->_bucketer->bucket($projectConfig, $experiment, $bucketingId, $userId); + $decideReasons = array_merge($decideReasons, $reasons); if ($variation === null) { $message = sprintf('User "%s" is in no variation.', $userId); $this->_logger->log(Logger::INFO, $message); @@ -195,7 +205,7 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ $decideReasons[] = $message; } - return $variation; + return [ $variation, $decideReasons ]; } /** @@ -206,33 +216,39 @@ public function getVariation(ProjectConfigInterface $projectConfig, Experiment $ * @param string $userId user ID * @param array $userAttributes user attributes * @param array $decideOptions Options to customize evaluation. - * @param array $decideReasons Evaluation Logs. - * @return Decision if getVariationForFeatureExperiment or getVariationForFeatureRollout returns a Decision - * null otherwise + * + * @return FeatureDecision representing decision. */ - public function getVariationForFeature(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = [], &$decideReasons = []) + public function getVariationForFeature(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = []) { + $decideReasons = []; + //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($projectConfig, $featureFlag, $userId, $userAttributes, $decideOptions, $decideReasons); - if ($decision) { + $decision = $this->getVariationForFeatureExperiment($projectConfig, $featureFlag, $userId, $userAttributes, $decideOptions); + if ($decision->getVariation()) { return $decision; } + $decideReasons = array_merge($decideReasons, $decision->getReasons()); + // Check if the feature flag has rollout and the user is bucketed into one of it's rules - $decision = $this->getVariationForFeatureRollout($projectConfig, $featureFlag, $userId, $userAttributes, $decideReasons); - if ($decision) { + $decision = $this->getVariationForFeatureRollout($projectConfig, $featureFlag, $userId, $userAttributes); + $decideReasons = array_merge($decideReasons, $decision->getReasons()); + + if ($decision->getVariation()) { $message = "User '{$userId}' is bucketed into rollout for feature flag '{$featureFlag->getKey()}'."; $this->_logger->log( Logger::INFO, $message ); + $decideReasons[] = $message; - return $decision; + return new FeatureDecision($decision->getExperiment(), $decision->getVariation(), FeatureDecision::DECISION_SOURCE_ROLLOUT, $decideReasons); } $message = "User '{$userId}' is not bucketed into rollout for feature flag '{$featureFlag->getKey()}'."; @@ -242,7 +258,7 @@ public function getVariationForFeature(ProjectConfigInterface $projectConfig, Fe ); $decideReasons[] = $message; - return new FeatureDecision(null, null, FeatureDecision::DECISION_SOURCE_ROLLOUT); + return new FeatureDecision(null, null, FeatureDecision::DECISION_SOURCE_ROLLOUT, $decideReasons); } /** @@ -253,12 +269,12 @@ public function getVariationForFeature(ProjectConfigInterface $projectConfig, Fe * @param string $userId user id * @param array $userAttributes user userAttributes * @param array $decideOptions Options to customize evaluation. - * @param array $decideReasons Evaluation Logs. - * @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 + * + * @return FeatureDecision representing decision. */ - public function getVariationForFeatureExperiment(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = [], &$decideReasons = []) + public function getVariationForFeatureExperiment(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, $decideOptions = []) { + $decideReasons = []; $featureFlagKey = $featureFlag->getKey(); $experimentIds = $featureFlag->getExperimentIds(); @@ -270,7 +286,7 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project $message ); $decideReasons[] = $message; - return null; + return new FeatureDecision(null, null, null, $decideReasons); } // Evaluate each experiment ID and return the first bucketed experiment variation @@ -281,7 +297,8 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project continue; } - $variation = $this->getVariation($projectConfig, $experiment, $userId, $userAttributes, $decideOptions, $decideReasons); + list($variation, $reasons) = $this->getVariation($projectConfig, $experiment, $userId, $userAttributes, $decideOptions); + $decideReasons = array_merge($decideReasons, $reasons); if ($variation && $variation->getKey()) { $message = "The user '{$userId}' is bucketed into experiment '{$experiment->getKey()}' of feature '{$featureFlagKey}'."; $this->_logger->log( @@ -290,7 +307,7 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project ); $decideReasons[] = $message; - return new FeatureDecision($experiment, $variation, FeatureDecision::DECISION_SOURCE_FEATURE_TEST); + return new FeatureDecision($experiment, $variation, FeatureDecision::DECISION_SOURCE_FEATURE_TEST, $decideReasons); } } @@ -301,7 +318,7 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project ); $decideReasons[] = $message; - return null; + return new FeatureDecision(null, null, null, $decideReasons); } /** @@ -313,15 +330,12 @@ public function getVariationForFeatureExperiment(ProjectConfigInterface $project * @param FeatureFlag $featureFlag The feature flag the user wants to access * @param string $userId user id * @param array $userAttributes user userAttributes - * @param array $decideReasons Evaluation Logs. - * @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 + * @return FeatureDecision representing decision. */ - public function getVariationForFeatureRollout(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes, &$decideReasons = []) + public function getVariationForFeatureRollout(ProjectConfigInterface $projectConfig, FeatureFlag $featureFlag, $userId, $userAttributes) { - $bucketing_id = $this->getBucketingId($userId, $userAttributes, $decideReasons); + $decideReasons = []; + list($bucketing_id, $reasons) = $this->getBucketingId($userId, $userAttributes); $featureFlagKey = $featureFlag->getKey(); $rollout_id = $featureFlag->getRolloutId(); if (empty($rollout_id)) { @@ -331,17 +345,17 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon $message ); $decideReasons[] = $message; - return null; + return new FeatureDecision(null, null, null, $decideReasons); } $rollout = $projectConfig->getRolloutFromId($rollout_id); if ($rollout && !($rollout->getId())) { // Error logged and thrown in getRolloutFromId - return null; + return new FeatureDecision(null, null, null, $decideReasons); } $rolloutRules = $rollout->getExperiments(); if (sizeof($rolloutRules) == 0) { - return null; + return new FeatureDecision(null, null, null, $decideReasons); } // Evaluate all rollout rules except for last one @@ -349,7 +363,9 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon $rolloutRule = $rolloutRules[$i]; // Evaluate if user meets the audience condition of this rollout rule - if (!Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', $i + 1)) { + list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', $i + 1); + $decideReasons = array_merge($decideReasons, $reasons); + if (!$evalResult) { $message = sprintf("User '%s' does not meet conditions for targeting rule %s.", $userId, $i+1); $this->_logger->log( Logger::DEBUG, @@ -361,9 +377,10 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon } // Evaluate if user satisfies the traffic allocation for this rollout rule - $variation = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId, $decideReasons); + list($variation, $reasons) = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId); + $decideReasons = array_merge($decideReasons, $reasons); if ($variation && $variation->getKey()) { - return new FeatureDecision($rolloutRule, $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT); + return new FeatureDecision($rolloutRule, $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT, $decideReasons); } break; } @@ -371,21 +388,24 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon $rolloutRule = $rolloutRules[sizeof($rolloutRules) - 1]; // Evaluate if user meets the audience condition of Everyone Else Rule / Last Rule now - if (!Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', 'Everyone Else')) { + list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($projectConfig, $rolloutRule, $userAttributes, $this->_logger, 'Optimizely\Enums\RolloutAudienceEvaluationLogs', 'Everyone Else'); + $decideReasons = array_merge($decideReasons, $reasons); + if (!$evalResult) { $message = sprintf("User '%s' does not meet conditions for targeting rule 'Everyone Else'.", $userId); $this->_logger->log( Logger::DEBUG, $message ); $decideReasons[] = $message; - return null; + return new FeatureDecision(null, null, null, $decideReasons); } - $variation = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId, $decideReasons); + list($variation, $reasons) = $this->_bucketer->bucket($projectConfig, $rolloutRule, $bucketing_id, $userId); + $decideReasons = array_merge($decideReasons, $reasons); if ($variation && $variation->getKey()) { return new FeatureDecision($rolloutRule, $variation, FeatureDecision::DECISION_SOURCE_ROLLOUT); } - return null; + return new FeatureDecision(null, null, null, $decideReasons); } @@ -395,15 +415,16 @@ public function getVariationForFeatureRollout(ProjectConfigInterface $projectCon * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. * @param $experimentKey string Key for experiment. * @param $userId string The user Id. - * @param $decideReasons array Evaluation Logs. * - * @return Variation The variation which the given user and experiment should be forced into. + * @return [ Variation, array ] The variation which the given user and experiment should be forced into and + * array of log messages representing decision making. */ - public function getForcedVariation(ProjectConfigInterface $projectConfig, $experimentKey, $userId, &$decideReasons = []) + public function getForcedVariation(ProjectConfigInterface $projectConfig, $experimentKey, $userId) { + $decideReasons = []; if (!isset($this->_forcedVariationMap[$userId])) { $this->_logger->log(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); - return null; + return [ null, $decideReasons]; } $experimentToVariationMap = $this->_forcedVariationMap[$userId]; @@ -412,13 +433,13 @@ public function getForcedVariation(ProjectConfigInterface $projectConfig, $exper // check for null and empty string experiment ID if (strlen($experimentId) == 0) { // this case is logged in getExperimentFromKey - return null; + return [ null, $decideReasons]; } if (!isset($experimentToVariationMap[$experimentId])) { $message = sprintf('No experiment "%s" mapped to user "%s" in the forced variation map.', $experimentKey, $userId); $this->_logger->log(Logger::DEBUG, $message); - return null; + return [ null, $decideReasons]; } $variationId = $experimentToVariationMap[$experimentId]; @@ -428,7 +449,7 @@ public function getForcedVariation(ProjectConfigInterface $projectConfig, $exper $message = sprintf('Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map', $variationKey, $experimentKey, $userId); $this->_logger->log(Logger::DEBUG, $message); $decideReasons[] = $message; - return $variation; + return [ $variation, $decideReasons]; } /** @@ -486,12 +507,13 @@ public function setForcedVariation(ProjectConfigInterface $projectConfig, $exper * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. * @param $experiment Experiment Experiment in which user is to be bucketed. * @param $userId string string - * @param $decideReasons array Evaluation Logs. * - * @return null|Variation Representing the variation the user is forced into. + * @return [ Variation, array ] Representing the variation the user is forced into and + * array of log messages representing decision making. */ - private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId, &$decideReasons = []) + private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, $userId) { + $decideReasons = []; // Check if user is whitelisted for a variation. $forcedVariations = $experiment->getForcedVariations(); if (!is_null($forcedVariations) && isset($forcedVariations[$userId])) { @@ -503,25 +525,26 @@ private function getWhitelistedVariation(ProjectConfigInterface $projectConfig, $this->_logger->log(Logger::INFO, $message); $decideReasons[] = $message; } else { - return null; + return [ null, $decideReasons ]; } - return $variation; + return [ $variation, $decideReasons]; } - return null; + return [ null, $decideReasons ]; } /** * Get the stored user profile for the given user ID. * * @param $userId string ID of the user. - * @param $decideReasons array Evaluation Logs. * - * @return null|UserProfile the stored user profile. + * @return [UserProfile, array] the stored user profile and array of log messages representing decision making. */ - private function getStoredUserProfile($userId, &$decideReasons = []) + private function getStoredUserProfile($userId) { + $decideReasons = []; + if (is_null($this->_userProfileService)) { - return null; + return [ null, $decideReasons ]; } try { @@ -532,7 +555,8 @@ private function getStoredUserProfile($userId, &$decideReasons = []) sprintf('No user profile found for user with ID "%s".', $userId) ); } elseif (UserProfileUtils::isValidUserProfileMap($userProfileMap)) { - return UserProfileUtils::convertMapToUserProfile($userProfileMap); + $userProfile = UserProfileUtils::convertMapToUserProfile($userProfileMap); + return [ $userProfile, $decideReasons]; } else { $this->_logger->log( Logger::WARNING, @@ -545,7 +569,7 @@ private function getStoredUserProfile($userId, &$decideReasons = []) $decideReasons[] = $message; } - return null; + return [ null, $decideReasons ]; } /** @@ -554,12 +578,12 @@ private function getStoredUserProfile($userId, &$decideReasons = []) * @param $projectConfig ProjectConfigInterface ProjectConfigInterface instance. * @param $experiment Experiment The experiment for which we are getting the stored variation. * @param $userProfile UserProfile The user profile from which we are getting the stored variation. - * @param $decideReasons array Evaluation Logs. * - * @return null|Variation the stored variation or null if not found. + * @return [ Variation, array ] the stored variation or null if not found, and array of log messages representing decision making. */ - private function getStoredVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, UserProfile $userProfile, &$decideReasons = []) + private function getStoredVariation(ProjectConfigInterface $projectConfig, Experiment $experiment, UserProfile $userProfile) { + $decideReasons = []; $experimentKey = $experiment->getKey(); $userId = $userProfile->getUserId(); $variationId = $userProfile->getVariationForExperiment($experiment->getId()); @@ -569,7 +593,7 @@ private function getStoredVariation(ProjectConfigInterface $projectConfig, Exper Logger::INFO, sprintf('No previously activated variation of experiment "%s" for user "%s" found in user profile.', $experimentKey, $userId) ); - return null; + return [ null, $decideReasons ]; } $variation = $projectConfig->getVariationFromId($experimentKey, $variationId); @@ -588,7 +612,7 @@ private function getStoredVariation(ProjectConfigInterface $projectConfig, Exper $decideReasons[] = $message; - return null; + return [ null, $decideReasons ]; } $this->_logger->log( @@ -600,7 +624,7 @@ private function getStoredVariation(ProjectConfigInterface $projectConfig, Exper $userId ) ); - return $variation; + return [ $variation, $decideReasons ]; } /** diff --git a/src/Optimizely/DecisionService/FeatureDecision.php b/src/Optimizely/DecisionService/FeatureDecision.php index e40f2851..388a67e5 100644 --- a/src/Optimizely/DecisionService/FeatureDecision.php +++ b/src/Optimizely/DecisionService/FeatureDecision.php @@ -43,6 +43,13 @@ class FeatureDecision */ private $_source; + /** + * Array of log messages that represent decision making. + * + * @var array + */ + private $reasons; + /** * FeatureDecision constructor. * @@ -50,11 +57,12 @@ class FeatureDecision * @param $variation * @param $source */ - public function __construct($experiment, $variation, $source) + public function __construct($experiment, $variation, $source, array $reasons = []) { $this->_experiment = $experiment; $this->_variation = $variation; $this->_source = $source; + $this->reasons = $reasons; } public function getExperiment() @@ -71,4 +79,9 @@ public function getSource() { return $this->_source; } + + public function getReasons() + { + return $this->reasons; + } } diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 9780414d..294ec89d 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -350,9 +350,10 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp $featureFlag, $userId, $userAttributes, - $decideOptions, - $decideReasons + $decideOptions ); + + $decideReasons = $decision->getReasons(); $variation = $decision->getVariation(); if ($variation) { @@ -681,7 +682,7 @@ public function getVariation($experimentKey, $userId, $attributes = null) return null; } - $variation = $this->_decisionService->getVariation($config, $experiment, $userId, $attributes); + list($variation, $reasons) = $this->_decisionService->getVariation($config, $experiment, $userId, $attributes); $variationKey = ($variation === null) ? null : $variation->getKey(); if ($config->isFeatureExperiment($experiment->getId())) { @@ -761,7 +762,7 @@ public function getForcedVariation($experimentKey, $userId) return null; } - $forcedVariation = $this->_decisionService->getForcedVariation($config, $experimentKey, $userId); + list($forcedVariation, $reasons) = $this->_decisionService->getForcedVariation($config, $experimentKey, $userId); if (isset($forcedVariation)) { return $forcedVariation->getKey(); } else { diff --git a/src/Optimizely/Utils/Validator.php b/src/Optimizely/Utils/Validator.php index e76cc0bb..ff2f1c5b 100644 --- a/src/Optimizely/Utils/Validator.php +++ b/src/Optimizely/Utils/Validator.php @@ -1,6 +1,6 @@ log(Logger::INFO, sprintf( + $message = sprintf( $loggingClass::AUDIENCE_EVALUATION_RESULT_COMBINED, $loggingKey, 'TRUE' - )); - return true; + ); + $logger->log(Logger::INFO, $message); + $decideReasons [] = $message; + return [ true, $decideReasons ]; } if ($userAttributes === null) { @@ -209,13 +214,16 @@ public static function doesUserMeetAudienceConditions($config, $experiment, $use $evalResult = $conditionTreeEvaluator->evaluate($audienceConditions, $evaluateAudience); $evalResult = $evalResult || false; - $logger->log(Logger::INFO, sprintf( + $message = sprintf( $loggingClass::AUDIENCE_EVALUATION_RESULT_COMBINED, $loggingKey, strtoupper(var_export($evalResult, true)) - )); + ); + + $logger->log(Logger::INFO, $message); + $decideReasons[] = $message; - return $evalResult; + return [ $evalResult, $decideReasons]; } /** diff --git a/tests/BucketerTest.php b/tests/BucketerTest.php index 5642cd53..28c62b73 100644 --- a/tests/BucketerTest.php +++ b/tests/BucketerTest.php @@ -1,6 +1,6 @@ method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 1000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); - $this->assertNull( - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('test_experiment'), - $this->testBucketingIdControl, - $this->testUserId - ) + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('test_experiment'), + $this->testBucketingIdControl, + $this->testUserId ); + $this->assertNull($actualVariation); + // control $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 3000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('test_experiment'), + $this->testBucketingIdControl, + $this->testUserId + ); + $this->assertEquals( new Variation('7722370027', 'control'), - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('test_experiment'), - $this->testBucketingIdControl, - $this->testUserId - ) + $actualVariation ); // variation @@ -133,14 +135,16 @@ public function testBucketValidExperimentNotInGroup() ->method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 7000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('test_experiment'), + $this->testBucketingIdControl, + $this->testUserId + ); + $this->assertEquals( new Variation('7721010009', 'variation'), - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('test_experiment'), - $this->testBucketingIdControl, - $this->testUserId - ) + $actualVariation ); // No variation @@ -148,14 +152,14 @@ public function testBucketValidExperimentNotInGroup() ->method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 9000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); - $this->assertNull( - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('test_experiment'), - $this->testBucketingIdControl, - $this->testUserId - ) + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('test_experiment'), + $this->testBucketingIdControl, + $this->testUserId ); + + $this->assertNull($actualVariation); } public function testBucketValidExperimentInGroup() @@ -171,13 +175,24 @@ public function testBucketValidExperimentInGroup() $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 1000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); + + $inExpMessage = 'User "testUserId" is in experiment group_experiment_1 of group 7722400015.'; $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::INFO, 'User "testUserId" is in experiment group_experiment_1 of group 7722400015.'); + ->with(Logger::INFO, $inExpMessage); $this->loggerMock->expects($this->at(2)) ->method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 4000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('group_experiment_1'), + $this->testBucketingIdControl, + $this->testUserId + ); + + $this->assertContains($inExpMessage, $reasons); + $this->assertEquals( new Variation( '7722260071', @@ -190,12 +205,7 @@ public function testBucketValidExperimentInGroup() ] ] ), - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('group_experiment_1'), - $this->testBucketingIdControl, - $this->testUserId - ) + $actualVariation ); // variation 2 @@ -210,6 +220,14 @@ public function testBucketValidExperimentInGroup() ->method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 7000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); + + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('group_experiment_1'), + $this->testBucketingIdControl, + $this->testUserId + ); + $this->assertEquals( new Variation( '7722360022', @@ -222,12 +240,7 @@ public function testBucketValidExperimentInGroup() ] ] ), - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('group_experiment_1'), - $this->testBucketingIdControl, - $this->testUserId - ) + $actualVariation ); // User not in experiment @@ -235,37 +248,48 @@ public function testBucketValidExperimentInGroup() $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::DEBUG, sprintf('Assigned bucket 5000 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); + + $noExpGroupMessage = 'User "testUserId" is not in experiment group_experiment_1 of group 7722400015.'; $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::INFO, 'User "testUserId" is not in experiment group_experiment_1 of group 7722400015.'); - - $this->assertNull( - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('group_experiment_1'), - $this->testBucketingIdControl, - $this->testUserId - ) + ->with(Logger::INFO, $noExpGroupMessage); + + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('group_experiment_1'), + $this->testBucketingIdControl, + $this->testUserId ); + $this->assertContains($noExpGroupMessage, $reasons); + + $this->assertNull($actualVariation); + // User not in any experiment (previously allocated space) $bucketer->setBucketValues([400]); + $bucketingMessage = sprintf('Assigned bucket 400 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, sprintf('Assigned bucket 400 to user "%s" with bucketing ID "%s".', $this->testUserId, $this->testBucketingIdControl)); + ->with(Logger::DEBUG, $bucketingMessage); + + $noExpMessage = 'User "testUserId" is in no experiment.'; $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::INFO, 'User "testUserId" is in no experiment.'); + ->with(Logger::INFO, $noExpMessage); - $this->assertNull( - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('group_experiment_1'), - $this->testBucketingIdControl, - $this->testUserId - ) + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('group_experiment_1'), + $this->testBucketingIdControl, + $this->testUserId ); + $this->assertContains($bucketingMessage, $reasons); + $this->assertContains($noExpMessage, $reasons); + $this->assertCount(2, $reasons); + + $this->assertNull($actualVariation); + // User not in any experiment (never allocated space) $bucketer->setBucketValues([9000]); $this->loggerMock->expects($this->at(0)) @@ -274,14 +298,15 @@ public function testBucketValidExperimentInGroup() $this->loggerMock->expects($this->at(1)) ->method('log') ->with(Logger::INFO, 'User "testUserId" is in no experiment.'); - $this->assertNull( - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('group_experiment_1'), - $this->testBucketingIdControl, - $this->testUserId - ) + + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('group_experiment_1'), + $this->testBucketingIdControl, + $this->testUserId ); + + $this->assertNull($actualVariation); } public function testBucketInvalidExperiment() @@ -290,9 +315,13 @@ public function testBucketInvalidExperiment() $this->loggerMock->expects($this->never()) ->method('log'); - $this->assertNull( - $bucketer->bucket($this->config, new Experiment(), $this->testBucketingIdControl, $this->testUserId) - ); + $actualResponse = $bucketer->bucket($this->config, new Experiment(), $this->testBucketingIdControl, $this->testUserId); + + $this->assertNull($actualResponse[0]); + + // $this->assertNull( + // $bucketer->bucket($this->config, new Experiment(), $this->testBucketingIdControl, $this->testUserId) + // ); } public function testBucketWithBucketingId() @@ -302,14 +331,17 @@ public function testBucketWithBucketingId() // make sure that the bucketing ID is used for the variation // bucketing and not the user ID + + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $experiment, + $this->testBucketingIdControl, + $this->testUserIdBucketsToVariation + ); + $this->assertEquals( new Variation('7722370027', 'control'), - $bucketer->bucket( - $this->config, - $experiment, - $this->testBucketingIdControl, - $this->testUserIdBucketsToVariation - ) + $actualVariation ); } @@ -320,14 +352,14 @@ public function testBucketVariationInvalidExperimentsWithBucketingId() $bucketer = new TestBucketer($this->loggerMock); $bucketer->setBucketValues([1000, 3000, 7000, 9000]); - $this->assertNull( - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('invalid_experiment'), - $this->testBucketingIdVariation, - $this->testUserId - ) + $actualResponse = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('invalid_experiment'), + $this->testBucketingIdVariation, + $this->testUserId ); + + $this->assertNull($actualResponse[0]); } // make sure that the bucketing ID is used to bucket the user into a group @@ -336,25 +368,28 @@ public function testBucketVariationGroupedExperimentsWithBucketingId() { $bucketer = new Bucketer($this->loggerMock); - $this->assertEquals( - new Variation( - '7725250007', - 'group_exp_2_var_2', - true, - [ - [ - "id" => "155563", - "value" => "groupie_2_v1" - ] - ] - ), - $bucketer->bucket( - $this->config, - $this->config->getExperimentFromKey('group_experiment_2'), - $this->testBucketingIdGroupExp2Var2, - $this->testUserIdBucketsToNoGroup - ) + $expectedVariation = new Variation( + '7725250007', + 'group_exp_2_var_2', + true, + [ + [ + "id" => "155563", + "value" => "groupie_2_v1" + ] + ] + ); + + $actualResponse = $bucketer->bucket( + $this->config, + $this->config->getExperimentFromKey('group_experiment_2'), + $this->testBucketingIdGroupExp2Var2, + $this->testUserIdBucketsToNoGroup ); + + $actualVariation = $actualResponse[0]; + + $this->assertEquals($expectedVariation, $actualVariation); } public function testBucketWithRolloutRule() @@ -383,23 +418,25 @@ public function testBucketWithRolloutRule() ] ); + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $rollout_rule, + 'bucketingId', + 'userId' + ); + $this->assertEquals( $expectedVariation, - $bucketer->bucket( - $this->config, - $rollout_rule, - 'bucketingId', - 'userId' - ) + $actualVariation ); - $this->assertNull( - $bucketer->bucket( - $this->config, - $rollout_rule, - 'bucketingId', - 'userId' - ) + list($actualVariation, $reasons) = $bucketer->bucket( + $this->config, + $rollout_rule, + 'bucketingId', + 'userId' ); + + $this->assertNull($actualVariation); } } diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 2f5f54a9..6c4669f8 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -1,6 +1,6 @@ getMock(); } + public function compareFeatureDecisionsExceptReasons(FeatureDecision $expectedObj, FeatureDecision $actualObj) + { + $this->assertEquals($expectedObj->getVariation(), $actualObj->getVariation()); + $this->assertEquals($expectedObj->getExperiment(), $actualObj->getExperiment()); + $this->assertEquals($expectedObj->getSource(), $actualObj->getSource()); + } + public function testGetVariationReturnsNullWhenExperimentIsNotRunning() { $this->bucketerMock->expects($this->never()) @@ -97,7 +104,7 @@ public function testGetVariationReturnsNullWhenExperimentIsNotRunning() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $pausedExperiment, $this->testUserId); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $pausedExperiment, $this->testUserId); $this->assertNull($variation); } @@ -107,7 +114,7 @@ public function testGetVariationBucketsUserWhenExperimentIsRunning() $expectedVariation = new Variation('7722370027', 'control'); $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); @@ -115,7 +122,7 @@ public function testGetVariationBucketsUserWhenExperimentIsRunning() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, $this->testUserId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->testUserId, $this->testUserAttributes); $this->assertEquals( $expectedVariation, @@ -136,8 +143,13 @@ public function testGetBucketingIdWhenBucketingIdIsNotString() $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::WARNING, 'Bucketing ID attribute is not a string. Defaulted to user ID.'); + + $expectedReasons = ['Bucketing ID attribute is not a string. Defaulted to user ID.']; - $this->assertSame($this->testUserId, $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId)); + $this->assertEquals( + [$this->testUserId, $expectedReasons], + $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId) + ); } public function testGetBucketingIdWhenBucketingIdIsNull() @@ -153,7 +165,7 @@ public function testGetBucketingIdWhenBucketingIdIsNull() $this->loggerMock->expects($this->never()) ->method('log'); - $this->assertSame($this->testUserId, $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId)); + $this->assertEquals([$this->testUserId, []], $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId)); } public function testGetBucketingIdWhenBucketingIdIsString() @@ -169,7 +181,7 @@ public function testGetBucketingIdWhenBucketingIdIsString() $this->loggerMock->expects($this->never()) ->method('log'); - $this->assertSame('i_am_bucketing_id', $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId)); + $this->assertEquals(['i_am_bucketing_id', []], $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId)); } public function testGetBucketingIdWhenBucketingIdIsEmptyString() @@ -185,7 +197,7 @@ public function testGetBucketingIdWhenBucketingIdIsEmptyString() $this->loggerMock->expects($this->never()) ->method('log'); - $this->assertSame('', $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId)); + $this->assertEquals(['', []], $decisionService->getBucketingId($this->testUserId, $userAttributesWithBucketingId)); } public function testGetVariationReturnsWhitelistedVariation() @@ -207,7 +219,7 @@ public function testGetVariationReturnsWhitelistedVariation() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1'); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1'); $this->assertEquals( $expectedVariation, @@ -233,23 +245,31 @@ public function testGetVariationReturnsWhitelistedVariationForGroupedExperiment( $callIndex = 0; $this->bucketerMock->expects($this->never()) ->method('bucket'); + + $forcedVarMessage = 'User "user1" is not in the forced variation map.'; $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); + ->with(Logger::DEBUG, $forcedVarMessage); + + $whitelistedVarMessage = 'User "user1" is forced in variation "group_exp_1_var_1" of experiment "group_experiment_1".'; $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".'); + ->with(Logger::INFO, $whitelistedVarMessage); $bucketer = new \ReflectionProperty(DecisionService::class, '_bucketer'); $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1'); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1'); $this->assertEquals( $expectedVariation, $variation ); + + $this->assertNotContains($forcedVarMessage, $reasons); + $this->assertContains($whitelistedVarMessage, $reasons); + $this->assertCount(1, $reasons); } public function testGetVariationBucketsWhenForcedVariationsIsEmpty() @@ -257,7 +277,7 @@ public function testGetVariationBucketsWhenForcedVariationsIsEmpty() $expectedVariation = new Variation('7722370027', 'control'); $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); @@ -270,7 +290,7 @@ public function testGetVariationBucketsWhenForcedVariationsIsEmpty() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1', $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1', $this->testUserAttributes); $this->assertEquals( $expectedVariation, @@ -283,7 +303,7 @@ public function testGetVariationBucketsWhenWhitelistedVariationIsInvalid() $expectedVariation = new Variation('7722370027', 'control'); $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); @@ -301,7 +321,7 @@ public function testGetVariationBucketsWhenWhitelistedVariationIsInvalid() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1', $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'user1', $this->testUserAttributes); $this->assertEquals( $expectedVariation, @@ -314,7 +334,7 @@ public function testGetVariationBucketsUserWhenUserIsNotWhitelisted() $expectedVariation = new Variation('7722370027', 'control'); $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $runningExperiment = $this->config->getExperimentFromKey('test_experiment'); @@ -322,7 +342,7 @@ public function testGetVariationBucketsUserWhenUserIsNotWhitelisted() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, 'not_whitelisted_user', $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, 'not_whitelisted_user', $this->testUserAttributes); $this->assertEquals( $expectedVariation, @@ -341,7 +361,7 @@ public function testGetVariationReturnsNullIfUserDoesNotMeetAudienceConditions() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, $this->testUserId); // no matching attributes + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $this->testUserId); // no matching attributes $this->assertNull($variation); } @@ -379,7 +399,7 @@ public function testGetVariationReturnsStoredVariationIfAvailable() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, $userId); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId); $this->assertEquals($expectedVariation, $variation); } @@ -391,7 +411,7 @@ public function testGetVariationBucketsIfNoStoredVariation() $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $this->loggerMock->expects($this->any()) ->method('log') @@ -423,7 +443,7 @@ public function testGetVariationBucketsIfNoStoredVariation() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); $this->assertEquals($expectedVariation, $variation); // Verify Logs @@ -440,7 +460,7 @@ public function testGetVariationBucketsIfStoredVariationIsInvalid() $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $this->loggerMock->expects($this->any()) ->method('log') @@ -476,13 +496,17 @@ public function testGetVariationBucketsIfStoredVariationIsInvalid() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); $this->assertEquals($expectedVariation, $variation); // Verify Logs $this->assertContains([Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)], $this->collectedLogs); - $this->assertContains([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.'], $this->collectedLogs); + + $userProfileMsg = '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.'; + $this->assertContains([Logger::INFO, $userProfileMsg], $this->collectedLogs); $this->assertContains([Logger::INFO, 'Saved variation "control" of experiment "test_experiment" for user "testUserId".'], $this->collectedLogs); + + $this->assertContains($userProfileMsg, $reasons); } public function testGetVariationBucketsIfUserProfileServiceLookupThrows() @@ -493,7 +517,7 @@ public function testGetVariationBucketsIfUserProfileServiceLookupThrows() $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $this->loggerMock->expects($this->any()) ->method('log') @@ -529,13 +553,17 @@ public function testGetVariationBucketsIfUserProfileServiceLookupThrows() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); $this->assertEquals($expectedVariation, $variation); // Verify Logs $this->assertContains([Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)], $this->collectedLogs); - $this->assertContains([Logger::ERROR, 'The User Profile Service lookup method failed: I am error.'], $this->collectedLogs); + + $lookupFailedMsg = 'The User Profile Service lookup method failed: I am error.'; + $this->assertContains([Logger::ERROR, $lookupFailedMsg], $this->collectedLogs); $this->assertContains([Logger::INFO, 'Saved variation "control" of experiment "test_experiment" for user "testUserId".'], $this->collectedLogs); + + $this->assertContains($lookupFailedMsg, $reasons); } public function testGetVariationBucketsIfUserProfileServiceSaveThrows() @@ -546,7 +574,7 @@ public function testGetVariationBucketsIfUserProfileServiceSaveThrows() $this->bucketerMock->expects($this->once()) ->method('bucket') - ->willReturn($expectedVariation); + ->willReturn([$expectedVariation, []]); $this->loggerMock->expects($this->any()) ->method('log') @@ -573,7 +601,7 @@ public function testGetVariationBucketsIfUserProfileServiceSaveThrows() $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - $variation = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); + list($variation, $reasons) = $this->decisionService->getVariation($this->config, $runningExperiment, $userId, $this->testUserAttributes); $this->assertEquals($expectedVariation, $variation); // Verify Logs @@ -696,7 +724,8 @@ public function testGetVariationForFeatureExperimentGivenNullExperimentIds() ->method('log') ->with(Logger::DEBUG, "The feature flag 'empty_feature' is not used in any experiments."); - $this->assertNull($this->decisionService->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', [])); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', []); + $this->assertNull($actualDecision->getVariation()); } // should return nil and log a message when the experiment is not in the datafile @@ -718,19 +747,19 @@ public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile "The user 'user1' is not bucketed into any of the experiments using the feature 'boolean_feature'." ); - $this->assertNull($this->decisionService->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', [])); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', []); + $this->assertNull($actualDecision->getVariation()); } // 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)); + ->will($this->returnValue([ null, []])); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -739,7 +768,9 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNot "The user 'user1' is not bucketed into any of the experiments using the feature 'multi_variate_feature'." ); $featureFlag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); - $this->assertNull($this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', [])); + + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', []); + $this->assertNull($actualDecision->getVariation()); } // should return the variation when the user is bucketed into a variation for the experiment on the feature flag @@ -750,7 +781,7 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsB $variation = $this->config->getVariationFromId('test_experiment_multivariate', '122231'); $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') - ->will($this->returnValue($variation)); + ->will($this->returnValue([$variation, []])); $featureFlag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); $expected_decision = new FeatureDecision($experiment, $variation, FeatureDecision::DECISION_SOURCE_FEATURE_TEST); @@ -759,13 +790,11 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsB ->method('log') ->with( Logger::INFO, - "The user 'user1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'." + "The user 'user_1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'." ); - $this->assertEquals( - $expected_decision, - $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user1', []) - ); + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user_1', []); + $this->compareFeatureDecisionsExceptReasons($expected_decision, $actualDecision); } // should return the variation the user is bucketed into when the user is bucketed into one of the experiments @@ -775,7 +804,7 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBuck $variation = $mutex_exp->getVariations()[0]; $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') - ->will($this->returnValue($variation)); + ->will($this->returnValue([$variation, []])); $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); $variation = $mutex_exp->getVariations()[0]; @@ -788,10 +817,9 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBuck Logger::INFO, "The user 'user_1' is bucketed into experiment 'group_experiment_1' of feature 'mutex_group_feature'." ); - $this->assertEquals( - $expected_decision, - $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user_1', []) - ); + + $actualDecision = $this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user_1', []); + $this->compareFeatureDecisionsExceptReasons($expected_decision, $actualDecision); } // should return nil and log a message when the user is not bucketed into any of the mutex experiments @@ -801,7 +829,7 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuc $variation = $mutex_exp->getVariations()[0]; $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') - ->will($this->returnValue(null)); + ->will($this->returnValue([null, []])); $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); @@ -812,7 +840,14 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuc Logger::INFO, "The user 'user_1' is not bucketed into any of the experiments using the feature 'boolean_feature'." ); - $this->assertNull($this->decisionServiceMock->getVariationForFeatureExperiment($this->config, $featureFlag, 'user_1', [])); + + $actualFeatureDecision = $this->decisionServiceMock->getVariationForFeatureExperiment( + $this->config, + $featureFlag, + 'user_1', + [] + ); + $this->assertNull($actualFeatureDecision->getVariation()); } // should return the bucketed experiment and variation @@ -862,10 +897,9 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout() FeatureDecision::DECISION_SOURCE_ROLLOUT ); - $decisionServiceMock ->method('getVariationForFeatureExperiment') - ->will($this->returnValue(null)); + ->will($this->returnValue(new FeatureDecision(null, null, null))); $decisionServiceMock ->method('getVariationForFeatureRollout') @@ -878,10 +912,8 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout() "User 'user_1' is bucketed into rollout for feature flag 'string_single_variable_feature'." ); - $this->assertEquals( - $expected_decision, - $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, 'user_1', []) - ); + $actualFeatureDecision = $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, 'user_1', []); + $this->compareFeatureDecisionsExceptReasons($expected_decision, $actualFeatureDecision); } // should return null @@ -896,11 +928,11 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur $decisionServiceMock ->method('getVariationForFeatureExperiment') - ->will($this->returnValue(null)); + ->will($this->returnValue(new FeatureDecision(null, null, null))); $decisionServiceMock ->method('getVariationForFeatureRollout') - ->will($this->returnValue(null)); + ->will($this->returnValue(new FeatureDecision(null, null, null))); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -915,10 +947,10 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur null, FeatureDecision::DECISION_SOURCE_ROLLOUT ); - $this->assertEquals( - $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, 'user_1', []), - $expectedDecision - ); + + $actualFeatureDecision = $decisionServiceMock->getVariationForFeature($this->config, $featureFlag, 'user_1', []); + + $this->compareFeatureDecisionsExceptReasons($expectedDecision, $actualFeatureDecision); } // should return null @@ -934,7 +966,13 @@ public function testGetVariationForFeatureRolloutWhenNoRolloutIsAssociatedToFeat "Feature flag 'boolean_feature' is not used in a rollout." ); - $this->assertNull($this->decisionServiceMock->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', [])); + $actualFeatureDecision = $this->decisionServiceMock->getVariationForFeatureRollout( + $this->config, + $featureFlag, + 'user_1', + [] + ); + $this->assertNull($actualFeatureDecision->getVariation()); } // should return null @@ -952,7 +990,13 @@ public function testGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() 'Rollout with ID "invalid_rollout_id" is not in the datafile.' ); - $this->assertNull($this->decisionServiceMock->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', [])); + $actualFeatureDecision = $this->decisionServiceMock->getVariationForFeatureRollout( + $this->config, + $featureFlag, + 'user_1', + [] + ); + $this->assertNull($actualFeatureDecision->getVariation()); } // should return null @@ -976,7 +1020,8 @@ public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperimen ->method('getRolloutFromId') ->will($this->returnValue($experiment_less_rollout)); - $this->assertNull($this->decisionService->getVariationForFeatureRollout($configMock, $featureFlag, 'user_1', [])); + $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout($configMock, $featureFlag, 'user_1', []); + $this->assertNull($actualFeatureDecision->getVariation()); } // ============== when the user qualifies for targeting rule (audience match) ====================== @@ -1004,9 +1049,9 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $this->bucketerMock ->method('bucket') - ->willReturn($expected_variation); + ->willReturn([$expected_variation, []]); - $this->assertEquals( + $this->compareFeatureDecisionsExceptReasons( $expected_decision, $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes) ); @@ -1038,11 +1083,11 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge // Make bucket return null when called for first targeting rule $this->bucketerMock->expects($this->at(0)) ->method('bucket') - ->willReturn(null); + ->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); + ->willReturn([$expected_variation, []]); $this->assertEquals( $expected_decision, @@ -1070,15 +1115,20 @@ public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheT // Make bucket return null when called for first targeting rule $this->bucketerMock->expects($this->at(0)) ->method('bucket') - ->willReturn(null); + ->willReturn([null, []]); // Make bucket return null when called second time for everyone else $this->bucketerMock->expects($this->at(1)) ->method('bucket') - ->willReturn(null); + ->willReturn([null, []]); - $this->assertNull( - $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes) + $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout( + $this->config, + $featureFlag, + 'user_1', + $user_attributes ); + + $this->assertNull($actualFeatureDecision->getVariation()); } // ============== END of tests - when the user qualifies for targeting rule (audience match) ====================== @@ -1113,7 +1163,7 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar // Expect bucket to be called exactly once for the everyone else/last rule. $this->bucketerMock->expects($this->exactly(1)) ->method('bucket') - ->willReturn($expected_variation); + ->willReturn([$expected_variation, []]); $this->loggerMock->expects($this->any()) ->method('log') @@ -1158,7 +1208,9 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar ->method('log') ->will($this->returnCallback($this->collectLogsForAssertion)); - $this->assertNull($this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes)); + $actualFeatureDecision = $this->decisionService->getVariationForFeatureRollout($this->config, $featureFlag, 'user_1', $user_attributes); + + $this->assertNull($actualFeatureDecision->getVariation()); // Verify Logs $this->assertContains([Logger::DEBUG, "User 'user_1' does not meet conditions for targeting rule 1."], $this->collectedLogs); @@ -1193,7 +1245,7 @@ public function testGetVariationForFeatureRolloutLogging() // Expect bucket to be called exactly once for the everyone else/last rule. $this->bucketerMock->expects($this->exactly(1)) ->method('bucket') - ->willReturn($expected_variation); + ->willReturn([$expected_variation, []]); $this->loggerMock->expects($this->any()) ->method('log') @@ -1241,36 +1293,36 @@ public function testSetGetForcedVariation() // invalid experiment key should return a null variation $this->assertFalse($this->decisionService->setForcedVariation($this->config, $invalidExperimentKey, $userId, $variationKey)); - $this->assertNull($this->decisionService->getForcedVariation($this->config, $invalidExperimentKey, $userId)); + $this->assertNull($this->decisionService->getForcedVariation($this->config, $invalidExperimentKey, $userId)[0]); // setting a null variation should return a null variation $this->assertTrue($this->decisionService->setForcedVariation($this->config, $experimentKey, $userId, null)); - $this->assertNull($this->decisionService->getForcedVariation($this->config, $experimentKey, $userId)); + $this->assertNull($this->decisionService->getForcedVariation($this->config, $experimentKey, $userId)[0]); // setting an invalid variation should return a null variation $this->assertFalse($this->decisionService->setForcedVariation($this->config, $experimentKey, $userId, $invalidVariationKey)); - $this->assertNull($this->decisionService->getForcedVariation($this->config, $experimentKey, $userId)); + $this->assertNull($this->decisionService->getForcedVariation($this->config, $experimentKey, $userId)[0]); // confirm the forced variation is returned after a set $this->assertTrue($this->decisionService->setForcedVariation($this->config, $experimentKey, $userId, $variationKey)); - $forcedVariation = $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId); + $forcedVariation = $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId)[0]; $this->assertEquals($variationKey, $forcedVariation->getKey()); // check multiple sets $this->assertTrue($this->decisionService->setForcedVariation($this->config, $experimentKey2, $userId, $variationKey2)); - $forcedVariation2 = $this->decisionService->getForcedVariation($this->config, $experimentKey2, $userId); + $forcedVariation2 = $this->decisionService->getForcedVariation($this->config, $experimentKey2, $userId)[0]; $this->assertEquals($variationKey2, $forcedVariation2->getKey()); // make sure the second set does not overwrite the first set - $forcedVariation = $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId); + $forcedVariation = $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId)[0]; $this->assertEquals($variationKey, $forcedVariation->getKey()); // make sure unsetting the second experiment-to-variation mapping does not unset the // first experiment-to-variation mapping $this->assertTrue($this->decisionService->setForcedVariation($this->config, $experimentKey2, $userId, null)); - $forcedVariation = $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId); + $forcedVariation = $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId)[0]; $this->assertEquals($variationKey, $forcedVariation->getKey()); // an invalid user ID should return a null variation - $this->assertNull($this->decisionService->getForcedVariation($this->config, $experimentKey, $invalidUserId)); + $this->assertNull($this->decisionService->getForcedVariation($this->config, $experimentKey, $invalidUserId)[0]); } // test that all the logs in setForcedVariation are getting called @@ -1339,9 +1391,11 @@ public function testGetForcedVariationLogs() $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::DEBUG, sprintf('No experiment "%s" mapped to user "%s" in the forced variation map.', $pausedExperimentKey, $userId)); + + $varMappedMsg = sprintf('Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map', $variationKey, $experimentKey, $userId); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map', $variationKey, $experimentKey, $userId)); + ->with(Logger::DEBUG, $varMappedMsg); $this->config = new DatafileProjectConfig(DATAFILE, $this->loggerMock, new NoOpErrorHandler()); @@ -1349,6 +1403,8 @@ public function testGetForcedVariationLogs() $this->decisionService->getForcedVariation($this->config, $experimentKey, $invalidUserId); $this->decisionService->getForcedVariation($this->config, $invalidExperimentKey, $userId); $this->decisionService->getForcedVariation($this->config, $pausedExperimentKey, $userId); - $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId); + list($var, $reasons) = $this->decisionService->getForcedVariation($this->config, $experimentKey, $userId); + + $this->assertContains($varMappedMsg, $reasons); } } diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 7ee90d68..1c696f26 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -1652,6 +1652,8 @@ public function testDecideOptionIncludeReasons() ->getMock(); $expectedReasons = [ + 'Audiences for experiment "test_experiment_double_feature" collectively evaluated to TRUE.', + 'Assigned bucket 4513 to user "test_user" with bucketing ID "test_user".', 'User "test_user" is in variation control of experiment test_experiment_double_feature.', "The user 'test_user' is bucketed into experiment 'test_experiment_double_feature' of feature 'double_single_variable_feature'." ]; @@ -1711,6 +1713,28 @@ public function testDecideOptionIncludeReasons() $this->assertEquals($expectedReasons, $optimizelyDecision->getReasons()); } + public function testDecideLogsWhenAudienceEvalFails() + { + $optimizely = new Optimizely($this->datafile); + $userContext = $optimizely->createUserContext('test_user'); + + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $expectedReasons = [ + 'Audiences for experiment "test_experiment_multivariate" collectively evaluated to FALSE.', + 'User "test_user" does not meet conditions to be in experiment "test_experiment_multivariate".', + "The user 'test_user' is not bucketed into any of the experiments using the feature 'multi_variate_feature'.", + "Feature flag 'multi_variate_feature' is not used in a rollout.", + "User 'test_user' is not bucketed into rollout for feature flag 'multi_variate_feature'." + ]; + + $optimizelyDecision = $optimizelyMock->decide($userContext, 'multi_variate_feature', ['INCLUDE_REASONS']); + $this->assertEquals($expectedReasons, $optimizelyDecision->getReasons()); + } + public function testDecideOptionIncludeReasonsWhenPassedInDefaultOptions() { $optimizely = new Optimizely($this->datafile); diff --git a/tests/UtilsTests/ValidatorLoggingTest.php b/tests/UtilsTests/ValidatorLoggingTest.php index 3549b505..71c55ed9 100644 --- a/tests/UtilsTests/ValidatorLoggingTest.php +++ b/tests/UtilsTests/ValidatorLoggingTest.php @@ -1,6 +1,6 @@ method('log') ->with(Logger::DEBUG, "Evaluating audiences for experiment \"test_experiment\": []."); + $evalCombinedAudienceMessage = "Audiences for experiment \"test_experiment\" collectively evaluated to TRUE."; $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::INFO, "Audiences for experiment \"test_experiment\" collectively evaluated to TRUE."); + ->with(Logger::INFO, $evalCombinedAudienceMessage); - $this->assertTrue(Validator::doesUserMeetAudienceConditions($this->config, $experiment, [], $this->loggerMock)); + list($evalResult, $reasons) = Validator::doesUserMeetAudienceConditions($this->config, $experiment, [], $this->loggerMock); + $this->assertTrue($evalResult); + $this->assertContains($evalCombinedAudienceMessage, $reasons); + $this->assertCount(1, $reasons); } public function testdoesUserMeetAudienceConditionsEvaluatesAudienceIds() @@ -94,7 +98,7 @@ public function testIsUserInExperimenEvaluatesAudienceConditions() ->method('log') ->will($this->returnCallback($this->collectLogsForAssertion)); - Validator::doesUserMeetAudienceConditions($this->typedConfig, $experiment, ["house" => "I am in Slytherin"], $this->loggerMock); + list($result, $reasons) = Validator::doesUserMeetAudienceConditions($this->typedConfig, $experiment, ["house" => "I am in Slytherin"], $this->loggerMock); $this->assertContains( [Logger::DEBUG, "Evaluating audiences for experiment \"audience_combinations_experiment\": [\"or\",[\"or\",\"3468206642\",\"3988293898\"]]."], @@ -116,6 +120,11 @@ public function testIsUserInExperimenEvaluatesAudienceConditions() [Logger::DEBUG, "Audience \"3988293898\" evaluated to TRUE."], $this->collectedLogs ); - $this->assertContains([Logger::INFO, "Audiences for experiment \"audience_combinations_experiment\" collectively evaluated to TRUE."], $this->collectedLogs); + + $evalCombinedAudienceMessage = "Audiences for experiment \"audience_combinations_experiment\" collectively evaluated to TRUE."; + + $this->assertContains([Logger::INFO, $evalCombinedAudienceMessage], $this->collectedLogs); + $this->assertContains($evalCombinedAudienceMessage, $reasons); + $this->assertCount(1, $reasons); } } diff --git a/tests/UtilsTests/ValidatorTest.php b/tests/UtilsTests/ValidatorTest.php index a71f46b0..5939242c 100644 --- a/tests/UtilsTests/ValidatorTest.php +++ b/tests/UtilsTests/ValidatorTest.php @@ -1,6 +1,6 @@ loggerMock - ) + )[0] ); $this->assertTrue( @@ -227,21 +227,21 @@ public function testDoesUserMeetAudienceConditionsAudienceUsedInExperimentNoAttr $experiment, [], $this->loggerMock - ) + )[0] ); } public function testDoesUserMeetAudienceConditionsAudienceMatch() { $config = new DatafileProjectConfig(DATAFILE, new NoOpLogger(), new NoOpErrorHandler()); - $this->assertTrue( - Validator::doesUserMeetAudienceConditions( - $config, - $config->getExperimentFromKey('test_experiment'), - ['device_type' => 'iPhone', 'location' => 'San Francisco'], - $this->loggerMock - ) + $result = Validator::doesUserMeetAudienceConditions( + $config, + $config->getExperimentFromKey('test_experiment'), + ['device_type' => 'iPhone', 'location' => 'San Francisco'], + $this->loggerMock ); + + $this->assertTrue($result[0]); } public function testDoesUserMeetAudienceConditionsAudienceNoMatch() @@ -253,7 +253,7 @@ public function testDoesUserMeetAudienceConditionsAudienceNoMatch() $config->getExperimentFromKey('test_experiment'), ['device_type' => 'Android', 'location' => 'San Francisco'], $this->loggerMock - ) + )[0] ); } @@ -272,7 +272,7 @@ public function testDoesUserMeetAudienceConditionsNoAudienceUsedInExperiment() $experiment, [], $this->loggerMock - ) + )[0] ); // Audience Ids exist but audience conditions is empty. @@ -284,7 +284,7 @@ public function testDoesUserMeetAudienceConditionsNoAudienceUsedInExperiment() $experiment, [], $this->loggerMock - ) + )[0] ); // Audience Ids is empty and audience conditions is null. @@ -296,7 +296,7 @@ public function testDoesUserMeetAudienceConditionsNoAudienceUsedInExperiment() $experiment, [], $this->loggerMock - ) + )[0] ); } @@ -317,7 +317,7 @@ public function testDoesUserMeetAudienceConditionsSomeAudienceUsedInExperiment() $experiment, ['device_type' => 'Android', 'location' => 'San Francisco'], $this->loggerMock - ) + )[0] ); // Audience Ids exist and audience conditions is null. @@ -331,7 +331,7 @@ public function testDoesUserMeetAudienceConditionsSomeAudienceUsedInExperiment() $experiment, ['device_type' => 'iPhone', 'location' => 'San Francisco'], $this->loggerMock - ) + )[0] ); } @@ -351,7 +351,7 @@ public function testDoesUserMeetAudienceConditionsWithAudienceConditionsSetToAud $experiment, ['device_type' => 'iPhone', 'location' => 'San Francisco'], $this->loggerMock - ) + )[0] ); } @@ -371,7 +371,7 @@ public function testDoesUserMeetAudienceConditionsWithUnknownAudienceId() $experiment, ['device_type' => 'iPhone', 'location' => 'San Francisco'], $this->loggerMock - ) + )[0] ); } @@ -404,7 +404,7 @@ public function testDoesUserMeetAudienceConditionsWithSimpleAudience() $experiment, ['device_type' => 'iPhone', 'location' => 'San Francisco'], $this->loggerMock - ) + )[0] ); } @@ -461,7 +461,7 @@ public function testDoesUserMeetAudienceConditionsWithComplexAudience() $experiment, ['should_do_it' => true, 'house' => 'foo'], $this->loggerMock - ) + )[0] ); } From 4562445e3438cb2860b3996f03ff6e48c958a08c Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Fri, 15 Jan 2021 16:24:53 +0500 Subject: [PATCH 21/21] fix: variation and rule keys --- src/Optimizely/Optimizely.php | 6 +++--- tests/OptimizelyTest.php | 36 +++++++++++++++++------------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 294ec89d..380f40ca 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -375,9 +375,9 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp $this->sendImpressionEvent( $config, $ruleKey, - $variationKey, + $variationKey === null ? '' : $variationKey, $flagKey, - $ruleKey, + $ruleKey === null ? '' : $ruleKey, $source, $featureEnabled, $userId, @@ -415,7 +415,7 @@ public function decide(OptimizelyUserContext $userContext, $key, array $decideOp 'flagKey'=> $flagKey, 'enabled'=> $featureEnabled, 'variables' => $allVariables, - 'variation' => $variationKey, + 'variationKey' => $variationKey, 'ruleKey' => $ruleKey, 'reasons' => $shouldIncludeReasons ? $decideReasons:[], 'decisionEventDispatched' => $decisionEventDispatched diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 1c696f26..c24f4107 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -430,7 +430,7 @@ public function testDecide() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -590,7 +590,7 @@ public function testDecidewhenUserIsBucketedIntoFeatureExperiment() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -681,7 +681,7 @@ public function testDecidewhenUserIsBucketedIntoRolloutAndSendFlagDecisionIsTrue 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -772,7 +772,7 @@ public function testDecidewhenUserIsBucketedIntoRolloutAndSendFlagDecisionIsFals 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => false @@ -852,7 +852,7 @@ public function testDecidewhenDecisionServiceReturnsNullAndSendFlagDecisionIsTru 'flagKey'=>'double_single_variable_feature', 'enabled'=> false, 'variables'=> ["double_variable" => 14.99], - 'variation' => null, + 'variationKey' => null, 'ruleKey' => null, 'reasons' => [], 'decisionEventDispatched' => true @@ -936,7 +936,7 @@ public function testwhenDecisionServiceReturnNullAndSendFlagDecisionIsFalse() 'flagKey'=>'double_single_variable_feature', 'enabled'=> false, 'variables'=> ["double_variable" => 14.99], - 'variation' => null, + 'variationKey' => null, 'ruleKey' => null, 'reasons' => [], 'decisionEventDispatched' => false @@ -1012,7 +1012,7 @@ public function testDecideOptionDisableDecisionEvent() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => false @@ -1078,7 +1078,7 @@ public function testDecideOptionDisableDecisionEventWhenPassedInDefaultOptions() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => false @@ -1140,7 +1140,7 @@ public function testDecideRespectsUserProfileServiceLookup() 'flagKey'=>'double_single_variable_feature', 'enabled'=> false, 'variables'=> ["double_variable" => 14.99], - 'variation' => 'variation', + 'variationKey' => 'variation', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -1214,7 +1214,7 @@ public function testDecideRespectsUserProfileServiceSave() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -1288,7 +1288,7 @@ public function testDecideOptionIgnoreUserProfileService() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -1364,7 +1364,7 @@ public function testDecideOptionIgnoreUserProfileServiceWhenPassedInDefaultOptio 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -1439,7 +1439,7 @@ public function testDecideOptionExcludeVariables() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> [], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -1527,7 +1527,7 @@ public function testDecideOptionExcludeVariablesWhenPassedInDefaultOptions() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> [], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => true @@ -1667,7 +1667,7 @@ public function testDecideOptionIncludeReasons() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> ["double_variable" => 42.42], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => $expectedReasons, 'decisionEventDispatched' => true @@ -1762,7 +1762,7 @@ public function testDecideOptionIncludeReasonsWhenPassedInDefaultOptions() 'flagKey'=>'empty_feature', 'enabled'=> false, 'variables'=> [], - 'variation' => null, + 'variationKey' => null, 'ruleKey' => null, 'reasons' => $expectedReasons, 'decisionEventDispatched' => false @@ -1840,7 +1840,7 @@ public function testDecideParamOptionsWorkTogetherWithDefaultOptions() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> [], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => false @@ -1951,7 +1951,7 @@ public function testDecidewithAllDecideOptionsSet() 'flagKey'=>'double_single_variable_feature', 'enabled'=> true, 'variables'=> [], - 'variation' => 'control', + 'variationKey' => 'control', 'ruleKey' => 'test_experiment_double_feature', 'reasons' => [], 'decisionEventDispatched' => false