Skip to content

Commit 4fbd03b

Browse files
oakbanithomaszurkan-optimizely
authored andcommitted
Fix feature variable accessor default value (#113)
* 🖊️ Return default value when no variable usage found for variation * Remove sorting
1 parent 9972c3d commit 4fbd03b

File tree

4 files changed

+94
-43
lines changed

4 files changed

+94
-43
lines changed

optimizely/optimizely.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,7 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
230230
decision = self.decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
231231
if decision.variation:
232232
variable_value = self.config.get_variable_value_for_variation(variable, decision.variation)
233-
self.logger.log(
234-
enums.LogLevels.INFO,
235-
'Value for variable "%s" of feature flag "%s" is %s for user "%s".' % (
236-
variable_key, feature_key, variable_value, user_id
237-
))
233+
238234
else:
239235
variable_value = variable.defaultValue
240236
self.logger.log(
@@ -413,7 +409,7 @@ def get_enabled_features(self, user_id, attributes=None):
413409
attributes: Dict representing user attributes.
414410
415411
Returns:
416-
A sorted list of the keys of the features that are enabled for the user.
412+
A list of the keys of the features that are enabled for the user.
417413
"""
418414

419415
enabled_features = []
@@ -425,7 +421,6 @@ def get_enabled_features(self, user_id, attributes=None):
425421
if self.is_feature_enabled(feature.key, user_id, attributes):
426422
enabled_features.append(feature.key)
427423

428-
enabled_features.sort()
429424
return enabled_features
430425

431426
def get_feature_variable_boolean(self, feature_key, variable_key, user_id, attributes=None):

optimizely/project_config.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2017, Optimizely
1+
# Copyright 2016-2018, 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
@@ -90,10 +90,9 @@ def __init__(self, datafile, logger, error_handler):
9090
self.variation_id_map[experiment.key] = {}
9191
for variation in self.variation_key_map.get(experiment.key).values():
9292
self.variation_id_map[experiment.key][variation.id] = variation
93-
if variation.variables:
94-
self.variation_variable_usage_map[variation.id] = self._generate_key_map(
95-
variation.variables, 'id', entities.Variation.VariableUsage
96-
)
93+
self.variation_variable_usage_map[variation.id] = self._generate_key_map(
94+
variation.variables, 'id', entities.Variation.VariableUsage
95+
)
9796

9897
self.feature_key_map = self._generate_key_map(self.feature_flags, 'key', entities.FeatureFlag)
9998
for feature in self.feature_key_map.values():
@@ -439,10 +438,27 @@ def get_variable_value_for_variation(self, variable, variation):
439438
variable_usages = self.variation_variable_usage_map[variation.id]
440439

441440
# Find usage in given variation
442-
variable_usage = variable_usages.get(variable.id)
441+
variable_usage = None
442+
if variable_usages:
443+
variable_usage = variable_usages.get(variable.id)
444+
445+
if variable_usage:
446+
variable_value = variable_usage.value
447+
self.logger.log(
448+
enums.LogLevels.INFO,
449+
'Value for variable "%s" for variation "%s" is "%s".' % (
450+
variable.key, variation.key, variable_value
451+
))
443452

444-
# Return default value in case there is no variable usage for the variable.
445-
return variable_usage.value if variable_usage else variable.defaultValue
453+
else:
454+
variable_value = variable.defaultValue
455+
self.logger.log(
456+
enums.LogLevels.INFO,
457+
'Variable "%s" is not used in variation "%s". Assinging default value "%s".' % (
458+
variable.key, variation.key, variable_value
459+
))
460+
461+
return variable_value
446462

447463
def get_variable_for_feature(self, feature_key, variable_key):
448464
""" Get the variable with the given variable key for the given feature.

tests/test_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,8 @@ def test_init__with_v4_datafile(self):
616616
'129': entities.Variation.VariableUsage('129', '112'),
617617
'130': entities.Variation.VariableUsage('130', '1.211')
618618
},
619+
'28905': {},
620+
'28906': {},
619621
'211113': {
620622
'131': entities.Variation.VariableUsage('131', '15')
621623
}

tests/test_optimizely.py

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,29 +1387,6 @@ def side_effect(*args, **kwargs):
13871387
mock_is_feature_enabled.assert_any_call('test_feature_in_group', 'user_1', None)
13881388
mock_is_feature_enabled.assert_any_call('test_feature_in_experiment_and_rollout', 'user_1', None)
13891389

1390-
def test_get_enabled_features_returns_a_sorted_list(self):
1391-
""" Test that get_enabled_features returns a sorted list of enabled feature keys. """
1392-
1393-
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
1394-
1395-
with mock.patch('optimizely.optimizely.Optimizely.is_feature_enabled',
1396-
return_value=True) as mock_is_feature_enabled:
1397-
received_features = opt_obj.get_enabled_features('user_1')
1398-
1399-
mock_is_feature_enabled.assert_any_call('test_feature_in_experiment', 'user_1', None)
1400-
mock_is_feature_enabled.assert_any_call('test_feature_in_rollout', 'user_1', None)
1401-
mock_is_feature_enabled.assert_any_call('test_feature_in_group', 'user_1', None)
1402-
mock_is_feature_enabled.assert_any_call('test_feature_in_experiment_and_rollout', 'user_1', None)
1403-
1404-
expected_sorted_features = [
1405-
'test_feature_in_experiment',
1406-
'test_feature_in_experiment_and_rollout',
1407-
'test_feature_in_group',
1408-
'test_feature_in_rollout'
1409-
]
1410-
1411-
self.assertEqual(expected_sorted_features, received_features)
1412-
14131390
def test_get_enabled_features__invalid_object(self):
14141391
""" Test that get_enabled_features returns empty list if Optimizely object is not valid. """
14151392

@@ -1436,7 +1413,7 @@ def test_get_feature_variable_boolean(self):
14361413

14371414
mock_logger.assert_called_once_with(
14381415
enums.LogLevels.INFO,
1439-
'Value for variable "is_working" of feature flag "test_feature_in_experiment" is true for user "test_user".'
1416+
'Value for variable "is_working" for variation "variation" is "true".'
14401417
)
14411418

14421419
def test_get_feature_variable_double(self):
@@ -1454,7 +1431,7 @@ def test_get_feature_variable_double(self):
14541431

14551432
mock_logger.assert_called_once_with(
14561433
enums.LogLevels.INFO,
1457-
'Value for variable "cost" of feature flag "test_feature_in_experiment" is 10.02 for user "test_user".'
1434+
'Value for variable "cost" for variation "variation" is "10.02".'
14581435
)
14591436

14601437
def test_get_feature_variable_integer(self):
@@ -1472,7 +1449,7 @@ def test_get_feature_variable_integer(self):
14721449

14731450
mock_logger.assert_called_once_with(
14741451
enums.LogLevels.INFO,
1475-
'Value for variable "count" of feature flag "test_feature_in_experiment" is 4243 for user "test_user".'
1452+
'Value for variable "count" for variation "variation" is "4243".'
14761453
)
14771454

14781455
def test_get_feature_variable_string(self):
@@ -1491,10 +1468,71 @@ def test_get_feature_variable_string(self):
14911468

14921469
mock_logger.assert_called_once_with(
14931470
enums.LogLevels.INFO,
1494-
'Value for variable "environment" of feature flag "test_feature_in_experiment" is staging for user "test_user".'
1471+
'Value for variable "environment" for variation "variation" is "staging".'
1472+
)
1473+
1474+
def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_variation(self):
1475+
""" Test that get_feature_variable_* returns default value if variable usage not present in variation. """
1476+
1477+
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
1478+
mock_experiment = opt_obj.config.get_experiment_from_key('test_experiment')
1479+
mock_variation = opt_obj.config.get_variation_from_id('test_experiment', '111129')
1480+
1481+
# Empty variable usage map for the mocked variation
1482+
opt_obj.config.variation_variable_usage_map['111129'] = None
1483+
1484+
# Boolean
1485+
with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature',
1486+
return_value=decision_service.Decision(mock_experiment, mock_variation,
1487+
decision_service.DECISION_SOURCE_EXPERIMENT)), \
1488+
mock.patch('optimizely.logger.NoOpLogger.log') as mock_logger:
1489+
self.assertTrue(opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user'))
1490+
1491+
mock_logger.assert_called_once_with(
1492+
enums.LogLevels.INFO,
1493+
'Variable "is_working" is not used in variation "variation". Assinging default value "true".'
1494+
)
1495+
1496+
# Double
1497+
with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature',
1498+
return_value=decision_service.Decision(mock_experiment, mock_variation,
1499+
decision_service.DECISION_SOURCE_EXPERIMENT)), \
1500+
mock.patch('optimizely.logger.NoOpLogger.log') as mock_logger:
1501+
self.assertEqual(10.99,
1502+
opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'))
1503+
1504+
mock_logger.assert_called_once_with(
1505+
enums.LogLevels.INFO,
1506+
'Variable "cost" is not used in variation "variation". Assinging default value "10.99".'
1507+
)
1508+
1509+
# Integer
1510+
with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature',
1511+
return_value=decision_service.Decision(mock_experiment, mock_variation,
1512+
decision_service.DECISION_SOURCE_EXPERIMENT)), \
1513+
mock.patch('optimizely.logger.NoOpLogger.log') as mock_logger:
1514+
self.assertEqual(999,
1515+
opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'))
1516+
1517+
mock_logger.assert_called_once_with(
1518+
enums.LogLevels.INFO,
1519+
'Variable "count" is not used in variation "variation". Assinging default value "999".'
1520+
)
1521+
1522+
# String
1523+
with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature',
1524+
return_value=decision_service.Decision(mock_experiment, mock_variation,
1525+
decision_service.DECISION_SOURCE_EXPERIMENT)), \
1526+
mock.patch('optimizely.logger.NoOpLogger.log') as mock_logger:
1527+
self.assertEqual('devel',
1528+
opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'))
1529+
1530+
mock_logger.assert_called_once_with(
1531+
enums.LogLevels.INFO,
1532+
'Variable "environment" is not used in variation "variation". Assinging default value "devel".'
14951533
)
14961534

1497-
def test_get_feature_variable__returns_default_value(self):
1535+
def test_get_feature_variable__returns_default_value_if_no_variation(self):
14981536
""" Test that get_feature_variable_* returns default value if no variation. """
14991537

15001538
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))

0 commit comments

Comments
 (0)