Skip to content

Commit a59b08d

Browse files
oakbanialiabbasrizvi
authored andcommitted
feat (audiences): Audience combinations (#150)
1 parent abb6723 commit a59b08d

File tree

5 files changed

+479
-115
lines changed

5 files changed

+479
-115
lines changed

optimizely/entities.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,23 @@ def __init__(self, id, key, experimentIds, **kwargs):
4646
class Experiment(BaseEntity):
4747

4848
def __init__(self, id, key, status, audienceIds, variations, forcedVariations,
49-
trafficAllocation, layerId, groupId=None, groupPolicy=None, **kwargs):
49+
trafficAllocation, layerId, audienceConditions=None, groupId=None, groupPolicy=None, **kwargs):
5050
self.id = id
5151
self.key = key
5252
self.status = status
5353
self.audienceIds = audienceIds
54+
self.audienceConditions = audienceConditions
5455
self.variations = variations
5556
self.forcedVariations = forcedVariations
5657
self.trafficAllocation = trafficAllocation
5758
self.layerId = layerId
5859
self.groupId = groupId
5960
self.groupPolicy = groupPolicy
6061

62+
def getAudienceConditionsOrIds(self):
63+
""" Returns audienceConditions if present, otherwise audienceIds. """
64+
return self.audienceConditions if self.audienceConditions is not None else self.audienceIds
65+
6166

6267
class FeatureFlag(BaseEntity):
6368

optimizely/helpers/audience.py

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,6 @@
1515
from . import condition_tree_evaluator
1616

1717

18-
def is_match(audience, attributes):
19-
""" Given audience information and user attributes determine if user meets the conditions.
20-
21-
Args:
22-
audience: Dict representing the audience.
23-
attributes: Dict representing user attributes which will be used in determining if the audience conditions are met.
24-
25-
Return:
26-
Boolean representing if user satisfies audience conditions or not.
27-
"""
28-
custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator(
29-
audience.conditionList, attributes)
30-
31-
is_match = condition_tree_evaluator.evaluate(
32-
audience.conditionStructure,
33-
lambda index: custom_attr_condition_evaluator.evaluate(index)
34-
)
35-
36-
return is_match or False
37-
38-
3918
def is_user_in_experiment(config, experiment, attributes):
4019
""" Determine for given experiment if user satisfies the audiences for the experiment.
4120
@@ -50,17 +29,34 @@ def is_user_in_experiment(config, experiment, attributes):
5029
"""
5130

5231
# Return True in case there are no audiences
53-
if not experiment.audienceIds:
32+
audience_conditions = experiment.getAudienceConditionsOrIds()
33+
if audience_conditions is None or audience_conditions == []:
5434
return True
5535

5636
if attributes is None:
5737
attributes = {}
5838

59-
# Return True if conditions for any one audience are met
60-
for audience_id in experiment.audienceIds:
61-
audience = config.get_audience(audience_id)
39+
def evaluate_custom_attr(audienceId, index):
40+
audience = config.get_audience(audienceId)
41+
custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator(
42+
audience.conditionList, attributes)
43+
44+
return custom_attr_condition_evaluator.evaluate(index)
45+
46+
def evaluate_audience(audienceId):
47+
audience = config.get_audience(audienceId)
6248

63-
if is_match(audience, attributes):
64-
return True
49+
if audience is None:
50+
return None
51+
52+
return condition_tree_evaluator.evaluate(
53+
audience.conditionStructure,
54+
lambda index: evaluate_custom_attr(audienceId, index)
55+
)
56+
57+
eval_result = condition_tree_evaluator.evaluate(
58+
audience_conditions,
59+
evaluate_audience
60+
)
6561

66-
return False
62+
return eval_result or False

tests/base.py

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,64 @@ def setUp(self, config_dict='config_dict'):
652652
}
653653
],
654654
'id': '11638870867'
655+
},
656+
{
657+
'experiments': [
658+
{
659+
'status': 'Running',
660+
'key': '11488548028',
661+
'layerId': '11551226732',
662+
'trafficAllocation': [
663+
{
664+
'entityId': '11557362670',
665+
'endOfRange': 10000
666+
}
667+
],
668+
'audienceIds': ['0'],
669+
'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899',
670+
'3468206646', '3468206647', '3468206644', '3468206643']],
671+
'variations': [
672+
{
673+
'variables': [],
674+
'id': '11557362670',
675+
'key': '11557362670',
676+
'featureEnabled': True
677+
}
678+
],
679+
'forcedVariations': {},
680+
'id': '11488548028'
681+
}
682+
],
683+
'id': '11551226732'
684+
},
685+
{
686+
'experiments': [
687+
{
688+
'status': 'Paused',
689+
'key': '11630490912',
690+
'layerId': '11638870868',
691+
'trafficAllocation': [
692+
{
693+
'entityId': '11475708559',
694+
'endOfRange': 0
695+
}
696+
],
697+
'audienceIds': [],
698+
'variations': [
699+
{
700+
'variables': [],
701+
'id': '11475708559',
702+
'key': '11475708559',
703+
'featureEnabled': False
704+
}
705+
],
706+
'forcedVariations': {},
707+
'id': '11630490912'
708+
}
709+
],
710+
'id': '11638870868'
655711
}
712+
656713
],
657714
'anonymizeIP': False,
658715
'projectId': '11624721371',
@@ -680,6 +737,27 @@ def setUp(self, config_dict='config_dict'):
680737
],
681738
'id': '11567102051',
682739
'key': 'feat_with_var'
740+
},
741+
{
742+
'experimentIds': [],
743+
'rolloutId': '11551226732',
744+
'variables': [],
745+
'id': '11567102052',
746+
'key': 'feat2'
747+
},
748+
{
749+
'experimentIds': ['1323241599'],
750+
'rolloutId': '11638870868',
751+
'variables': [
752+
{
753+
'defaultValue': '10',
754+
'type': 'integer',
755+
'id': '11535264367',
756+
'key': 'z'
757+
}
758+
],
759+
'id': '11567102053',
760+
'key': 'feat2_with_var'
683761
}
684762
],
685763
'experiments': [
@@ -732,7 +810,59 @@ def setUp(self, config_dict='config_dict'):
732810
'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646',
733811
'3468206647', '3468206644', '3468206643'],
734812
'forcedVariations': {}
735-
}
813+
},
814+
{
815+
'id': '1323241598',
816+
'key': 'audience_combinations_experiment',
817+
'layerId': '1323241598',
818+
'status': 'Running',
819+
'variations': [
820+
{
821+
'id': '1423767504',
822+
'key': 'A',
823+
'variables': []
824+
}
825+
],
826+
'trafficAllocation': [
827+
{
828+
'entityId': '1423767504',
829+
'endOfRange': 10000
830+
}
831+
],
832+
'audienceIds': ['0'],
833+
'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899',
834+
'3468206646', '3468206647', '3468206644', '3468206643']],
835+
'forcedVariations': {}
836+
},
837+
{
838+
'id': '1323241599',
839+
'key': 'feat2_with_var_test',
840+
'layerId': '1323241600',
841+
'status': 'Running',
842+
'variations': [
843+
{
844+
'variables': [
845+
{
846+
'id': '11535264367',
847+
'value': '150'
848+
}
849+
],
850+
'id': '1423767505',
851+
'key': 'variation_2',
852+
'featureEnabled': True
853+
}
854+
],
855+
'trafficAllocation': [
856+
{
857+
'entityId': '1423767505',
858+
'endOfRange': 10000
859+
}
860+
],
861+
'audienceIds': ['0'],
862+
'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646',
863+
'3468206647', '3468206644', '3468206643']],
864+
'forcedVariations': {}
865+
},
736866
],
737867
'audiences': [
738868
{
@@ -769,44 +899,60 @@ def setUp(self, config_dict='config_dict'):
769899
'id': '3468206643',
770900
'name': '$$dummyExactBoolean',
771901
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
902+
},
903+
{
904+
'id': '3468206645',
905+
'name': '$$dummyMultipleCustomAttrs',
906+
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
907+
},
908+
{
909+
'id': '0',
910+
'name': '$$dummy',
911+
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }',
772912
}
773913
],
774914
'typedAudiences': [
775915
{
776916
'id': '3988293898',
777917
'name': 'substringString',
778918
'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute',
779-
'match': 'substring', 'value': 'Slytherin'}]]]
919+
'match': 'substring', 'value': 'Slytherin'}]]]
780920
},
781921
{
782922
'id': '3988293899',
783923
'name': 'exists',
784924
'conditions': ['and', ['or', ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute',
785-
'match': 'exists'}]]]
925+
'match': 'exists'}]]]
786926
},
787927
{
788928
'id': '3468206646',
789929
'name': 'exactNumber',
790930
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
791-
'match': 'exact', 'value': 45.5}]]]
931+
'match': 'exact', 'value': 45.5}]]]
792932
},
793933
{
794934
'id': '3468206647',
795935
'name': 'gtNumber',
796936
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
797-
'match': 'gt', 'value': 70}]]]
937+
'match': 'gt', 'value': 70}]]]
798938
},
799939
{
800940
'id': '3468206644',
801941
'name': 'ltNumber',
802942
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
803-
'match': 'lt', 'value': 1.0}]]]
943+
'match': 'lt', 'value': 1.0}]]]
804944
},
805945
{
806946
'id': '3468206643',
807947
'name': 'exactBoolean',
808948
'conditions': ['and', ['or', ['or', {'name': 'should_do_it', 'type': 'custom_attribute',
809-
'match': 'exact', 'value': True}]]]
949+
'match': 'exact', 'value': True}]]]
950+
},
951+
{
952+
'id': '3468206645',
953+
'name': 'multiple_custom_attrs',
954+
'conditions': ["and", ["or", ["or", {"type": "custom_attribute", "name": "browser", "value": "chrome"},
955+
{"type": "custom_attribute", "name": "browser", "value": "firefox"}]]]
810956
}
811957
],
812958
'groups': [],
@@ -838,6 +984,11 @@ def setUp(self, config_dict='config_dict'):
838984
'11564051718',
839985
'1323241597'
840986
]
987+
},
988+
{
989+
'key': 'user_signed_up',
990+
'id': '594090',
991+
'experimentIds': ['1323241598', '1323241599'],
841992
}
842993
],
843994
'revision': '3'

0 commit comments

Comments
 (0)