Skip to content

Commit

Permalink
Updating evaluation of rollout rules (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
aliabbasrizvi committed Aug 29, 2017
1 parent b76340c commit 669bca2
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 126 deletions.
47 changes: 37 additions & 10 deletions optimizely/decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,29 +155,55 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal

return None

def get_variation_for_layer(self, layer, user_id, attributes=None, ignore_user_profile=False):
""" Determine which variation the user is in for a given layer.
def get_variation_for_rollout(self, rollout, user_id, attributes=None, ignore_user_profile=False):
""" Determine which variation the user is in for a given rollout.
Returns the variation of the first experiment the user qualifies for.
Args:
layer: Layer for which we are getting the variation.
rollout: Rollout for which we are getting the variation.
user_id: ID for user.
attributes: Dict representing user attributes.
ignore_user_profile: True to ignore the user profile lookup. Defaults to False.
Returns:
Variation the user should see. None if the user is not in any of the layer's experiments.
"""

# Go through each experiment in order and try to get the variation for the user
if layer:
for experiment_dict in layer.experiments:
experiment = self.config.get_experiment_from_key(experiment_dict['key'])
variation = self.get_variation(experiment, user_id, attributes, ignore_user_profile)
if rollout and len(rollout.experiments) > 0:
for idx in range(len(rollout.experiments) - 1):
experiment = self.config.get_experiment_from_key(rollout.experiments[idx].get('key'))

# Check if user meets audience conditions for targeting rule
if not audience_helper.is_user_in_experiment(self.config, experiment, attributes):
self.logger.log(
enums.LogLevels.DEBUG,
'User "%s" does not meet conditions for targeting rule %s.' % (user_id, idx + 1)
)
continue

self.logger.log(enums.LogLevels.DEBUG, 'User "%s" meets conditions for targeting rule %s.' % (user_id, idx + 1))
variation = self.bucketer.bucket(experiment, user_id)
if variation:
self.logger.log(enums.LogLevels.DEBUG,
'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key))
# Return as soon as we get a variation
return variation
else:
# Evaluate no further rules
self.logger.log(enums.LogLevels.DEBUG,
'User "%s" is not in the traffic group for the targeting else. '
'Checking "Everyone Else" rule now.' % user_id)
break

# Evaluate last rule i.e. "Everyone Else" rule
everyone_else_experiment = rollout.experiments[-1]
if audience_helper.is_user_in_experiment(self.config,
self.config.get_experiment_from_key(rollout.experiments[-1].get('key')),
attributes):
variation = self.bucketer.bucket(everyone_else_experiment, user_id)
if variation:
self.logger.log(enums.LogLevels.DEBUG,
'User "%s" meets conditions for targeting rule "Everyone Else".' % user_id)
return variation

return None
Expand All @@ -193,6 +219,7 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
Returns:
Variation that the user is bucketed in. None if the user is not in any variation.
"""

variation = None

# First check if the feature is in a mutex group
Expand Down Expand Up @@ -223,7 +250,7 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
# Next check if user is part of a rollout
if not variation and feature.rolloutId:
rollout = self.config.get_layer_from_id(feature.rolloutId)
variation = self.get_variation_for_layer(rollout, user_id, attributes, ignore_user_profile=True)
variation = self.get_variation_for_rollout(rollout, user_id, attributes, ignore_user_profile=True)

return variation

Expand Down
110 changes: 89 additions & 21 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,15 @@ def setUp(self):
'id': '111094'
}],
'audiences': [{
'name': 'Test attribute users',
'name': 'Test attribute users 1',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]',
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]',
'id': '11154'
}, {
'name': 'Test attribute users 2',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]',
'id': '11159'
}],
'projectId': '111001'
}
Expand Down Expand Up @@ -242,10 +247,15 @@ def setUp(self):
'id': '111094'
}],
'audiences': [{
'name': 'Test attribute users',
'name': 'Test attribute users 1',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]',
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]',
'id': '11154'
}, {
'name': 'Test attribute users 2',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]',
'id': '11159'
}],
'rollouts': [{
'id': '211111',
Expand All @@ -257,24 +267,48 @@ def setUp(self):
'layerId': '211111',
'audienceIds': ['11154'],
'trafficAllocation': [{
'entityId': '211128',
'endOfRange': 5000
}, {
'entityId': '211129',
'endOfRange': 9000
}],
'variations': [{
'key': '211128',
'id': '211128'
}, {
'key': '211129',
'id': '211129'
}]
}, {
'id': '211137',
'key': '211137',
'status': 'Running',
'forcedVariations': {},
'layerId': '211111',
'audienceIds': ['11159'],
'trafficAllocation': [{
'entityId': '211139',
'endOfRange': 3000
}],
'variations': [{
'key': '211139',
'id': '211139'
}]
}, {
'id': '211147',
'key': '211147',
'status': 'Running',
'forcedVariations': {},
'layerId': '211111',
'audienceIds': [],
'trafficAllocation': [{
'entityId': '211149',
'endOfRange': 6000
}],
'variations': [{
'key': '211149',
'id': '211149'
}]
}]
}],
'featureFlags': [{
'id': '91111',
'key': 'test_feature_1',
'key': 'test_feature_in_experiment',
'experimentIds': ['111127'],
'rolloutId': '',
'variables': [{
Expand All @@ -290,7 +324,7 @@ def setUp(self):
}]
}, {
'id': '91112',
'key': 'test_feature_2',
'key': 'test_feature_in_rollout',
'experimentIds': [],
'rolloutId': '211111',
'variables': [],
Expand Down Expand Up @@ -424,8 +458,13 @@ def setUp(self):
'audiences': [{
'name': 'Test attribute users',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]',
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]',
'id': '11154'
}, {
'name': 'Test attribute users',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]',
'id': '11159'
}],
'projectId': '111001'
}
Expand Down Expand Up @@ -474,10 +513,15 @@ def setUp(self):
'id': '111094'
}],
'audiences': [{
'name': 'Test attribute users',
'name': 'Test attribute users 1',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]',
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]',
'id': '11154'
}, {
'name': 'Test attribute users 2',
'conditions': '["and", ["or", ["or", '
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]',
'id': '11159'
}],
'rollouts': [{
'id': '211111',
Expand All @@ -489,19 +533,43 @@ def setUp(self):
'layerId': '211111',
'audienceIds': ['11154'],
'trafficAllocation': [{
'entityId': '211128',
'endOfRange': 5000
}, {
'entityId': '211129',
'endOfRange': 9000
}],
'variations': [{
'key': '211128',
'id': '211128'
}, {
'key': '211129',
'id': '211129'
}]
}, {
'id': '211137',
'key': '211137',
'status': 'Running',
'forcedVariations': {},
'layerId': '211111',
'audienceIds': ['11159'],
'trafficAllocation': [{
'entityId': '211139',
'endOfRange': 3000
}],
'variations': [{
'key': '211139',
'id': '211139'
}]
}, {
'id': '211147',
'key': '211147',
'status': 'Running',
'forcedVariations': {},
'layerId': '211111',
'audienceIds': [],
'trafficAllocation': [{
'entityId': '211149',
'endOfRange': 6000
}],
'variations': [{
'key': '211149',
'id': '211149'
}]
}]
}],
'featureFlags': [{
Expand Down
6 changes: 3 additions & 3 deletions tests/helpers_tests/test_audience.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_is_match__audience_condition_matches(self):
""" Test that is_match returns True when audience conditions are met. """

user_attributes = {
'test_attribute': 'test_value',
'test_attribute': 'test_value_1',
'browser_type': 'firefox',
'location': 'San Francisco'
}
Expand All @@ -45,7 +45,7 @@ def test_is_user_in_experiment__no_audience(self):
""" Test that is_user_in_experiment returns True when experiment is using no audience. """

user_attributes = {
'test_attribute': 'test_value',
'test_attribute': 'test_value_1',
'browser_type': 'firefox',
'location': 'San Francisco'
}
Expand All @@ -58,7 +58,7 @@ def test_is_user_in_experiment__audience_conditions_are_met(self):
""" Test that is_user_in_experiment returns True when audience conditions are met. """

user_attributes = {
'test_attribute': 'test_value',
'test_attribute': 'test_value_1',
'browser_type': 'firefox',
'location': 'San Francisco'
}
Expand Down
4 changes: 2 additions & 2 deletions tests/helpers_tests/test_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def setUp(self):
self.config_dict['audiences'][0]['conditions']
)
attributes = {
'test_attribute': 'test_value',
'test_attribute': 'test_value_1',
'browser_type': 'firefox',
'location': 'San Francisco'
}
Expand Down Expand Up @@ -121,4 +121,4 @@ def test_loads(self):
)

self.assertEqual(['and', ['or', ['or', 0]]], condition_structure)
self.assertEqual([['test_attribute', 'test_value']], condition_list)
self.assertEqual([['test_attribute', 'test_value_1']], condition_list)
Loading

0 comments on commit 669bca2

Please sign in to comment.