Skip to content

Commit

Permalink
feat(DecisionListener): Adds feature decision listener. (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
rashidsp authored and aliabbasrizvi committed Apr 2, 2019
1 parent 1f34b2a commit e4d949a
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 41 deletions.
12 changes: 7 additions & 5 deletions optimizely/decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
from .user_profile import UserProfile

Decision = namedtuple('Decision', 'experiment variation source')
DECISION_SOURCE_EXPERIMENT = 'experiment'
DECISION_SOURCE_ROLLOUT = 'rollout'
DECISION_SOURCE_EXPERIMENT = 'EXPERIMENT'
DECISION_SOURCE_ROLLOUT = 'ROLLOUT'


class DecisionService(object):
Expand Down Expand Up @@ -296,6 +296,7 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
variation.key,
experiment.key
))
return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)
else:
self.logger.error(enums.Errors.INVALID_GROUP_ID_ERROR.format('_get_variation_for_feature'))

Expand All @@ -312,10 +313,11 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
variation.key,
experiment.key
))
return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)

# Next check if user is part of a rollout
if not variation and feature.rolloutId:
if feature.rolloutId:
rollout = self.config.get_rollout_from_id(feature.rolloutId)
return self.get_variation_for_rollout(rollout, user_id, attributes)

return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)
else:
return Decision(None, None, DECISION_SOURCE_ROLLOUT)
5 changes: 3 additions & 2 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ class NotificationTypes(object):
DecisionInfoTypes type, str user_id, dict attributes (can be None), dict decision_info
"""
ACTIVATE = "ACTIVATE:experiment, user_id, attributes, variation, event"
DECISION = "DECISION:type, user_id, attributes, decision_info"
TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"
DECISION = "DECISON:type, user_id, attributes, decision_info"


class DecisionInfoTypes(object):
EXPERIMENT = "experiment"
EXPERIMENT = "experiment"
FEATURE = "feature"
35 changes: 29 additions & 6 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,21 +409,44 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
if not feature:
return False

experiment_key = None
feature_enabled = False
variation_key = None
decision = self.decision_service.get_variation_for_feature(feature, user_id, attributes)
is_source_experiment = decision.source == decision_service.DECISION_SOURCE_EXPERIMENT

if decision.variation:
if decision.variation.featureEnabled is True:
feature_enabled = True
# Send event if Decision came from an experiment.
if decision.source == decision_service.DECISION_SOURCE_EXPERIMENT:
if is_source_experiment:
experiment_key = decision.experiment.key
variation_key = decision.variation.key
self._send_impression_event(decision.experiment,
decision.variation,
user_id,
attributes)

if decision.variation.featureEnabled:
self.logger.info('Feature "%s" is enabled for user "%s".' % (feature_key, user_id))
return True
if feature_enabled:
self.logger.info('Feature "%s" is enabled for user "%s".' % (feature_key, user_id))
else:
self.logger.info('Feature "%s" is not enabled for user "%s".' % (feature_key, user_id))

self.notification_center.send_notifications(
enums.NotificationTypes.DECISION,
enums.DecisionInfoTypes.FEATURE,
user_id,
attributes or {},
{
'feature_key': feature_key,
'feature_enabled': feature_enabled,
'source': decision.source,
'source_experiment_key': experiment_key,
'source_variation_key': variation_key
}
)

self.logger.info('Feature "%s" is not enabled for user "%s".' % (feature_key, user_id))
return False
return feature_enabled

def get_enabled_features(self, user_id, attributes=None):
""" Returns the list of features that are enabled for the user.
Expand Down
14 changes: 6 additions & 8 deletions tests/test_decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_group(self):
with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group',
return_value=None) as mock_get_experiment_in_group, \
mock.patch('optimizely.decision_service.DecisionService.get_variation') as mock_decision:
self.assertEqual(decision_service.Decision(None, None, decision_service.DECISION_SOURCE_EXPERIMENT),
self.assertEqual(decision_service.Decision(None, None, decision_service.DECISION_SOURCE_ROLLOUT),
self.decision_service.get_variation_for_feature(feature, 'test_user'))

mock_get_experiment_in_group.assert_called_once_with(self.project_config.get_group('19228'), 'test_user')
Expand All @@ -647,12 +647,11 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self
""" Test that get_variation_for_feature returns None for user not in the associated experiment. """

feature = self.project_config.get_feature_from_key('test_feature_in_experiment')
expected_experiment = self.project_config.get_experiment_from_key('test_experiment')

with mock.patch('optimizely.decision_service.DecisionService.get_variation', return_value=None) as mock_decision:
self.assertEqual(decision_service.Decision(expected_experiment,
self.assertEqual(decision_service.Decision(None,
None,
decision_service.DECISION_SOURCE_EXPERIMENT),
decision_service.DECISION_SOURCE_ROLLOUT),
self.decision_service.get_variation_for_feature(feature, 'test_user'))

mock_decision.assert_called_once_with(
Expand All @@ -667,7 +666,7 @@ def test_get_variation_for_feature__returns_none_for_invalid_group_id(self):

with self.mock_decision_logger as mock_decision_logging:
self.assertEqual(
decision_service.Decision(None, None, decision_service.DECISION_SOURCE_EXPERIMENT),
decision_service.Decision(None, None, decision_service.DECISION_SOURCE_ROLLOUT),
self.decision_service.get_variation_for_feature(feature, 'test_user')
)
mock_decision_logging.error.assert_called_once_with(
Expand All @@ -679,13 +678,12 @@ def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_no
not targeting a feature, then None is returned. """

feature = self.project_config.get_feature_from_key('test_feature_in_group')
expected_experiment = self.project_config.get_experiment_from_key('group_exp_2')

with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group',
return_value=self.project_config.get_experiment_from_key('group_exp_2')) as mock_decision:
self.assertEqual(decision_service.Decision(expected_experiment,
self.assertEqual(decision_service.Decision(None,
None,
decision_service.DECISION_SOURCE_EXPERIMENT),
decision_service.DECISION_SOURCE_ROLLOUT),
self.decision_service.get_variation_for_feature(feature, 'test_user'))

mock_decision.assert_called_once_with(self.project_config.get_group('19228'), 'test_user')
Expand Down
Loading

0 comments on commit e4d949a

Please sign in to comment.