Skip to content

Commit 6339201

Browse files
committed
feat: decision listener for get feature variable *
1 parent f772ecc commit 6339201

File tree

7 files changed

+455
-42
lines changed

7 files changed

+455
-42
lines changed

optimizely/decision_service.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
from .user_profile import UserProfile
2323

2424
Decision = namedtuple('Decision', 'experiment variation source')
25-
DECISION_SOURCE_EXPERIMENT = 'experiment'
26-
DECISION_SOURCE_ROLLOUT = 'rollout'
25+
DECISION_SOURCE_EXPERIMENT = 'EXPERIMENT'
26+
DECISION_SOURCE_ROLLOUT = 'ROLLOUT'
2727

2828

2929
class DecisionService(object):
@@ -296,6 +296,7 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
296296
variation.key,
297297
experiment.key
298298
))
299+
return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)
299300
else:
300301
self.logger.error(enums.Errors.INVALID_GROUP_ID_ERROR.format('_get_variation_for_feature'))
301302

@@ -312,10 +313,11 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
312313
variation.key,
313314
experiment.key
314315
))
316+
return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)
315317

316318
# Next check if user is part of a rollout
317-
if not variation and feature.rolloutId:
319+
if feature.rolloutId:
318320
rollout = self.config.get_rollout_from_id(feature.rolloutId)
319321
return self.get_variation_for_rollout(rollout, user_id, attributes)
320-
321-
return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)
322+
else:
323+
return Decision(None, None, DECISION_SOURCE_ROLLOUT)

optimizely/helpers/enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,8 @@ class NotificationTypes(object):
9292
"""
9393
ACTIVATE = "ACTIVATE:experiment, user_id, attributes, variation, event"
9494
TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"
95+
DECISION = "DECISION:type, user_id, attributes, decision_info"
96+
97+
98+
class DecisionInfoTypes(object):
99+
FEATURE_VARIABLE = "feature_variable"

optimizely/optimizely.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,23 +208,48 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
208208
)
209209
return None
210210

211+
feature_enabled = False
212+
211213
decision = self.decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
212214
if decision.variation:
213215
variable_value = self.config.get_variable_value_for_variation(variable, decision.variation)
214-
216+
feature_enabled = decision.variation.featureEnabled
215217
else:
216218
variable_value = variable.defaultValue
217219
self.logger.info(
218220
'User "%s" is not in any variation or rollout rule. '
219221
'Returning default value for variable "%s" of feature flag "%s".' % (user_id, variable_key, feature_key)
220222
)
221223

224+
experiment_key = None
225+
variation_key = None
226+
227+
if decision.source == decision_service.DECISION_SOURCE_EXPERIMENT:
228+
experiment_key = decision.experiment.key
229+
variation_key = decision.variation.key
230+
222231
try:
223232
actual_value = self.config.get_typecast_value(variable_value, variable_type)
224233
except:
225234
self.logger.error('Unable to cast value. Returning None.')
226235
actual_value = None
227236

237+
self.notification_center.send_notifications(
238+
enums.NotificationTypes.DECISION,
239+
enums.DecisionInfoTypes.FEATURE_VARIABLE,
240+
user_id,
241+
attributes or {},
242+
{
243+
'feature_key': feature_key,
244+
'feature_enabled': feature_enabled,
245+
'variable_key': variable_key,
246+
'variable_value': actual_value,
247+
'variable_type': variable_type,
248+
'source': decision.source,
249+
'source_experiment_key': experiment_key,
250+
'source_variation_key': variation_key
251+
}
252+
)
228253
return actual_value
229254

230255
def activate(self, experiment_key, user_id, attributes=None):

tests/base.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2018, Optimizely
1+
# Copyright 2016-2019, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -307,7 +307,29 @@ def setUp(self, config_dict='config_dict'):
307307
'variations': [{
308308
'key': '211129',
309309
'id': '211129',
310-
'featureEnabled': True
310+
'featureEnabled': True,
311+
'variables': [{
312+
'id': '132', 'value': 'true'
313+
}, {
314+
'id': '133', 'value': 'Hello audience'
315+
}, {
316+
'id': '134', 'value': '39.99'
317+
}, {
318+
'id': '135', 'value': '399'
319+
}]
320+
}, {
321+
'key': '211229',
322+
'id': '211229',
323+
'featureEnabled': False,
324+
'variables': [{
325+
'id': '132', 'value': 'true'
326+
}, {
327+
'id': '133', 'value': 'environment'
328+
}, {
329+
'id': '134', 'value': '49.99'
330+
}, {
331+
'id': '135', 'value': '499'
332+
}]
311333
}]
312334
}, {
313335
'id': '211137',
@@ -379,7 +401,27 @@ def setUp(self, config_dict='config_dict'):
379401
'key': 'test_feature_in_rollout',
380402
'experimentIds': [],
381403
'rolloutId': '211111',
382-
'variables': [],
404+
'variables': [{
405+
'id': '132',
406+
'key': 'is_running',
407+
'defaultValue': 'false',
408+
'type': 'boolean'
409+
}, {
410+
'id': '133',
411+
'key': 'message',
412+
'defaultValue': 'Hello',
413+
'type': 'string'
414+
}, {
415+
'id': '134',
416+
'key': 'price',
417+
'defaultValue': '99.99',
418+
'type': 'double'
419+
}, {
420+
'id': '135',
421+
'key': 'count',
422+
'defaultValue': '999',
423+
'type': 'integer'
424+
}]
383425
}, {
384426
'id': '91113',
385427
'key': 'test_feature_in_group',

tests/test_config.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2018, Optimizely
1+
# Copyright 2016-2019, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -885,7 +885,19 @@ def test_get_feature_from_key__valid_feature_key(self):
885885
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
886886
project_config = opt_obj.config
887887

888-
expected_feature = entities.FeatureFlag('91112', 'test_feature_in_rollout', [], '211111', {})
888+
expected_feature = entities.FeatureFlag(
889+
'91112',
890+
'test_feature_in_rollout',
891+
[],
892+
'211111',
893+
{
894+
'is_running': entities.Variable('132', 'is_running', 'boolean', 'false'),
895+
'message': entities.Variable('133', 'message', 'string', 'Hello'),
896+
'price': entities.Variable('134', 'price', 'double', '99.99'),
897+
'count': entities.Variable('135', 'count', 'integer', '999')
898+
}
899+
)
900+
889901
self.assertEqual(expected_feature, project_config.get_feature_from_key('test_feature_in_rollout'))
890902

891903
def test_get_feature_from_key__invalid_feature_key(self):
@@ -916,7 +928,29 @@ def test_get_rollout_from_id__valid_rollout_id(self):
916928
'variations': [{
917929
'key': '211129',
918930
'id': '211129',
919-
'featureEnabled': True
931+
'featureEnabled': True,
932+
'variables': [{
933+
'id': '132', 'value': 'true'
934+
}, {
935+
'id': '133', 'value': 'Hello audience'
936+
}, {
937+
'id': '134', 'value': '39.99'
938+
}, {
939+
'id': '135', 'value': '399'
940+
}]
941+
}, {
942+
'key': '211229',
943+
'id': '211229',
944+
'featureEnabled': False,
945+
'variables': [{
946+
'id': '132', 'value': 'true'
947+
}, {
948+
'id': '133', 'value': 'environment'
949+
}, {
950+
'id': '134', 'value': '49.99'
951+
}, {
952+
'id': '135', 'value': '499'
953+
}]
920954
}]
921955
}, {
922956
'id': '211137',

tests/test_decision_service.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_group(self):
637637
with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group',
638638
return_value=None) as mock_get_experiment_in_group, \
639639
mock.patch('optimizely.decision_service.DecisionService.get_variation') as mock_decision:
640-
self.assertEqual(decision_service.Decision(None, None, decision_service.DECISION_SOURCE_EXPERIMENT),
640+
self.assertEqual(decision_service.Decision(None, None, decision_service.DECISION_SOURCE_ROLLOUT),
641641
self.decision_service.get_variation_for_feature(feature, 'test_user'))
642642

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

649649
feature = self.project_config.get_feature_from_key('test_feature_in_experiment')
650-
expected_experiment = self.project_config.get_experiment_from_key('test_experiment')
651650

652651
with mock.patch('optimizely.decision_service.DecisionService.get_variation', return_value=None) as mock_decision:
653-
self.assertEqual(decision_service.Decision(expected_experiment,
652+
self.assertEqual(decision_service.Decision(None,
654653
None,
655-
decision_service.DECISION_SOURCE_EXPERIMENT),
654+
decision_service.DECISION_SOURCE_ROLLOUT),
656655
self.decision_service.get_variation_for_feature(feature, 'test_user'))
657656

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

668667
with self.mock_decision_logger as mock_decision_logging:
669668
self.assertEqual(
670-
decision_service.Decision(None, None, decision_service.DECISION_SOURCE_EXPERIMENT),
669+
decision_service.Decision(None, None, decision_service.DECISION_SOURCE_ROLLOUT),
671670
self.decision_service.get_variation_for_feature(feature, 'test_user')
672671
)
673672
mock_decision_logging.error.assert_called_once_with(
@@ -679,13 +678,12 @@ def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_no
679678
not targeting a feature, then None is returned. """
680679

681680
feature = self.project_config.get_feature_from_key('test_feature_in_group')
682-
expected_experiment = self.project_config.get_experiment_from_key('group_exp_2')
683681

684682
with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group',
685683
return_value=self.project_config.get_experiment_from_key('group_exp_2')) as mock_decision:
686-
self.assertEqual(decision_service.Decision(expected_experiment,
684+
self.assertEqual(decision_service.Decision(None,
687685
None,
688-
decision_service.DECISION_SOURCE_EXPERIMENT),
686+
decision_service.DECISION_SOURCE_ROLLOUT),
689687
self.decision_service.get_variation_for_feature(feature, 'test_user'))
690688

691689
mock_decision.assert_called_once_with(self.project_config.get_group('19228'), 'test_user')

0 commit comments

Comments
 (0)