Skip to content

Commit

Permalink
Merge 37705bb into 9fcf5af
Browse files Browse the repository at this point in the history
  • Loading branch information
oakbani committed Nov 30, 2018
2 parents 9fcf5af + 37705bb commit acfe310
Show file tree
Hide file tree
Showing 6 changed files with 445 additions and 9 deletions.
8 changes: 4 additions & 4 deletions optimizely/helpers/audience.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def is_user_in_experiment(config, experiment, attributes):
Args:
config: project_config.ProjectConfig object representing the project.
experiment: Object representing the experiment.
attributes: Dict representing user attributes which will be used in determining if the audience conditions are met.
attributes: Dict representing user attributes which will be used in determining
if the audience conditions are met. If not provided, default to an empty dict.
Returns:
Boolean representing if user satisfies audience conditions for any of the audiences or not.
Expand All @@ -52,9 +53,8 @@ def is_user_in_experiment(config, experiment, attributes):
if not experiment.audienceIds:
return True

# Return False if there are audiences, but no attributes
if not attributes:
return False
if attributes is None:
attributes = {}

# Return True if conditions for any one audience are met
for audience_id in experiment.audienceIds:
Expand Down
10 changes: 10 additions & 0 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def __init__(self, datafile, logger, error_handler):
self.events = config.get('events', [])
self.attributes = config.get('attributes', [])
self.audiences = config.get('audiences', [])
self.typed_audiences = config.get('typedAudiences', [])
self.feature_flags = config.get('featureFlags', [])
self.rollouts = config.get('rollouts', [])
self.anonymize_ip = config.get('anonymizeIP', False)
Expand All @@ -63,7 +64,16 @@ def __init__(self, datafile, logger, error_handler):
self.experiment_key_map = self._generate_key_map(self.experiments, 'key', entities.Experiment)
self.event_key_map = self._generate_key_map(self.events, 'key', entities.Event)
self.attribute_key_map = self._generate_key_map(self.attributes, 'key', entities.Attribute)

self.audience_id_map = self._generate_key_map(self.audiences, 'id', entities.Audience)

# Conditions of audiences in typedAudiences are not expected
# to be string-encoded as they are in audiences.
for typed_audience in self.typed_audiences:
typed_audience['conditions'] = json.dumps(typed_audience['conditions'])
typed_audience_id_map = self._generate_key_map(self.typed_audiences, 'id', entities.Audience)
self.audience_id_map.update(typed_audience_id_map)

self.rollout_id_map = self._generate_key_map(self.rollouts, 'id', entities.Layer)
for layer in self.rollout_id_map.values():
for experiment in layer.experiments:
Expand Down
248 changes: 248 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,254 @@ def setUp(self, config_dict='config_dict'):
'revision': '1337'
}

self.config_dict_with_typed_audiences = {
'version': '4',
'rollouts': [
{
'experiments': [
{
'status': 'Running',
'key': '11488548027',
'layerId': '11551226731',
'trafficAllocation': [
{
'entityId': '11557362669',
'endOfRange': 10000
}
],
'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646',
'3468206647', '3468206644', '3468206643'],
'variations': [
{
'variables': [],
'id': '11557362669',
'key': '11557362669',
'featureEnabled':True
}
],
'forcedVariations': {},
'id': '11488548027'
}
],
'id': '11551226731'
},
{
'experiments': [
{
'status': 'Paused',
'key': '11630490911',
'layerId': '11638870867',
'trafficAllocation': [
{
'entityId': '11475708558',
'endOfRange': 0
}
],
'audienceIds': [],
'variations': [
{
'variables': [],
'id': '11475708558',
'key': '11475708558',
'featureEnabled':False
}
],
'forcedVariations': {},
'id': '11630490911'
}
],
'id': '11638870867'
}
],
'anonymizeIP': False,
'projectId': '11624721371',
'variables': [],
'featureFlags': [
{
'experimentIds': [],
'rolloutId': '11551226731',
'variables': [],
'id': '11477755619',
'key': 'feat'
},
{
'experimentIds': [
'11564051718'
],
'rolloutId': '11638870867',
'variables': [
{
'defaultValue': 'x',
'type': 'string',
'id': '11535264366',
'key': 'x'
}
],
'id': '11567102051',
'key': 'feat_with_var'
}
],
'experiments': [
{
'status': 'Running',
'key': 'feat_with_var_test',
'layerId': '11504144555',
'trafficAllocation': [
{
'entityId': '11617170975',
'endOfRange': 10000
}
],
'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646',
'3468206647', '3468206644', '3468206643'],
'variations': [
{
'variables': [
{
'id': '11535264366',
'value': 'xyz'
}
],
'id': '11617170975',
'key': 'variation_2',
'featureEnabled': True
}
],
'forcedVariations': {},
'id': '11564051718'
},
{
'id': '1323241597',
'key': 'typed_audience_experiment',
'layerId': '1630555627',
'status': 'Running',
'variations': [
{
'id': '1423767503',
'key': 'A',
'variables': []
}
],
'trafficAllocation': [
{
'entityId': '1423767503',
'endOfRange': 10000
}
],
'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646',
'3468206647', '3468206644', '3468206643'],
'forcedVariations': {}
}
],
'audiences': [
{
'id': '3468206642',
'name': 'exactString',
'conditions': '["and", ["or", ["or", {"name": "house", "type": "custom_attribute", "value": "Gryffindor"}]]]'
},
{
'id': '3988293898',
'name': '$$dummySubstringString',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
},
{
'id': '3988293899',
'name': '$$dummyExists',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
},
{
'id': '3468206646',
'name': '$$dummyExactNumber',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
},
{
'id': '3468206647',
'name': '$$dummyGtNumber',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
},
{
'id': '3468206644',
'name': '$$dummyLtNumber',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
},
{
'id': '3468206643',
'name': '$$dummyExactBoolean',
'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'}]]]
},
{
'id': '3988293899',
'name': 'exists',
'conditions': ['and', ['or', ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute',
'match': 'exists'}]]]
},
{
'id': '3468206646',
'name': 'exactNumber',
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
'match': 'exact', 'value': 45.5}]]]
},
{
'id': '3468206647',
'name': 'gtNumber',
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
'match': 'gt', 'value': 70}]]]
},
{
'id': '3468206644',
'name': 'ltNumber',
'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute',
'match': 'lt', 'value': 1.0}]]]
},
{
'id': '3468206643',
'name': 'exactBoolean',
'conditions': ['and', ['or', ['or', {'name': 'should_do_it', 'type': 'custom_attribute',
'match': 'exact', 'value': True}]]]
}
],
'groups': [],
'attributes': [
{
'key': 'house',
'id': '594015'
},
{
'key': 'lasers',
'id': '594016'
},
{
'key': 'should_do_it',
'id': '594017'
},
{
'key': 'favorite_ice_cream',
'id': '594018'
}
],
'botFiltering': False,
'accountId': '4879520872',
'events': [
{
'key': 'item_bought',
'id': '594089',
'experimentIds': [
'11564051718',
'1323241597'
]
}
],
'revision': '3'
}

config = getattr(self, config_dict)
self.optimizely = optimizely.Optimizely(json.dumps(config))
self.project_config = self.optimizely.config
23 changes: 18 additions & 5 deletions tests/helpers_tests/test_audience.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,27 @@ def test_is_user_in_experiment__no_audience(self):
self.assertTrue(audience.is_user_in_experiment(self.project_config, experiment, user_attributes))

def test_is_user_in_experiment__no_attributes(self):
""" Test that is_user_in_experiment returns True when experiment is using no audience. """
""" Test that is_user_in_experiment defaults attributes to empty Dict and
is_match does get called with empty attributes. """

with mock.patch('optimizely.helpers.audience.is_match') as mock_is_match:
audience.is_user_in_experiment(
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'), None
)

self.assertFalse(audience.is_user_in_experiment(
self.project_config, self.project_config.get_experiment_from_key('test_experiment'), None)
mock_is_match.assert_called_once_with(
self.optimizely.config.get_audience('11154'), {}
)

self.assertFalse(audience.is_user_in_experiment(
self.project_config, self.project_config.get_experiment_from_key('test_experiment'), {})
with mock.patch('optimizely.helpers.audience.is_match') as mock_is_match:
audience.is_user_in_experiment(
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'), {}
)

mock_is_match.assert_called_once_with(
self.optimizely.config.get_audience('11154'), {}
)

def test_is_user_in_experiment__audience_conditions_are_met(self):
Expand Down
37 changes: 37 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,43 @@ def test_get_audience__invalid_id(self):

self.assertIsNone(self.project_config.get_audience('42'))

def test_get_audience__prefers_typedAudiences_over_audiences(self):
opt = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences))
config = opt.config

audiences = self.config_dict_with_typed_audiences['audiences']
typed_audiences = self.config_dict_with_typed_audiences['typedAudiences']

audience_3988293898 = {
'id': '3988293898',
'name': '$$dummySubstringString',
'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }'
}

self.assertTrue(audience_3988293898 in audiences)

typed_audience_3988293898 = {
'id': '3988293898',
'name': 'substringString',
'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute',
'match': 'substring', 'value': 'Slytherin'}]]]
}

self.assertTrue(typed_audience_3988293898 in typed_audiences)

audience = config.get_audience('3988293898')

self.assertEqual('3988293898', audience.id)
self.assertEqual('substringString', audience.name)

# compare parsed JSON as conditions for typedAudiences is generated via json.dumps
# which can be different for python versions.
self.assertEqual(json.loads(
'["and", ["or", ["or", {"match": "substring", "type": "custom_attribute",'
' "name": "house", "value": "Slytherin"}]]]'),
json.loads(audience.conditions)
)

def test_get_variation_from_key__valid_experiment_key(self):
""" Test that variation is retrieved correctly when valid experiment key and variation key are provided. """

Expand Down
Loading

0 comments on commit acfe310

Please sign in to comment.