Skip to content

Commit ec028d9

Browse files
rashidspaliabbasrizvi
authored andcommitted
feat(decision-listener): Incorporated new decision notification listener changes. (#174)
1 parent 1ed413d commit ec028d9

File tree

8 files changed

+279
-170
lines changed

8 files changed

+279
-170
lines changed

optimizely/decision_service.py

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

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

2826

2927
class DecisionService(object):
@@ -215,7 +213,7 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
215213
variation.key,
216214
experiment.key
217215
))
218-
return Decision(experiment, variation, DECISION_SOURCE_ROLLOUT)
216+
return Decision(experiment, variation, enums.DecisionSources.ROLLOUT)
219217
else:
220218
# Evaluate no further rules
221219
self.logger.debug('User "%s" is not in the traffic group for the targeting else. '
@@ -233,9 +231,9 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
233231
variation = self.bucketer.bucket(everyone_else_experiment, user_id, bucketing_id)
234232
if variation:
235233
self.logger.debug('User "%s" meets conditions for targeting rule "Everyone Else".' % user_id)
236-
return Decision(everyone_else_experiment, variation, DECISION_SOURCE_ROLLOUT)
234+
return Decision(everyone_else_experiment, variation, enums.DecisionSources.ROLLOUT)
237235

238-
return Decision(None, None, DECISION_SOURCE_ROLLOUT)
236+
return Decision(None, None, enums.DecisionSources.ROLLOUT)
239237

240238
def get_experiment_in_group(self, group, bucketing_id):
241239
""" Determine which experiment in the group the user is bucketed into.
@@ -296,7 +294,7 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
296294
variation.key,
297295
experiment.key
298296
))
299-
return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)
297+
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST)
300298
else:
301299
self.logger.error(enums.Errors.INVALID_GROUP_ID_ERROR.format('_get_variation_for_feature'))
302300

@@ -313,11 +311,11 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
313311
variation.key,
314312
experiment.key
315313
))
316-
return Decision(experiment, variation, DECISION_SOURCE_EXPERIMENT)
314+
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST)
317315

318316
# Next check if user is part of a rollout
319317
if feature.rolloutId:
320318
rollout = self.config.get_rollout_from_id(feature.rolloutId)
321319
return self.get_variation_for_rollout(rollout, user_id, attributes)
322320
else:
323-
return Decision(None, None, DECISION_SOURCE_ROLLOUT)
321+
return Decision(None, None, enums.DecisionSources.ROLLOUT)

optimizely/helpers/enums.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ class DatafileVersions(object):
4848
V4 = '4'
4949

5050

51+
class DecisionNotificationTypes(object):
52+
AB_TEST = 'ab-test'
53+
FEATURE = 'feature'
54+
FEATURE_TEST = 'feature-test'
55+
FEATURE_VARIABLE = 'feature-variable'
56+
57+
58+
class DecisionSources(object):
59+
FEATURE_TEST = 'feature-test'
60+
ROLLOUT = 'rollout'
61+
62+
5163
class Errors(object):
5264
INVALID_ATTRIBUTE_ERROR = 'Provided attribute is not in datafile.'
5365
INVALID_ATTRIBUTE_FORMAT = 'Attributes provided are in an invalid format.'
@@ -90,14 +102,8 @@ class NotificationTypes(object):
90102
TRACK notification listener has the following parameters:
91103
str event_key, str user_id, dict attributes (can be None), event_tags (can be None), Event event
92104
DECISION notification listener has the following parameters:
93-
DecisionInfoTypes type, str user_id, dict attributes (can be None), dict decision_info
105+
DecisionNotificationTypes type, str user_id, dict attributes, dict decision_info
94106
"""
95-
ACTIVATE = "ACTIVATE:experiment, user_id, attributes, variation, event"
96-
DECISION = "DECISION:type, user_id, attributes, decision_info"
97-
TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"
98-
99-
100-
class DecisionInfoTypes(object):
101-
EXPERIMENT = "experiment"
102-
FEATURE = "feature"
103-
FEATURE_VARIABLE = "feature_variable"
107+
ACTIVATE = 'ACTIVATE:experiment, user_id, attributes, variation, event'
108+
DECISION = 'DECISION:type, user_id, attributes, decision_info'
109+
TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event'

optimizely/optimizely.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
209209
return None
210210

211211
feature_enabled = False
212+
source_info = {}
212213
variable_value = variable.defaultValue
213214
decision = self.decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
214215
if decision.variation:
@@ -232,12 +233,11 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
232233
'Returning default value for variable "%s" of feature flag "%s".' % (user_id, variable_key, feature_key)
233234
)
234235

235-
experiment_key = None
236-
variation_key = None
237-
238-
if decision.source == decision_service.DECISION_SOURCE_EXPERIMENT:
239-
experiment_key = decision.experiment.key
240-
variation_key = decision.variation.key
236+
if decision.source == enums.DecisionSources.FEATURE_TEST:
237+
source_info = {
238+
'experiment_key': decision.experiment.key,
239+
'variation_key': decision.variation.key
240+
}
241241

242242
try:
243243
actual_value = self.config.get_typecast_value(variable_value, variable_type)
@@ -247,18 +247,17 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
247247

248248
self.notification_center.send_notifications(
249249
enums.NotificationTypes.DECISION,
250-
enums.DecisionInfoTypes.FEATURE_VARIABLE,
250+
enums.DecisionNotificationTypes.FEATURE_VARIABLE,
251251
user_id,
252252
attributes or {},
253253
{
254254
'feature_key': feature_key,
255255
'feature_enabled': feature_enabled,
256+
'source': decision.source,
256257
'variable_key': variable_key,
257258
'variable_value': actual_value,
258259
'variable_type': variable_type,
259-
'source': decision.source,
260-
'source_experiment_key': experiment_key,
261-
'source_variation_key': variation_key
260+
'source_info': source_info
262261
}
263262
)
264263
return actual_value
@@ -388,9 +387,14 @@ def get_variation(self, experiment_key, user_id, attributes=None):
388387
if variation:
389388
variation_key = variation.key
390389

390+
if self.config.is_feature_experiment(experiment.id):
391+
decision_notification_type = enums.DecisionNotificationTypes.FEATURE_TEST
392+
else:
393+
decision_notification_type = enums.DecisionNotificationTypes.AB_TEST
394+
391395
self.notification_center.send_notifications(
392396
enums.NotificationTypes.DECISION,
393-
enums.DecisionInfoTypes.EXPERIMENT,
397+
decision_notification_type,
394398
user_id,
395399
attributes or {},
396400
{
@@ -432,19 +436,20 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
432436
if not feature:
433437
return False
434438

435-
experiment_key = None
436439
feature_enabled = False
437-
variation_key = None
440+
source_info = {}
438441
decision = self.decision_service.get_variation_for_feature(feature, user_id, attributes)
439-
is_source_experiment = decision.source == decision_service.DECISION_SOURCE_EXPERIMENT
442+
is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST
440443

441444
if decision.variation:
442445
if decision.variation.featureEnabled is True:
443446
feature_enabled = True
444447
# Send event if Decision came from an experiment.
445448
if is_source_experiment:
446-
experiment_key = decision.experiment.key
447-
variation_key = decision.variation.key
449+
source_info = {
450+
'experiment_key': decision.experiment.key,
451+
'variation_key': decision.variation.key
452+
}
448453
self._send_impression_event(decision.experiment,
449454
decision.variation,
450455
user_id,
@@ -457,15 +462,14 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
457462

458463
self.notification_center.send_notifications(
459464
enums.NotificationTypes.DECISION,
460-
enums.DecisionInfoTypes.FEATURE,
465+
enums.DecisionNotificationTypes.FEATURE,
461466
user_id,
462467
attributes or {},
463468
{
464469
'feature_key': feature_key,
465470
'feature_enabled': feature_enabled,
466471
'source': decision.source,
467-
'source_experiment_key': experiment_key,
468-
'source_variation_key': variation_key
472+
'source_info': source_info
469473
}
470474
)
471475

optimizely/project_config.py

Lines changed: 21 additions & 2 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
@@ -106,12 +106,19 @@ def __init__(self, datafile, logger, error_handler):
106106
)
107107

108108
self.feature_key_map = self._generate_key_map(self.feature_flags, 'key', entities.FeatureFlag)
109+
110+
# Dict containing map of experiment ID to feature ID.
111+
# for checking that experiment is a feature experiment or not.
112+
self.experiment_feature_map = {}
109113
for feature in self.feature_key_map.values():
110114
feature.variables = self._generate_key_map(feature.variables, 'key', entities.Variable)
111115

112-
# Check if any of the experiments are in a group and add the group id for faster bucketing later on
113116
for exp_id in feature.experimentIds:
117+
# Add this experiment in experiment-feature map.
118+
self.experiment_feature_map[exp_id] = [feature.id]
119+
114120
experiment_in_feature = self.experiment_id_map[exp_id]
121+
# Check if any of the experiments are in a group and add the group id for faster bucketing later on
115122
if experiment_in_feature.groupId:
116123
feature.groupId = experiment_in_feature.groupId
117124
# Experiments in feature can only belong to one mutex group
@@ -609,3 +616,15 @@ def get_bot_filtering_value(self):
609616
"""
610617

611618
return self.bot_filtering
619+
620+
def is_feature_experiment(self, experiment_id):
621+
""" Determines if given experiment is a feature test.
622+
623+
Args:
624+
experiment_id: Experiment ID for which feature test is to be determined.
625+
626+
Returns:
627+
A boolean value that indicates if given experiment is a feature test.
628+
"""
629+
630+
return experiment_id in self.experiment_feature_map

tests/base.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,41 @@ def setUp(self, config_dict='config_dict'):
212212
'id': '130', 'value': '4243'
213213
}]
214214
}]
215+
}, {
216+
'key': 'test_experiment2',
217+
'status': 'Running',
218+
'layerId': '5',
219+
'audienceIds': [],
220+
'id': '111133',
221+
'forcedVariations': {},
222+
'trafficAllocation': [{
223+
'entityId': '122239',
224+
'endOfRange': 5000
225+
}, {
226+
'entityId': '122240',
227+
'endOfRange': 10000
228+
}],
229+
'variations': [{
230+
'id': '122239',
231+
'key': 'control',
232+
'featureEnabled': True,
233+
'variables': [
234+
{
235+
'id': '155551',
236+
'value': '42.42'
237+
}
238+
]
239+
}, {
240+
'id': '122240',
241+
'key': 'variation',
242+
'featureEnabled': True,
243+
'variables': [
244+
{
245+
'id': '155551',
246+
'value': '13.37'
247+
}
248+
]
249+
}]
215250
}],
216251
'groups': [{
217252
'id': '19228',
@@ -431,7 +466,7 @@ def setUp(self, config_dict='config_dict'):
431466
}, {
432467
'id': '91114',
433468
'key': 'test_feature_in_experiment_and_rollout',
434-
'experimentIds': ['111127'],
469+
'experimentIds': ['32223'],
435470
'rolloutId': '211111',
436471
'variables': [],
437472
}]

tests/test_config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,11 @@ def test_init__with_v4_datafile(self):
626626
}
627627
}
628628

629+
expected_experiment_feature_map = {
630+
'111127': ['91111'],
631+
'32222': ['91113']
632+
}
633+
629634
self.assertEqual(expected_variation_variable_usage_map['28901'],
630635
project_config.variation_variable_usage_map['28901'])
631636
self.assertEqual(expected_group_id_map, project_config.group_id_map)
@@ -639,6 +644,7 @@ def test_init__with_v4_datafile(self):
639644
self.assertEqual(expected_feature_key_map, project_config.feature_key_map)
640645
self.assertEqual(expected_rollout_id_map, project_config.rollout_id_map)
641646
self.assertEqual(expected_variation_variable_usage_map, project_config.variation_variable_usage_map)
647+
self.assertEqual(expected_experiment_feature_map, project_config.experiment_feature_map)
642648

643649
def test_variation_has_featureEnabled_false_if_prop_undefined(self):
644650
""" Test that featureEnabled property by default is set to False, when not given in the data file"""
@@ -1333,3 +1339,15 @@ def test_get_group__invalid_id(self):
13331339
self.assertRaisesRegexp(exceptions.InvalidGroupException,
13341340
enums.Errors.INVALID_GROUP_ID_ERROR,
13351341
self.project_config.get_group, '42')
1342+
1343+
def test_is_feature_experiment(self):
1344+
""" Test that a true is returned if experiment is a feature test, false otherwise. """
1345+
1346+
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
1347+
project_config = opt_obj.config
1348+
1349+
experiment = project_config.get_experiment_from_key('test_experiment2')
1350+
feature_experiment = project_config.get_experiment_from_key('test_experiment')
1351+
1352+
self.assertStrictFalse(project_config.is_feature_experiment(experiment.id))
1353+
self.assertStrictTrue(project_config.is_feature_experiment(feature_experiment.id))

0 commit comments

Comments
 (0)