Skip to content

Commit a34b8a8

Browse files
fix(logs): Fixing log messages for Targeted Rollouts (#268)
1 parent b7475e4 commit a34b8a8

File tree

9 files changed

+479
-197
lines changed

9 files changed

+479
-197
lines changed

optimizely/bucketer.py

+29-32
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2017, 2019 Optimizely
1+
# Copyright 2016-2017, 2019-2020 Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -38,41 +38,41 @@ def __init__(self):
3838
def _generate_unsigned_hash_code_32_bit(self, bucketing_id):
3939
""" Helper method to retrieve hash code.
4040
41-
Args:
42-
bucketing_id: ID for bucketing.
41+
Args:
42+
bucketing_id: ID for bucketing.
4343
44-
Returns:
45-
Hash code which is a 32 bit unsigned integer.
46-
"""
44+
Returns:
45+
Hash code which is a 32 bit unsigned integer.
46+
"""
4747

4848
# Adjusting MurmurHash code to be unsigned
4949
return mmh3.hash(bucketing_id, self.bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
5050

5151
def _generate_bucket_value(self, bucketing_id):
5252
""" Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
5353
54-
Args:
55-
bucketing_id: ID for bucketing.
54+
Args:
55+
bucketing_id: ID for bucketing.
5656
57-
Returns:
58-
Bucket value corresponding to the provided bucketing ID.
59-
"""
57+
Returns:
58+
Bucket value corresponding to the provided bucketing ID.
59+
"""
6060

6161
ratio = float(self._generate_unsigned_hash_code_32_bit(bucketing_id)) / MAX_HASH_VALUE
6262
return math.floor(ratio * MAX_TRAFFIC_VALUE)
6363

6464
def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocations):
6565
""" Determine entity based on bucket value and traffic allocations.
6666
67-
Args:
68-
project_config: Instance of ProjectConfig.
69-
bucketing_id: ID to be used for bucketing the user.
70-
parent_id: ID representing group or experiment.
71-
traffic_allocations: Traffic allocations representing traffic allotted to experiments or variations.
67+
Args:
68+
project_config: Instance of ProjectConfig.
69+
bucketing_id: ID to be used for bucketing the user.
70+
parent_id: ID representing group or experiment.
71+
traffic_allocations: Traffic allocations representing traffic allotted to experiments or variations.
7272
73-
Returns:
74-
Entity ID which may represent experiment or variation.
75-
"""
73+
Returns:
74+
Entity ID which may represent experiment or variation.
75+
"""
7676

7777
bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id)
7878
bucketing_number = self._generate_bucket_value(bucketing_key)
@@ -90,20 +90,21 @@ def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocatio
9090
def bucket(self, project_config, experiment, user_id, bucketing_id):
9191
""" For a given experiment and bucketing ID determines variation to be shown to user.
9292
93-
Args:
94-
project_config: Instance of ProjectConfig.
95-
experiment: Object representing the experiment for which user is to be bucketed.
96-
user_id: ID for user.
97-
bucketing_id: ID to be used for bucketing the user.
93+
Args:
94+
project_config: Instance of ProjectConfig.
95+
experiment: Object representing the experiment or rollout rule in which user is to be bucketed.
96+
user_id: ID for user.
97+
bucketing_id: ID to be used for bucketing the user.
9898
99-
Returns:
100-
Variation in which user with ID user_id will be put in. None if no variation.
101-
"""
99+
Returns:
100+
Variation in which user with ID user_id will be put in. None if no variation.
101+
"""
102102

103103
if not experiment:
104104
return None
105105

106-
# Determine if experiment is in a mutually exclusive group
106+
# Determine if experiment is in a mutually exclusive group.
107+
# This will not affect evaluation of rollout rules.
107108
if experiment.groupPolicy in GROUP_POLICIES:
108109
group = project_config.get_group(experiment.groupId)
109110

@@ -131,10 +132,6 @@ def bucket(self, project_config, experiment, user_id, bucketing_id):
131132
variation_id = self.find_bucket(project_config, bucketing_id, experiment.id, experiment.trafficAllocation)
132133
if variation_id:
133134
variation = project_config.get_variation_from_id(experiment.key, variation_id)
134-
project_config.logger.info(
135-
'User "%s" is in variation "%s" of experiment %s.' % (user_id, variation.key, experiment.key)
136-
)
137135
return variation
138136

139-
project_config.logger.info('User "%s" is in no variation.' % user_id)
140137
return None

optimizely/decision_service.py

+43-27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2017-2019, Optimizely
1+
# Copyright 2017-2020, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -21,6 +21,7 @@
2121
from .helpers import validator
2222
from .user_profile import UserProfile
2323

24+
2425
Decision = namedtuple('Decision', 'experiment variation source')
2526

2627

@@ -250,7 +251,7 @@ def get_variation(self, project_config, experiment, user_id, attributes, ignore_
250251
try:
251252
retrieved_profile = self.user_profile_service.lookup(user_id)
252253
except:
253-
self.logger.exception('Unable to retrieve user profile for user "%s" as lookup failed.' % user_id)
254+
self.logger.exception('Unable to retrieve user profile for user "{}" as lookup failed.'.format(user_id))
254255
retrieved_profile = None
255256

256257
if validator.is_user_profile_valid(retrieved_profile):
@@ -262,24 +263,33 @@ def get_variation(self, project_config, experiment, user_id, attributes, ignore_
262263
self.logger.warning('User profile has invalid format.')
263264

264265
# Bucket user and store the new decision
265-
if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger):
266-
self.logger.info('User "%s" does not meet conditions to be in experiment "%s".' % (user_id, experiment.key))
266+
audience_conditions = experiment.get_audience_conditions_or_ids()
267+
if not audience_helper.does_user_meet_audience_conditions(project_config, audience_conditions,
268+
enums.ExperimentAudienceEvaluationLogs,
269+
experiment.key,
270+
attributes, self.logger):
271+
self.logger.info(
272+
'User "{}" does not meet conditions to be in experiment "{}".'.format(user_id, experiment.key))
267273
return None
268274

269275
# Determine bucketing ID to be used
270276
bucketing_id = self._get_bucketing_id(user_id, attributes)
271277
variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id)
272278

273279
if variation:
280+
self.logger.info(
281+
'User "%s" is in variation "%s" of experiment %s.' % (user_id, variation.key, experiment.key)
282+
)
274283
# Store this new decision and return the variation for the user
275284
if not ignore_user_profile and self.user_profile_service:
276285
try:
277286
user_profile.save_variation_for_experiment(experiment.id, variation.id)
278287
self.user_profile_service.save(user_profile.__dict__)
279288
except:
280-
self.logger.exception('Unable to save user profile for user "%s".' % user_id)
289+
self.logger.exception('Unable to save user profile for user "{}".'.format(user_id))
281290
return variation
282291

292+
self.logger.info('User "%s" is in no variation.' % user_id)
283293
return None
284294

285295
def get_variation_for_rollout(self, project_config, rollout, user_id, attributes=None):
@@ -299,44 +309,56 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes
299309
# Go through each experiment in order and try to get the variation for the user
300310
if rollout and len(rollout.experiments) > 0:
301311
for idx in range(len(rollout.experiments) - 1):
302-
experiment = project_config.get_experiment_from_key(rollout.experiments[idx].get('key'))
312+
logging_key = str(idx + 1)
313+
rollout_rule = project_config.get_experiment_from_key(rollout.experiments[idx].get('key'))
303314

304315
# Check if user meets audience conditions for targeting rule
305-
if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger):
306-
self.logger.debug('User "%s" does not meet conditions for targeting rule %s.' % (user_id, idx + 1))
316+
audience_conditions = rollout_rule.get_audience_conditions_or_ids()
317+
if not audience_helper.does_user_meet_audience_conditions(project_config,
318+
audience_conditions,
319+
enums.RolloutRuleAudienceEvaluationLogs,
320+
logging_key,
321+
attributes,
322+
self.logger):
323+
self.logger.debug(
324+
'User "{}" does not meet conditions for targeting rule {}.'.format(user_id, logging_key))
307325
continue
308326

309-
self.logger.debug('User "%s" meets conditions for targeting rule %s.' % (user_id, idx + 1))
327+
self.logger.debug(
328+
'User "{}" meets audience conditions for targeting rule {}.'.format(user_id, idx + 1))
310329
# Determine bucketing ID to be used
311330
bucketing_id = self._get_bucketing_id(user_id, attributes)
312-
variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id)
331+
variation = self.bucketer.bucket(project_config, rollout_rule, user_id, bucketing_id)
313332
if variation:
314333
self.logger.debug(
315-
'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key)
334+
'User "{}" is in the traffic group of targeting rule {}.'.format(user_id, logging_key)
316335
)
317-
return Decision(experiment, variation, enums.DecisionSources.ROLLOUT)
336+
return Decision(rollout_rule, variation, enums.DecisionSources.ROLLOUT)
318337
else:
319338
# Evaluate no further rules
320339
self.logger.debug(
321-
'User "%s" is not in the traffic group for the targeting else. '
322-
'Checking "Everyone Else" rule now.' % user_id
340+
'User "{}" is not in the traffic group for targeting rule {}. '
341+
'Checking "Everyone Else" rule now.'.format(user_id, logging_key)
323342
)
324343
break
325344

326345
# Evaluate last rule i.e. "Everyone Else" rule
327-
everyone_else_experiment = project_config.get_experiment_from_key(rollout.experiments[-1].get('key'))
328-
if audience_helper.is_user_in_experiment(
346+
everyone_else_rule = project_config.get_experiment_from_key(rollout.experiments[-1].get('key'))
347+
audience_conditions = everyone_else_rule.get_audience_conditions_or_ids()
348+
if audience_helper.does_user_meet_audience_conditions(
329349
project_config,
330-
project_config.get_experiment_from_key(rollout.experiments[-1].get('key')),
350+
audience_conditions,
351+
enums.RolloutRuleAudienceEvaluationLogs,
352+
'Everyone Else',
331353
attributes,
332-
self.logger,
354+
self.logger
333355
):
334356
# Determine bucketing ID to be used
335357
bucketing_id = self._get_bucketing_id(user_id, attributes)
336-
variation = self.bucketer.bucket(project_config, everyone_else_experiment, user_id, bucketing_id)
358+
variation = self.bucketer.bucket(project_config, everyone_else_rule, user_id, bucketing_id)
337359
if variation:
338-
self.logger.debug('User "%s" meets conditions for targeting rule "Everyone Else".' % user_id)
339-
return Decision(everyone_else_experiment, variation, enums.DecisionSources.ROLLOUT,)
360+
self.logger.debug('User "{}" meets conditions for targeting rule "Everyone Else".'.format(user_id))
361+
return Decision(everyone_else_rule, variation, enums.DecisionSources.ROLLOUT,)
340362

341363
return Decision(None, None, enums.DecisionSources.ROLLOUT)
342364

@@ -392,9 +414,6 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes
392414
variation = self.get_variation(project_config, experiment, user_id, attributes)
393415

394416
if variation:
395-
self.logger.debug(
396-
'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key)
397-
)
398417
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST)
399418
else:
400419
self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature'))
@@ -407,9 +426,6 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes
407426
variation = self.get_variation(project_config, experiment, user_id, attributes)
408427

409428
if variation:
410-
self.logger.debug(
411-
'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key)
412-
)
413429
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST)
414430

415431
# Next check if user is part of a rollout

optimizely/helpers/audience.py

+20-16
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,33 @@
1515

1616
from . import condition as condition_helper
1717
from . import condition_tree_evaluator
18-
from .enums import AudienceEvaluationLogs as audience_logs
1918

2019

21-
def is_user_in_experiment(config, experiment, attributes, logger):
20+
def does_user_meet_audience_conditions(config,
21+
audience_conditions,
22+
audience_logs,
23+
logging_key,
24+
attributes,
25+
logger):
2226
""" Determine for given experiment if user satisfies the audiences for the experiment.
2327
24-
Args:
25-
config: project_config.ProjectConfig object representing the project.
26-
experiment: Object representing the experiment.
27-
attributes: Dict representing user attributes which will be used in determining
28-
if the audience conditions are met. If not provided, default to an empty dict.
29-
logger: Provides a logger to send log messages to.
28+
Args:
29+
config: project_config.ProjectConfig object representing the project.
30+
audience_conditions: Audience conditions corresponding to the experiment or rollout rule.
31+
audience_logs: Log class capturing the messages to be logged .
32+
logging_key: String representing experiment key or rollout rule. To be used in log messages only.
33+
attributes: Dict representing user attributes which will be used in determining
34+
if the audience conditions are met. If not provided, default to an empty dict.
35+
logger: Provides a logger to send log messages to.
3036
31-
Returns:
32-
Boolean representing if user satisfies audience conditions for any of the audiences or not.
33-
"""
34-
35-
audience_conditions = experiment.get_audience_conditions_or_ids()
36-
logger.debug(audience_logs.EVALUATING_AUDIENCES_COMBINED.format(experiment.key, json.dumps(audience_conditions)))
37+
Returns:
38+
Boolean representing if user satisfies audience conditions for any of the audiences or not.
39+
"""
40+
logger.debug(audience_logs.EVALUATING_AUDIENCES_COMBINED.format(logging_key, json.dumps(audience_conditions)))
3741

3842
# Return True in case there are no audiences
3943
if audience_conditions is None or audience_conditions == []:
40-
logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(experiment.key, 'TRUE'))
44+
logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(logging_key, 'TRUE'))
4145

4246
return True
4347

@@ -71,5 +75,5 @@ def evaluate_audience(audience_id):
7175

7276
eval_result = condition_tree_evaluator.evaluate(audience_conditions, evaluate_audience)
7377
eval_result = eval_result or False
74-
logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(experiment.key, str(eval_result).upper()))
78+
logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(logging_key, str(eval_result).upper()))
7579
return eval_result

optimizely/helpers/condition.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, 2018-2019, Optimizely
1+
# Copyright 2016, 2018-2020, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -17,7 +17,7 @@
1717
from six import string_types
1818

1919
from . import validator
20-
from .enums import AudienceEvaluationLogs as audience_logs
20+
from .enums import CommonAudienceEvaluationLogs as audience_logs
2121

2222

2323
class ConditionOperatorTypes(object):

optimizely/helpers/enums.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2019, Optimizely
1+
# Copyright 2016-2020, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -14,11 +14,9 @@
1414
import logging
1515

1616

17-
class AudienceEvaluationLogs(object):
17+
class CommonAudienceEvaluationLogs(object):
1818
AUDIENCE_EVALUATION_RESULT = 'Audience "{}" evaluated to {}.'
19-
AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for experiment "{}" collectively evaluated to {}.'
2019
EVALUATING_AUDIENCE = 'Starting to evaluate audience "{}" with conditions: {}.'
21-
EVALUATING_AUDIENCES_COMBINED = 'Evaluating audiences for experiment "{}": {}.'
2220
INFINITE_ATTRIBUTE_VALUE = (
2321
'Audience condition "{}" evaluated to UNKNOWN because the number value '
2422
'for user attribute "{}" is not in the range [-2^53, +2^53].'
@@ -48,6 +46,16 @@ class AudienceEvaluationLogs(object):
4846
)
4947

5048

49+
class ExperimentAudienceEvaluationLogs(CommonAudienceEvaluationLogs):
50+
AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for experiment "{}" collectively evaluated to {}.'
51+
EVALUATING_AUDIENCES_COMBINED = 'Evaluating audiences for experiment "{}": {}.'
52+
53+
54+
class RolloutRuleAudienceEvaluationLogs(CommonAudienceEvaluationLogs):
55+
AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for rule {} collectively evaluated to {}.'
56+
EVALUATING_AUDIENCES_COMBINED = 'Evaluating audiences for rule {}: {}.'
57+
58+
5159
class ConfigManager(object):
5260
AUTHENTICATED_DATAFILE_URL_TEMPLATE = 'https://config.optimizely.com/datafiles/auth/{sdk_key}.json'
5361
AUTHORIZATION_HEADER_DATA_TEMPLATE = 'Bearer {access_token}'

0 commit comments

Comments
 (0)