-
Notifications
You must be signed in to change notification settings - Fork 35
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
feat(Audience Evaluation): Audience Logging #156
Conversation
4 similar comments
optimizely/helpers/audience.py
Outdated
if audience_conditions is None or audience_conditions == []: | ||
logger.info(logs.NO_AUDIENCE_ATTACHED.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to log why it evaluates to True in this case. Not in doc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea to specifically mention the "no audiences" case. But to reduce the number of types of log messages, and to increase consistency with other SDKs, can we find a way to rely on the EVALUATING_AUDIENCES
and AUDIENCE_EVALUATION_RESULT_COMBINED
messages instead?
optimizely/helpers/audience.py
Outdated
json.dumps(audience_conditions) | ||
)) | ||
|
||
logger.debug(logs.USER_ATTRIBUTES.format(json.dumps(attributes))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc sort of suggests that we log attributes with every audience evaluation. I have logged it here at top-level since attributes remain same irrespective of audience conditions
optimizely/helpers/condition.py
Outdated
not validator.are_values_same_type(condition_value, user_value): | ||
return None | ||
if not validator.are_values_same_type(condition_value, user_value): | ||
self.logger.debug(logs.MISMATCH_TYPE.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this log to distinguish exactly what is inapplicable and what is type mismatch for exact evaluator.
Not in docs.
For exact, user value {}, [], () are inapplicable but integer 5 is a mismatch if condition value is a string
self.mock_client_logger = mock.MagicMock() | ||
|
||
def test_evaluate__match_type__invalid(self): | ||
log_level = 'warning' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added log_level variable in every testcase to make the expected logging level visible.
optimizely/helpers/audience.py
Outdated
audience.conditionStructure, | ||
lambda index: evaluate_custom_attr(audienceId, index) | ||
) | ||
|
||
result_log = str(result) if result is not None else 'UNKNOWN' | ||
logger.debug(logs.AUDIENCE_EVALUATION_RESULT.format(audienceId, result_log)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc suggests that this should be info. I have made it debug to avoid logs being too verbose.
At info level we should be interested in the overall result, which we are already logging outside on line#87
…python-sdk into oakbani/logging-for-audience
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this and sorry for the late review!
optimizely/helpers/audience.py
Outdated
if audience_conditions is None or audience_conditions == []: | ||
logger.info(logs.NO_AUDIENCE_ATTACHED.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea to specifically mention the "no audiences" case. But to reduce the number of types of log messages, and to increase consistency with other SDKs, can we find a way to rely on the EVALUATING_AUDIENCES
and AUDIENCE_EVALUATION_RESULT_COMBINED
messages instead?
optimizely/helpers/enums.py
Outdated
class AudienceEvaluationLogs(object): | ||
AUDIENCE_EVALUATION_RESULT = 'Audience "{}" evaluated to {}.' | ||
AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for experiment "{}" collectively evaluated to {}.' | ||
EVALUATING_AUDIENCES = 'Evaluating audiences for experiment "{}": "{}".' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To match the changes we made in optimizely/javascript-sdk#210
EVALUATING_AUDIENCES = 'Evaluating audiences for experiment "{}": "{}".' | |
EVALUATING_AUDIENCES_COMBINED = 'Evaluating audiences for experiment "{}": {}.' |
optimizely/helpers/enums.py
Outdated
AUDIENCE_EVALUATION_RESULT = 'Audience "{}" evaluated to {}.' | ||
AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for experiment "{}" collectively evaluated to {}.' | ||
EVALUATING_AUDIENCES = 'Evaluating audiences for experiment "{}": "{}".' | ||
EVALUATING_AUDIENCE_WITH_CONDITIONS = 'Starting to evaluate audience "{}" with conditions: "{}".' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To match the changes we made in optimizely/javascript-sdk#210
EVALUATING_AUDIENCE_WITH_CONDITIONS = 'Starting to evaluate audience "{}" with conditions: "{}".' | |
EVALUATING_AUDIENCE = 'Evaluating audiences for experiment "{}": {}.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renamed the key. The existing message is same as in JS SDK https://github.com/optimizely/javascript-sdk/pull/210/files#diff-79c0858a5dc2611f6570afc1265258daR127
optimizely/helpers/audience.py
Outdated
@@ -49,14 +63,28 @@ def evaluate_audience(audienceId): | |||
if audience is None: | |||
return None | |||
|
|||
return condition_tree_evaluator.evaluate( | |||
logger.debug(logs.EVALUATING_AUDIENCE_WITH_CONDITIONS.format(audienceId, audience.conditions)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logger.debug(logs.EVALUATING_AUDIENCE_WITH_CONDITIONS.format(audienceId, audience.conditions)) | |
logger.debug(logs.EVALUATING_AUDIENCE_WITH_CONDITIONS.format(audienceId, json.dumps(audience.conditions))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
audience.conditions are already JSON string encoded. https://github.com/optimizely/python-sdk/blob/master/optimizely/project_config.py#L73
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thanks!
optimizely/helpers/audience.py
Outdated
audience.conditionStructure, | ||
lambda index: evaluate_custom_attr(audienceId, index) | ||
) | ||
|
||
result_str = str(result) if result is not None else 'UNKNOWN' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
result_str = str(result) if result is not None else 'UNKNOWN' | |
result_str = str(result).upper() if result is not None else 'UNKNOWN' |
optimizely/helpers/condition.py
Outdated
not validator.are_values_same_type(condition_value, user_value): | ||
return None | ||
self.logger.debug(logs.UNEXPECTED_TYPE.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.logger.debug(logs.UNEXPECTED_TYPE.format( | |
self.logger.warning(logs.UNEXPECTED_TYPE.format( |
optimizely/helpers/condition.py
Outdated
|
||
if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value): | ||
if not validator.is_finite_number(user_value): | ||
self.logger.debug(logs.UNEXPECTED_TYPE.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.logger.debug(logs.UNEXPECTED_TYPE.format( | |
self.logger.warning(logs.UNEXPECTED_TYPE.format( |
optimizely/helpers/condition.py
Outdated
|
||
if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value): | ||
if not validator.is_finite_number(user_value): | ||
self.logger.debug(logs.UNEXPECTED_TYPE.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.logger.debug(logs.UNEXPECTED_TYPE.format( | |
self.logger.warning(logs.UNEXPECTED_TYPE.format( |
optimizely/helpers/condition.py
Outdated
return None | ||
|
||
if not isinstance(user_value, string_types): | ||
self.logger.debug(logs.UNEXPECTED_TYPE.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.logger.debug(logs.UNEXPECTED_TYPE.format( | |
self.logger.warning(logs.UNEXPECTED_TYPE.format( |
optimizely/helpers/condition.py
Outdated
return None | ||
|
||
if self.attributes.get(attribute_key) is None: | ||
self.logger.warning(logs.NULL_ATTRIBUTE_VALUE.format(self._get_condition_log(index), attribute_key)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.logger.warning(logs.NULL_ATTRIBUTE_VALUE.format(self._get_condition_log(index), attribute_key)) | |
self.logger.debug(logs.NULL_ATTRIBUTE_VALUE.format(self._get_condition_log(index), attribute_key)) |
@nchilada Feedback addressed. The build is failing only for 3.4 due to some unrelated issue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code and tests are looking really good! @aliabbasrizvi this will be ready for final review and merging after the next round of updates.
optimizely/helpers/condition.py
Outdated
return None | ||
if not self.is_value_type_valid_for_exact_conditions(condition_value) or \ | ||
self.is_value_a_number(condition_value) and \ | ||
not validator.is_finite_number(condition_value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use parens to explicitly group the latter two conditions?
(self.is_value_a_number(condition_value) and
not validator.is_finite_number(condition_value):
optimizely/helpers/condition.py
Outdated
self.logger.warning(logs.UNKNOWN_CONDITION_VALUE.format( | ||
self._get_condition_json(index) | ||
)) | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: it would be more conventional to indent these two statements by only 2 spaces.
optimizely/helpers/enums.py
Outdated
@@ -17,21 +17,20 @@ | |||
class AudienceEvaluationLogs(object): | |||
AUDIENCE_EVALUATION_RESULT = 'Audience "{}" evaluated to {}.' | |||
AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for experiment "{}" collectively evaluated to {}.' | |||
EVALUATING_AUDIENCES = 'Evaluating audiences for experiment "{}": "{}".' | |||
EVALUATING_AUDIENCE_WITH_CONDITIONS = 'Starting to evaluate audience "{}" with conditions: "{}".' | |||
EVALUATING_AUDIENCE = 'Starting to evaluate audience "{}" with conditions: "{}".' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we actually remove the surrounding quotation marks? That's what we ended up going with in other SDKs although we included quotes in the original design spec. Sorry!
EVALUATING_AUDIENCE = 'Starting to evaluate audience "{}" with conditions: "{}".' | |
EVALUATING_AUDIENCE = 'Starting to evaluate audience "{}" with conditions: {}.' |
tests/helpers_tests/test_audience.py
Outdated
experiment.audienceIds = [] | ||
experiment.audienceConditions = [] | ||
|
||
with mock.patch('optimizely.logger.reset_logger', return_value=self.mock_client_logger): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: does optimizely.logger.reset_logger
actually need to return self.mock_client_logger
given that we are passing self.mock_client_logger
directly to the function we're testing, on the line below?
Same question elsewhere in the new tests.
tests/helpers_tests/test_audience.py
Outdated
|
||
self.mock_client_logger.assert_has_calls([ | ||
mock.call.debug('Evaluating audiences for experiment "test_experiment": ["11154", "11159"].'), | ||
mock.call.debug('Starting to evaluate audience "11154" with conditions: "' + audience_11154.conditions + '".'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to change this to
mock.call.debug('Starting to evaluate audience "11154" with conditions: "' + audience_11154.conditions + '".'), | |
mock.call.debug('Starting to evaluate audience "11154" with conditions: ' + audience_11154.conditions + '.'), |
per the suggestion that I've made in enums.py. Or even better, IMO:
mock.call.debug('Starting to evaluate audience "11154" with conditions: "' + audience_11154.conditions + '".'), | |
mock.call.debug('Starting to evaluate audience "11154" with conditions: ["and", ["or", ["or", ' | |
'{"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]].'), |
(Same thing below)
mock_log.assert_called_once_with(( | ||
'Audience condition "{}" uses an unknown match ' | ||
'type. You may need to upgrade to a newer release of the Optimizely SDK.') | ||
.format(json.dumps(expected_condition_log))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd still prefer to avoid using .format
in our assertions since it's an ugly mirror, but it does help that we're only using it for the conditions now and not for other template variables in our log messages. I guess this is okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, for consistent unit tests we do need to use format in cases where representation varies across different python versions. Same for variable types.
|
||
self.assertStrictFalse(evaluator.evaluate(0)) | ||
|
||
self.mock_client_logger.debug.assert_not_called() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also confirm that info
and warning
weren't called?
|
||
self.assertStrictFalse(evaluator.evaluate(0)) | ||
|
||
self.mock_client_logger.debug.assert_not_called() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also confirm that info
and warning
weren't called?
mock_log = getattr(self.mock_client_logger, log_level) | ||
mock_log.assert_called_once_with(( | ||
'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed for ' | ||
'user attribute "favorite_constellation".').format(json.dumps(expected_condition_log), type({}))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be really nice to inline the observed type into our assertion string. Both here and below. (As long as it works for both Python 2 and Python 3... might be tricky if we want to test the gt
and lt
match types with string-valued user attributes.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We get different outputs in python 2 and 3 so can't hardcode type in string.
AssertionError: Expected call: warning('Audience condition "{"name": "meters_travelled", "value": 48, "type": "custom_attribute", "match": "gt"}" evaluated to UNKNOWN because a value of type "<type 'str'>" was passed for user attribute "meters_travelled".')
Actual call: warning('Audience condition "{"name": "meters_travelled", "value": 48, "type": "custom_attribute", "match": "gt"}" evaluated to UNKNOWN because a value of type "<class 'str'>" was passed for user attribute "meters_travelled".')
Thanks @nchilada . I am doing a review as well. |
.travis.yml
Outdated
@@ -25,7 +25,7 @@ jobs: | |||
- stage: 'Linting' | |||
language: python | |||
python: "2.7" | |||
install: "pip install flake8" | |||
install: "pip install flake8==3.6.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put a note here why this is at 3.6.0. It will be useful for anyone else working on this file.
optimizely/helpers/audience.py
Outdated
from . import condition as condition_helper | ||
from . import condition_tree_evaluator | ||
from .enums import AudienceEvaluationLogs as logs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. ... as audience_logs
|
||
logger.debug(logs.EVALUATING_AUDIENCES_COMBINED.format( | ||
experiment.key, | ||
json.dumps(audience_conditions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second argument here I feel is unnecessary as one can figure out what the audience conditions are from the datafile and IDs anyway will not help a lot to me as a customer. I guess we have resorted to this message in other SDKs as well @nchilada ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aliabbasrizvi yeah, same message in other SDKs... Part of my reasoning was that log messages might be recorded and used for debugging after the fact, by which time the datafile may have changed. Currently it's very difficult (and it may always be unintuitive) for customers to look up the datafile that was contemporaneous with a particular log message.
optimizely/helpers/condition.py
Outdated
|
||
from six import string_types | ||
|
||
from . import validator | ||
from .enums import AudienceEvaluationLogs as logs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. ... as audience_logs
optimizely/helpers/condition.py
Outdated
""" Method to validate if the value is valid for exact match type evaluation. | ||
|
||
Args: | ||
value: Value to validate. | ||
|
||
Returns: | ||
Boolean: True if value is a string type, or a boolean, or is finite. Otherwise False. | ||
Boolean: True if value is a string type, or a boolean, or is a number. Otherwise False. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence is too verbose. True if value is a string, boolean, or number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the updates and explanations! Looks great. 🙂
Summary
This adds logging for audience evaluation.
Test plan
Unit tests written in
Issues