Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: decision service for group and multiple experiments. #322

Merged
merged 16 commits into from
May 4, 2021
60 changes: 7 additions & 53 deletions optimizely/decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,39 +413,6 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes

return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons

def get_experiment_in_group(self, project_config, group, bucketing_id):
""" Determine which experiment in the group the user is bucketed into.

Args:
project_config: Instance of ProjectConfig.
group: The group to bucket the user into.
bucketing_id: ID to be used for bucketing the user.

Returns:
Experiment if the user is bucketed into an experiment in the specified group. None otherwise
and array of log messages representing decision making.
"""
decide_reasons = []
experiment_id = self.bucketer.find_bucket(
project_config, bucketing_id, group.id, group.trafficAllocation)
if experiment_id:
experiment = project_config.get_experiment_from_id(experiment_id)
if experiment:
message = 'User with bucketing ID "%s" is in experiment %s of group %s.' % \
(bucketing_id, experiment.key, group.id)
self.logger.info(
message
)
decide_reasons.append(message)
return experiment, decide_reasons
message = 'User with bucketing ID "%s" is not in any experiments of group %s.' % (bucketing_id, group.id)
self.logger.info(
message
)
decide_reasons.append(message)

return None, decide_reasons

def get_variation_for_feature(self, project_config, feature, user_id, attributes=None, ignore_user_profile=False):
""" Returns the experiment/variation the user is bucketed in for the given feature.

Expand All @@ -462,31 +429,18 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes
decide_reasons = []
bucketing_id, reasons = self._get_bucketing_id(user_id, attributes)
decide_reasons += reasons
# First check if the feature is in a mutex group
if feature.groupId:
group = project_config.get_group(feature.groupId)
if group:
experiment, reasons = self.get_experiment_in_group(project_config, group, bucketing_id)
decide_reasons += reasons
if experiment and experiment.id in feature.experimentIds:

# Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments
if feature.experimentIds:
# Evaluate each experiment ID and return the first bucketed experiment variation
for experiment in feature.experimentIds:
experiment = project_config.get_experiment_from_id(experiment)
if experiment:
variation, variation_reasons = self.get_variation(
project_config, experiment, user_id, attributes, ignore_user_profile)
decide_reasons += variation_reasons
if variation:
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons
else:
self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature'))

# Next check if the feature is being experimented on
elif feature.experimentIds:
# If an experiment is not in a group, then the feature can only be associated with one experiment
experiment = project_config.get_experiment_from_id(feature.experimentIds[0])
if experiment:
variation, variation_reasons = self.get_variation(
project_config, experiment, user_id, attributes, ignore_user_profile)
decide_reasons += variation_reasons
if variation:
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons

# Next check if user is part of a rollout
if feature.rolloutId:
Expand Down
7 changes: 0 additions & 7 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,6 @@ def __init__(self, datafile, logger, error_handler):
# Add this experiment in experiment-feature map.
self.experiment_feature_map[exp_id] = [feature.id]

experiment_in_feature = self.experiment_id_map[exp_id]
# Check if any of the experiments are in a group and add the group id for faster bucketing later on
if experiment_in_feature.groupId:
feature.groupId = experiment_in_feature.groupId
# Experiments in feature can only belong to one mutex group
break

@staticmethod
def _generate_key_map(entity_list, key, entity_class):
""" Helper method to generate map from key to entity object for given list of dicts.
Expand Down
158 changes: 158 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,78 @@ def setUp(self, config_dict='config_dict'):
},
],
},
{
'key': 'test_experiment3',
'status': 'Running',
'layerId': '6',
"audienceConditions": [
"or",
"11160"
],
'audienceIds': ['11160'],
'id': '111134',
'forcedVariations': {},
'trafficAllocation': [
{'entityId': '222239', 'endOfRange': 2500},
{'entityId': '', 'endOfRange': 5000},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to discuss offline.

{'entityId': '', 'endOfRange': 7500},
{'entityId': '', 'endOfRange': 10000}
],
'variations': [
{
'id': '222239',
'key': 'control',
'variables': [],
}
],
},
{
'key': 'test_experiment4',
'status': 'Running',
'layerId': '7',
"audienceConditions": [
"or",
"11160"
],
'audienceIds': ['11160'],
'id': '111135',
'forcedVariations': {},
'trafficAllocation': [
{'entityId': '222240', 'endOfRange': 5000},
{'entityId': '', 'endOfRange': 7500},
{'entityId': '', 'endOfRange': 10000}
],
'variations': [
{
'id': '222240',
'key': 'control',
'variables': [],
}
],
},
{
'key': 'test_experiment5',
'status': 'Running',
'layerId': '8',
"audienceConditions": [
"or",
"11160"
],
'audienceIds': ['11160'],
'id': '111136',
'forcedVariations': {},
'trafficAllocation': [
{'entityId': '222241', 'endOfRange': 7500},
{'entityId': '', 'endOfRange': 10000}
],
'variations': [
{
'id': '222241',
'key': 'control',
'variables': [],
}
],
},
],
'groups': [
{
Expand Down Expand Up @@ -239,6 +311,72 @@ def setUp(self, config_dict='config_dict'):
{'entityId': '32222', "endOfRange": 3000},
{'entityId': '32223', 'endOfRange': 7500},
],
},
{
'id': '19229',
'policy': 'random',
'experiments': [
{
'id': '42222',
'key': 'group_2_exp_1',
'status': 'Running',
"audienceConditions": [
"or",
"11160"
],
'audienceIds': ['11160'],
'layerId': '211183',
'variations': [
{'key': 'var_1', 'id': '38901'},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append experiment key name with the key.

],
'forcedVariations': {},
'trafficAllocation': [
{'entityId': '38901', 'endOfRange': 10000}
],
},
{
'id': '42223',
'key': 'group_2_exp_2',
'status': 'Running',
"audienceConditions": [
"or",
"11160"
],
'audienceIds': ['11160'],
'layerId': '211184',
'variations': [
{'key': 'var_1', 'id': '38905'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

],
'forcedVariations': {},
'trafficAllocation': [
{'entityId': '38905', 'endOfRange': 10000}
],
},
{
'id': '42224',
'key': 'group_2_exp_3',
'status': 'Running',
"audienceConditions": [
"or",
"11160"
],
'audienceIds': ['11160'],
'layerId': '211185',
'variations': [
{'key': 'var_1', 'id': '38906'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.

],
'forcedVariations': {},
'trafficAllocation': [
{'entityId': '38906', 'endOfRange': 10000}
],
}
],
'trafficAllocation': [
{'entityId': '42222', "endOfRange": 2500},
{'entityId': '42223', 'endOfRange': 5000},
{'entityId': '42224', "endOfRange": 7500},
{'entityId': '', 'endOfRange': 10000},
],
}
],
'attributes': [{'key': 'test_attribute', 'id': '111094'}],
Expand All @@ -255,6 +393,12 @@ def setUp(self, config_dict='config_dict'):
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]',
'id': '11159',
},
{
'name': 'Test attribute users 3',
'conditions': "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \
\"experiment_attr\", \"type\": \"custom_attribute\", \"value\": \"group_experiment\"}]]]",
'id': '11160',
}
],
'rollouts': [
{'id': '201111', 'experiments': []},
Expand Down Expand Up @@ -364,6 +508,20 @@ def setUp(self, config_dict='config_dict'):
'rolloutId': '211111',
'variables': [],
},
{
'id': '91115',
'key': 'test_feature_in_exclusion_group',
'experimentIds': ['42222', '42223', '42224'],
'rolloutId': '211111',
'variables': [],
},
{
'id': '91116',
'key': 'test_feature_in_multiple_experiments',
'experimentIds': ['111134', '111135', '111136'],
'rolloutId': '211111',
'variables': [],
},
],
}

Expand Down
2 changes: 1 addition & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ def test_init__with_v4_datafile(self):
'211111',
{'number_of_projects': entities.Variable('131', 'number_of_projects', 'integer', '10')},
),
'test_feature_in_group': entities.FeatureFlag('91113', 'test_feature_in_group', ['32222'], '', {}, '19228'),
'test_feature_in_group': entities.FeatureFlag('91113', 'test_feature_in_group', ['32222'], '', {}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why 19228 removed.

}

expected_rollout_id_map = {
Expand Down
Loading