Skip to content

Commit

Permalink
Merge 4de0ea9 into 92490ee
Browse files Browse the repository at this point in the history
  • Loading branch information
rashidsp authored Apr 1, 2019
2 parents 92490ee + 4de0ea9 commit cf1e2f0
Show file tree
Hide file tree
Showing 7 changed files with 452 additions and 43 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)
3 changes: 2 additions & 1 deletion 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"
FEATURE_VARIABLE = "feature_variable"
27 changes: 26 additions & 1 deletion optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,23 +208,48 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
)
return None

feature_enabled = False

decision = self.decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
if decision.variation:
variable_value = self.config.get_variable_value_for_variation(variable, decision.variation)

feature_enabled = decision.variation.featureEnabled
else:
variable_value = variable.defaultValue
self.logger.info(
'User "%s" is not in any variation or rollout rule. '
'Returning default value for variable "%s" of feature flag "%s".' % (user_id, variable_key, feature_key)
)

experiment_key = None
variation_key = None

if decision.source == decision_service.DECISION_SOURCE_EXPERIMENT:
experiment_key = decision.experiment.key
variation_key = decision.variation.key

try:
actual_value = self.config.get_typecast_value(variable_value, variable_type)
except:
self.logger.error('Unable to cast value. Returning None.')
actual_value = None

self.notification_center.send_notifications(
enums.NotificationTypes.DECISION,
enums.DecisionInfoTypes.FEATURE_VARIABLE,
user_id,
attributes or {},
{
'feature_key': feature_key,
'feature_enabled': feature_enabled,
'variable_key': variable_key,
'variable_value': actual_value,
'variable_type': variable_type,
'source': decision.source,
'source_experiment_key': experiment_key,
'source_variation_key': variation_key
}
)
return actual_value

def activate(self, experiment_key, user_id, attributes=None):
Expand Down
48 changes: 45 additions & 3 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2018, Optimizely
# Copyright 2016-2019, Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand Down Expand Up @@ -307,7 +307,29 @@ def setUp(self, config_dict='config_dict'):
'variations': [{
'key': '211129',
'id': '211129',
'featureEnabled': True
'featureEnabled': True,
'variables': [{
'id': '132', 'value': 'true'
}, {
'id': '133', 'value': 'Hello audience'
}, {
'id': '134', 'value': '39.99'
}, {
'id': '135', 'value': '399'
}]
}, {
'key': '211229',
'id': '211229',
'featureEnabled': False,
'variables': [{
'id': '132', 'value': 'true'
}, {
'id': '133', 'value': 'environment'
}, {
'id': '134', 'value': '49.99'
}, {
'id': '135', 'value': '499'
}]
}]
}, {
'id': '211137',
Expand Down Expand Up @@ -379,7 +401,27 @@ def setUp(self, config_dict='config_dict'):
'key': 'test_feature_in_rollout',
'experimentIds': [],
'rolloutId': '211111',
'variables': [],
'variables': [{
'id': '132',
'key': 'is_running',
'defaultValue': 'false',
'type': 'boolean'
}, {
'id': '133',
'key': 'message',
'defaultValue': 'Hello',
'type': 'string'
}, {
'id': '134',
'key': 'price',
'defaultValue': '99.99',
'type': 'double'
}, {
'id': '135',
'key': 'count',
'defaultValue': '999',
'type': 'integer'
}]
}, {
'id': '91113',
'key': 'test_feature_in_group',
Expand Down
40 changes: 37 additions & 3 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2018, Optimizely
# Copyright 2016-2019, Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand Down Expand Up @@ -885,7 +885,19 @@ def test_get_feature_from_key__valid_feature_key(self):
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
project_config = opt_obj.config

expected_feature = entities.FeatureFlag('91112', 'test_feature_in_rollout', [], '211111', {})
expected_feature = entities.FeatureFlag(
'91112',
'test_feature_in_rollout',
[],
'211111',
{
'is_running': entities.Variable('132', 'is_running', 'boolean', 'false'),
'message': entities.Variable('133', 'message', 'string', 'Hello'),
'price': entities.Variable('134', 'price', 'double', '99.99'),
'count': entities.Variable('135', 'count', 'integer', '999')
}
)

self.assertEqual(expected_feature, project_config.get_feature_from_key('test_feature_in_rollout'))

def test_get_feature_from_key__invalid_feature_key(self):
Expand Down Expand Up @@ -916,7 +928,29 @@ def test_get_rollout_from_id__valid_rollout_id(self):
'variations': [{
'key': '211129',
'id': '211129',
'featureEnabled': True
'featureEnabled': True,
'variables': [{
'id': '132', 'value': 'true'
}, {
'id': '133', 'value': 'Hello audience'
}, {
'id': '134', 'value': '39.99'
}, {
'id': '135', 'value': '399'
}]
}, {
'key': '211229',
'id': '211229',
'featureEnabled': False,
'variables': [{
'id': '132', 'value': 'true'
}, {
'id': '133', 'value': 'environment'
}, {
'id': '134', 'value': '49.99'
}, {
'id': '135', 'value': '499'
}]
}]
}, {
'id': '211137',
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 cf1e2f0

Please sign in to comment.