Skip to content

Commit e164ef3

Browse files
rashidspaliabbasrizvi
authored andcommitted
feat(DecisionListener): Adds feature variables decision listener. (#162)
1 parent 4a68390 commit e164ef3

File tree

2 files changed

+293
-4
lines changed

2 files changed

+293
-4
lines changed

src/Optimizely/Optimizely.php

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -627,19 +627,27 @@ public function getFeatureVariableValueForType(
627627
return null;
628628
}
629629

630+
$featureEnabled = false;
630631
$decision = $this->_decisionService->getVariationForFeature($featureFlag, $userId, $attributes);
631632
$variableValue = $variable->getDefaultValue();
632-
$variation = $decision->getVariation();
633633

634-
if ($variation === null) {
634+
if ($decision->getVariation() === null) {
635635
$this->_logger->log(
636636
Logger::INFO,
637637
"User '{$userId}'is not in any variation, ".
638638
"returning default value '{$variableValue}'."
639639
);
640640
} else {
641+
$experiment = $decision->getExperiment();
641642
$variation = $decision->getVariation();
642-
if ($variation->getFeatureEnabled()) {
643+
$featureEnabled = $variation->getFeatureEnabled();
644+
645+
if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_EXPERIMENT) {
646+
$experimentKey = $experiment->getKey();
647+
$variationKey = $variation->getKey();
648+
}
649+
650+
if ($featureEnabled) {
643651
$variableUsage = $variation->getVariableUsageById($variable->getId());
644652
if ($variableUsage) {
645653
$variableValue = $variableUsage->getValue();
@@ -664,6 +672,26 @@ public function getFeatureVariableValueForType(
664672
}
665673
}
666674

675+
$attributes = $attributes ?: [];
676+
$this->notificationCenter->sendNotifications(
677+
NotificationType::DECISION,
678+
array(
679+
DecisionInfoTypes::FEATURE_VARIABLE,
680+
$userId,
681+
$attributes,
682+
(object) array(
683+
'featureKey'=>$featureFlagKey,
684+
'featureEnabled'=> $featureEnabled,
685+
'variableKey'=> $variableKey,
686+
'variableType'=> $variableType,
687+
'variableValue'=> $variableValue,
688+
'source'=> $decision->getSource(),
689+
'sourceExperimentKey'=> isset($experimentKey) ? $experimentKey : null,
690+
'sourceVariationKey'=> isset($variationKey) ? $variationKey : null
691+
)
692+
)
693+
);
694+
667695
return $variableValue;
668696
}
669697

tests/OptimizelyTest.php

Lines changed: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2199,7 +2199,6 @@ public function testIsFeatureEnabledGivenGetVariationForFeatureReturnsRolloutDec
21992199
FeatureDecision::DECISION_SOURCE_ROLLOUT
22002200
);
22012201

2202-
// mock getVariationForFeature to return null
22032202
$decisionServiceMock->expects($this->exactly(1))
22042203
->method('getVariationForFeature')
22052204
->will($this->returnValue($expectedDecision));
@@ -3741,6 +3740,268 @@ public function testGetFeatureVariableReturnsDefaultValueForComplexAudienceMisma
37413740
$this->assertSame(10, $this->optimizelyTypedAudienceObject->getFeatureVariableInteger('feat2_with_var', 'z', 'user1', $userAttributes));
37423741
}
37433742

3743+
public function testGetFeatureVariableValueForTypeCallsDecisionListenerGivenUserInExperimentAndFeatureFlagIsEnabled()
3744+
{
3745+
// should return specific value
3746+
$decisionServiceMock = $this->getMockBuilder(DecisionService::class)
3747+
->setConstructorArgs(array($this->loggerMock, $this->projectConfig))
3748+
->setMethods(array('getVariationForFeature'))
3749+
->getMock();
3750+
$decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService');
3751+
$decisionService->setAccessible(true);
3752+
$decisionService->setValue($this->optimizelyObject, $decisionServiceMock);
3753+
$experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature');
3754+
$expectedVariation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control');
3755+
$expectedDecision = new FeatureDecision(
3756+
$experiment,
3757+
$expectedVariation,
3758+
FeatureDecision::DECISION_SOURCE_EXPERIMENT
3759+
);
3760+
3761+
$decisionServiceMock->expects($this->once())
3762+
->method('getVariationForFeature')
3763+
->will($this->returnValue($expectedDecision));
3764+
3765+
$this->assertTrue($expectedVariation->getFeatureEnabled());
3766+
$arrayParam = array(
3767+
DecisionInfoTypes::FEATURE_VARIABLE,
3768+
'user_id',
3769+
[],
3770+
(object) array(
3771+
'featureKey'=>'double_single_variable_feature',
3772+
'featureEnabled'=> true,
3773+
'variableKey'=> 'double_variable',
3774+
'variableType'=> 'double',
3775+
'variableValue'=> '42.42',
3776+
'source'=> 'EXPERIMENT',
3777+
'sourceExperimentKey'=> 'test_experiment_double_feature',
3778+
'sourceVariationKey'=> 'control'
3779+
)
3780+
);
3781+
$this->notificationCenterMock->expects($this->once())
3782+
->method('sendNotifications')
3783+
->with(
3784+
NotificationType::DECISION,
3785+
$arrayParam
3786+
);
3787+
3788+
$this->optimizelyObject->notificationCenter = $this->notificationCenterMock;
3789+
3790+
$this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double');
3791+
}
3792+
3793+
public function testGetFeatureVariableValueForTypeCallsDecisionListenerGivenUserInExperimentAndFeatureFlagIsNotEnabled()
3794+
{
3795+
$userAttributes = [
3796+
'device_type' => 'iPhone'
3797+
];
3798+
3799+
// should return specific value
3800+
$decisionServiceMock = $this->getMockBuilder(DecisionService::class)
3801+
->setConstructorArgs(array($this->loggerMock, $this->projectConfig))
3802+
->setMethods(array('getVariationForFeature'))
3803+
->getMock();
3804+
$decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService');
3805+
$decisionService->setAccessible(true);
3806+
$decisionService->setValue($this->optimizelyObject, $decisionServiceMock);
3807+
$experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature');
3808+
$expectedVariation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control');
3809+
$expectedVariation->setFeatureEnabled(false);
3810+
$expectedDecision = new FeatureDecision(
3811+
$experiment,
3812+
$expectedVariation,
3813+
FeatureDecision::DECISION_SOURCE_EXPERIMENT
3814+
);
3815+
3816+
$decisionServiceMock->expects($this->once())
3817+
->method('getVariationForFeature')
3818+
->will($this->returnValue($expectedDecision));
3819+
3820+
// assert variation's 'featureEnabled' is set to false
3821+
$this->assertFalse($expectedVariation->getFeatureEnabled());
3822+
3823+
$arrayParam = array(
3824+
DecisionInfoTypes::FEATURE_VARIABLE,
3825+
'user_id',
3826+
$userAttributes,
3827+
(object) array(
3828+
'featureKey'=>'double_single_variable_feature',
3829+
'featureEnabled'=> false,
3830+
'variableKey'=> 'double_variable',
3831+
'variableType'=> 'double',
3832+
'variableValue'=> '14.99',
3833+
'source'=> 'EXPERIMENT',
3834+
'sourceExperimentKey'=> 'test_experiment_double_feature',
3835+
'sourceVariationKey'=> 'control'
3836+
)
3837+
);
3838+
$this->notificationCenterMock->expects($this->once())
3839+
->method('sendNotifications')
3840+
->with(
3841+
NotificationType::DECISION,
3842+
$arrayParam
3843+
);
3844+
3845+
$this->optimizelyObject->notificationCenter = $this->notificationCenterMock;
3846+
3847+
$this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', $userAttributes, 'double');
3848+
}
3849+
3850+
public function testGetFeatureVariableValueCallsDecisionListenerGivenUserInRolloutAndFeatureFlagIsEnabled()
3851+
{
3852+
// should return specific value
3853+
$decisionServiceMock = $this->getMockBuilder(DecisionService::class)
3854+
->setConstructorArgs(array($this->loggerMock, $this->projectConfig))
3855+
->setMethods(array('getVariationForFeature'))
3856+
->getMock();
3857+
$decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService');
3858+
$decisionService->setAccessible(true);
3859+
$decisionService->setValue($this->optimizelyObject, $decisionServiceMock);
3860+
$featureFlag = $this->projectConfig->getFeatureFlagFromKey('boolean_single_variable_feature');
3861+
$rolloutId = $featureFlag->getRolloutId();
3862+
$rollout = $this->projectConfig->getRolloutFromId($rolloutId);
3863+
$experiment = $rollout->getExperiments()[0];
3864+
$expectedVariation = $experiment->getVariations()[0];
3865+
$expectedDecision = new FeatureDecision(
3866+
$experiment,
3867+
$expectedVariation,
3868+
FeatureDecision::DECISION_SOURCE_ROLLOUT
3869+
);
3870+
$decisionServiceMock->expects($this->once())
3871+
->method('getVariationForFeature')
3872+
->will($this->returnValue($expectedDecision));
3873+
3874+
$this->assertTrue($expectedVariation->getFeatureEnabled());
3875+
3876+
$arrayParam = array(
3877+
DecisionInfoTypes::FEATURE_VARIABLE,
3878+
'user_id',
3879+
[],
3880+
(object) array(
3881+
'featureKey'=>'boolean_single_variable_feature',
3882+
'featureEnabled'=> true,
3883+
'variableKey'=> 'boolean_variable',
3884+
'variableType'=> 'boolean',
3885+
'variableValue'=> 'true',
3886+
'source'=> 'ROLLOUT',
3887+
'sourceExperimentKey'=> null,
3888+
'sourceVariationKey'=> null
3889+
)
3890+
);
3891+
$this->notificationCenterMock->expects($this->once())
3892+
->method('sendNotifications')
3893+
->with(
3894+
NotificationType::DECISION,
3895+
$arrayParam
3896+
);
3897+
3898+
$this->optimizelyObject->notificationCenter = $this->notificationCenterMock;
3899+
3900+
$this->optimizelyObject->getFeatureVariableBoolean('boolean_single_variable_feature', 'boolean_variable', 'user_id', []);
3901+
}
3902+
3903+
public function testGetFeatureVariableValueCallsDecisionListenerGivenUserInRolloutAndFeatureFlagIsNotEnabled()
3904+
{
3905+
// should return specific value
3906+
$decisionServiceMock = $this->getMockBuilder(DecisionService::class)
3907+
->setConstructorArgs(array($this->loggerMock, $this->projectConfig))
3908+
->setMethods(array('getVariationForFeature'))
3909+
->getMock();
3910+
$decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService');
3911+
$decisionService->setAccessible(true);
3912+
$decisionService->setValue($this->optimizelyObject, $decisionServiceMock);
3913+
$featureFlag = $this->projectConfig->getFeatureFlagFromKey('boolean_single_variable_feature');
3914+
$rolloutId = $featureFlag->getRolloutId();
3915+
$rollout = $this->projectConfig->getRolloutFromId($rolloutId);
3916+
$experiment = $rollout->getExperiments()[0];
3917+
$expectedVariation = $experiment->getVariations()[0];
3918+
$expectedVariation->setFeatureEnabled(false);
3919+
$expectedDecision = new FeatureDecision(
3920+
$experiment,
3921+
$expectedVariation,
3922+
FeatureDecision::DECISION_SOURCE_ROLLOUT
3923+
);
3924+
$decisionServiceMock->expects($this->once())
3925+
->method('getVariationForFeature')
3926+
->will($this->returnValue($expectedDecision));
3927+
3928+
$this->assertFalse($expectedVariation->getFeatureEnabled());
3929+
3930+
$arrayParam = array(
3931+
DecisionInfoTypes::FEATURE_VARIABLE,
3932+
'user_id',
3933+
[],
3934+
(object) array(
3935+
'featureKey'=>'boolean_single_variable_feature',
3936+
'featureEnabled'=> false,
3937+
'variableKey'=> 'boolean_variable',
3938+
'variableType'=> 'boolean',
3939+
'variableValue'=> 'true',
3940+
'source'=> 'ROLLOUT',
3941+
'sourceExperimentKey'=> null,
3942+
'sourceVariationKey'=> null
3943+
)
3944+
);
3945+
$this->notificationCenterMock->expects($this->once())
3946+
->method('sendNotifications')
3947+
->with(
3948+
NotificationType::DECISION,
3949+
$arrayParam
3950+
);
3951+
3952+
$this->optimizelyObject->notificationCenter = $this->notificationCenterMock;
3953+
3954+
$this->optimizelyObject->getFeatureVariableBoolean('boolean_single_variable_feature', 'boolean_variable', 'user_id', []);
3955+
}
3956+
3957+
public function testGetFeatureVariableValueForTypeCallsDecisionListenerWhenUserNeitherInExperimentNorInRollout()
3958+
{
3959+
// should return default value
3960+
$decisionServiceMock = $this->getMockBuilder(DecisionService::class)
3961+
->setConstructorArgs(array($this->loggerMock, $this->projectConfig))
3962+
->setMethods(array('getVariationForFeature'))
3963+
->getMock();
3964+
$decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService');
3965+
$decisionService->setAccessible(true);
3966+
$decisionService->setValue($this->optimizelyObject, $decisionServiceMock);
3967+
3968+
// mock getVariationForFeature to return rolloutDecision
3969+
$expectedDecision = new FeatureDecision(
3970+
null,
3971+
null,
3972+
FeatureDecision::DECISION_SOURCE_ROLLOUT
3973+
);
3974+
$decisionServiceMock->expects($this->exactly(1))
3975+
->method('getVariationForFeature')
3976+
->will($this->returnValue($expectedDecision));
3977+
3978+
$arrayParam = array(
3979+
DecisionInfoTypes::FEATURE_VARIABLE,
3980+
'user_id',
3981+
[],
3982+
(object) array(
3983+
'featureKey'=>'double_single_variable_feature',
3984+
'featureEnabled'=> false,
3985+
'variableKey'=> 'double_variable',
3986+
'variableType'=> 'double',
3987+
'variableValue'=> '14.99',
3988+
'source'=> 'ROLLOUT',
3989+
'sourceExperimentKey'=> null,
3990+
'sourceVariationKey'=> null
3991+
)
3992+
);
3993+
$this->notificationCenterMock->expects($this->once())
3994+
->method('sendNotifications')
3995+
->with(
3996+
NotificationType::DECISION,
3997+
$arrayParam
3998+
);
3999+
4000+
$this->optimizelyObject->notificationCenter = $this->notificationCenterMock;
4001+
4002+
$this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double');
4003+
}
4004+
37444005
public function testSendImpressionEventWithNoAttributes()
37454006
{
37464007
$optlyObject = new OptimizelyTester($this->datafile, new ValidEventDispatcher(), $this->loggerMock);

0 commit comments

Comments
 (0)