Skip to content

Commit

Permalink
feat(audience-evaluation) : add support for complex audiences
Browse files Browse the repository at this point in the history
  • Loading branch information
oakbani committed Dec 4, 2018
1 parent 2ca253d commit 87af6f7
Show file tree
Hide file tree
Showing 5 changed files with 479 additions and 115 deletions.
7 changes: 6 additions & 1 deletion optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,23 @@ def __init__(self, id, key, experimentIds, **kwargs):
class Experiment(BaseEntity):

def __init__(self, id, key, status, audienceIds, variations, forcedVariations,
trafficAllocation, layerId, groupId=None, groupPolicy=None, **kwargs):
trafficAllocation, layerId, audienceConditions=None, groupId=None, groupPolicy=None, **kwargs):
self.id = id
self.key = key
self.status = status
self.audienceIds = audienceIds
self.audienceConditions = audienceConditions
self.variations = variations
self.forcedVariations = forcedVariations
self.trafficAllocation = trafficAllocation
self.layerId = layerId
self.groupId = groupId
self.groupPolicy = groupPolicy

def getAudienceConditionsOrIds(self):
""" Returns audienceConditions if present, otherwise audienceIds. """
return self.audienceConditions if self.audienceConditions is not None else self.audienceIds


class FeatureFlag(BaseEntity):

Expand Down
52 changes: 24 additions & 28 deletions optimizely/helpers/audience.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,6 @@
from . import condition_tree_evaluator


def is_match(audience, attributes):
""" Given audience information and user attributes determine if user meets the conditions.
Args:
audience: Dict representing the audience.
attributes: Dict representing user attributes which will be used in determining if the audience conditions are met.
Return:
Boolean representing if user satisfies audience conditions or not.
"""
custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator(
audience.conditionList, attributes)

is_match = condition_tree_evaluator.evaluate(
audience.conditionStructure,
lambda index: custom_attr_condition_evaluator.evaluate(index)
)

return is_match or False


def is_user_in_experiment(config, experiment, attributes):
""" Determine for given experiment if user satisfies the audiences for the experiment.
Expand All @@ -50,17 +29,34 @@ def is_user_in_experiment(config, experiment, attributes):
"""

# Return True in case there are no audiences
if not experiment.audienceIds:
audience_conditions = experiment.getAudienceConditionsOrIds()
if not audience_conditions:
return True

if attributes is None:
attributes = {}

# Return True if conditions for any one audience are met
for audience_id in experiment.audienceIds:
audience = config.get_audience(audience_id)
def evaluate_custom_attr(audienceIdToConditionIndexDict):
audienceId = list(audienceIdToConditionIndexDict.keys())[0]
index = audienceIdToConditionIndexDict[audienceId]

audience = config.get_audience(audienceId)
custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator(
audience.conditionList, attributes)

return custom_attr_condition_evaluator.evaluate(index)

if is_match(audience, attributes):
return True
def evaluate_audience(audienceId):
audience = config.get_audience(audienceId)

return condition_tree_evaluator.evaluate(
audience.conditionStructure,
lambda index: evaluate_custom_attr({audienceId: index})
)

eval_result = condition_tree_evaluator.evaluate(
audience_conditions,
lambda audienceId: evaluate_audience(audienceId)
)

return False
return eval_result or False
165 changes: 158 additions & 7 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,64 @@ def setUp(self, config_dict='config_dict'):
}
],
'id': '11638870867'
},
{
'experiments': [
{
'status': 'Running',
'key': '11488548028',
'layerId': '11551226732',
'trafficAllocation': [
{
'entityId': '11557362670',
'endOfRange': 10000
}
],
'audienceIds': ['0'],
'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899',
'3468206646', '3468206647', '3468206644', '3468206643']],
'variations': [
{
'variables': [],
'id': '11557362670',
'key': '11557362670',
'featureEnabled': True
}
],
'forcedVariations': {},
'id': '11488548028'
}
],
'id': '11551226732'
},
{
'experiments': [
{
'status': 'Paused',
'key': '11630490912',
'layerId': '11638870868',
'trafficAllocation': [
{
'entityId': '11475708559',
'endOfRange': 0
}
],
'audienceIds': [],
'variations': [
{
'variables': [],
'id': '11475708559',
'key': '11475708559',
'featureEnabled': False
}
],
'forcedVariations': {},
'id': '11630490912'
}
],
'id': '11638870868'
}

],
'anonymizeIP': False,
'projectId': '11624721371',
Expand Down Expand Up @@ -680,6 +737,27 @@ def setUp(self, config_dict='config_dict'):
],
'id': '11567102051',
'key': 'feat_with_var'
},
{
'experimentIds': [],
'rolloutId': '11551226732',
'variables': [],
'id': '11567102052',
'key': 'feat2'
},
{
'experimentIds': ['1323241599'],
'rolloutId': '11638870868',
'variables': [
{
'defaultValue': '10',
'type': 'integer',
'id': '11535264367',
'key': 'z'
}
],
'id': '11567102053',
'key': 'feat2_with_var'
}
],
'experiments': [
Expand Down Expand Up @@ -732,7 +810,59 @@ def setUp(self, config_dict='config_dict'):
'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646',
'3468206647', '3468206644', '3468206643'],
'forcedVariations': {}
}
},
{
'id': '1323241598',
'key': 'audience_combinations_experiment',
'layerId': '1323241598',
'status': 'Running',
'variations': [
{
'id': '1423767504',
'key': 'A',
'variables': []
}
],
'trafficAllocation': [
{
'entityId': '1423767504',
'endOfRange': 10000
}
],
'audienceIds': ['0'],
'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899',
'3468206646', '3468206647', '3468206644', '3468206643']],
'forcedVariations': {}
},
{
'id': '1323241599',
'key': 'feat2_with_var_test',
'layerId': '1323241600',
'status': 'Running',
'variations': [
{
'variables': [
{
'id': '11535264367',
'value': '150'
}
],
'id': '1423767505',
'key': 'variation_2',
'featureEnabled': True
}
],
'trafficAllocation': [
{
'entityId': '1423767505',
'endOfRange': 10000
}
],
'audienceIds': ['0'],
'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646',
'3468206647', '3468206644', '3468206643']],
'forcedVariations': {}
},
],
'audiences': [
{
Expand Down Expand Up @@ -769,44 +899,60 @@ def setUp(self, config_dict='config_dict'):
'id': '3468206643',
'name': '$$dummyExactBoolean',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
},
{
'id': '3468206645',
'name': '$$dummyMultipleCustomAttrs',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
},
{
'id': '0',
'name': '$$dummy',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }',
}
],
'typedAudiences': [
{
'id': '3988293898',
'name': 'substringString',
'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute',
'match': 'substring', 'value': 'Slytherin'}]]]
'match': 'substring', 'value': 'Slytherin'}]]]
},
{
'id': '3988293899',
'name': 'exists',
'conditions': ['and', ['or', ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute',
'match': 'exists'}]]]
'match': 'exists'}]]]
},
{
'id': '3468206646',
'name': 'exactNumber',
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
'match': 'exact', 'value': 45.5}]]]
'match': 'exact', 'value': 45.5}]]]
},
{
'id': '3468206647',
'name': 'gtNumber',
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
'match': 'gt', 'value': 70}]]]
'match': 'gt', 'value': 70}]]]
},
{
'id': '3468206644',
'name': 'ltNumber',
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
'match': 'lt', 'value': 1.0}]]]
'match': 'lt', 'value': 1.0}]]]
},
{
'id': '3468206643',
'name': 'exactBoolean',
'conditions': ['and', ['or', ['or', {'name': 'should_do_it', 'type': 'custom_attribute',
'match': 'exact', 'value': True}]]]
'match': 'exact', 'value': True}]]]
},
{
'id': '3468206645',
'name': 'multiple_custom_attrs',
'conditions': ["and", ["or", ["or", {"type": "custom_attribute", "name": "browser", "value": "chrome"},
{"type": "custom_attribute", "name": "browser", "value": "firefox"}]]]
}
],
'groups': [],
Expand Down Expand Up @@ -838,6 +984,11 @@ def setUp(self, config_dict='config_dict'):
'11564051718',
'1323241597'
]
},
{
'key': 'user_signed_up',
'id': '594090',
'experimentIds': ['1323241598', '1323241599'],
}
],
'revision': '3'
Expand Down
Loading

0 comments on commit 87af6f7

Please sign in to comment.