Skip to content

Commit

Permalink
feat(Audience Evaluation): Audience Logging (#152)
Browse files Browse the repository at this point in the history
Summary
-------
This adds logging to audience evaluation. 

Test plan
---------
Unit tests written in 
- spec/audience_spec.rb
- spec/custom_attribute_condition_evaluator_spec.rb

Issues
------
- OASIS-3852
  • Loading branch information
rashidsp authored and nchilada committed Mar 6, 2019
1 parent 4c8b534 commit c2a0931
Show file tree
Hide file tree
Showing 7 changed files with 634 additions and 178 deletions.
56 changes: 51 additions & 5 deletions lib/optimizely/audience.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
require 'json'
require_relative './custom_attribute_condition_evaluator'
require_relative 'condition_tree_evaluator'
require_relative 'helpers/constants'

module Optimizely
module Audience
Expand All @@ -35,12 +36,31 @@ def user_in_experiment?(config, experiment, attributes)

audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']

config.logger.log(
Logger::DEBUG,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCES_COMBINED'],
experiment['key'],
audience_conditions
)
)

# Return true if there are no audiences
return true if audience_conditions.empty?
if audience_conditions.empty?
config.logger.log(
Logger::INFO,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
experiment['key'],
'TRUE'
)
)
return true
end

attributes ||= {}

custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes)
custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes, config.logger)

evaluate_custom_attr = lambda do |condition|
return custom_attr_condition_evaluator.evaluate(condition)
Expand All @@ -51,13 +71,39 @@ def user_in_experiment?(config, experiment, attributes)
return nil unless audience

audience_conditions = audience['conditions']
config.logger.log(
Logger::DEBUG,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCE'],
audience_id,
audience_conditions
)
)

audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
return ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
config.logger.log(
Logger::INFO,
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
)
result
end

return true if ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)
eval_result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)

eval_result ||= false

config.logger.log(
Logger::INFO,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
experiment['key'],
eval_result.to_s.upcase
)
)

false
eval_result
end
end
end
157 changes: 139 additions & 18 deletions lib/optimizely/custom_attribute_condition_evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
require_relative 'helpers/constants'
require_relative 'helpers/validator'

module Optimizely
Expand All @@ -38,8 +39,9 @@ class CustomAttributeConditionEvaluator

attr_reader :user_attributes

def initialize(user_attributes)
def initialize(user_attributes, logger)
@user_attributes = user_attributes
@logger = logger
end

def evaluate(leaf_condition)
Expand All @@ -51,11 +53,47 @@ def evaluate(leaf_condition)
# Returns boolean if the given user attributes match/don't match the given conditions,
# nil if the given conditions can't be evaluated.

return nil unless leaf_condition['type'] == CUSTOM_ATTRIBUTE_CONDITION_TYPE
unless leaf_condition['type'] == CUSTOM_ATTRIBUTE_CONDITION_TYPE
@logger.log(
Logger::WARN,
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_TYPE'], leaf_condition)
)
return nil
end

condition_match = leaf_condition['match'] || EXACT_MATCH_TYPE

return nil unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match)
if !@user_attributes.key?(leaf_condition['name']) && condition_match != EXISTS_MATCH_TYPE
@logger.log(
Logger::DEBUG,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['MISSING_ATTRIBUTE_VALUE'],
leaf_condition,
leaf_condition['name']
)
)
return nil
end

if @user_attributes[leaf_condition['name']].nil? && condition_match != EXISTS_MATCH_TYPE
@logger.log(
Logger::DEBUG,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['NULL_ATTRIBUTE_VALUE'],
leaf_condition,
leaf_condition['name']
)
)
return nil
end

unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match)
@logger.log(
Logger::WARN,
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_MATCH_TYPE'], leaf_condition)
)
return nil
end

send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
end
Expand All @@ -73,13 +111,40 @@ def exact_evaluator(condition)

user_provided_value = @user_attributes[condition['name']]

if user_provided_value.is_a?(Numeric) && condition_value.is_a?(Numeric)
return true if condition_value.to_f == user_provided_value.to_f
if !value_type_valid_for_exact_conditions?(condition_value) ||
(condition_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(condition_value))
@logger.log(
Logger::WARN,
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
)
return nil
end

return nil if !value_valid_for_exact_conditions?(user_provided_value) ||
!value_valid_for_exact_conditions?(condition_value) ||
!Helpers::Validator.same_types?(condition_value, user_provided_value)
if !value_type_valid_for_exact_conditions?(user_provided_value) ||
!Helpers::Validator.same_types?(condition_value, user_provided_value)
@logger.log(
Logger::WARN,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
condition,
user_provided_value.class,
condition['name']
)
)
return nil
end

if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
@logger.log(
Logger::WARN,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
condition,
condition['name']
)
)
return nil
end

condition_value == user_provided_value
end
Expand All @@ -103,8 +168,7 @@ def greater_than_evaluator(condition)
condition_value = condition['value']
user_provided_value = @user_attributes[condition['name']]

return nil if !Helpers::Validator.finite_number?(user_provided_value) ||
!Helpers::Validator.finite_number?(condition_value)
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)

user_provided_value > condition_value
end
Expand All @@ -118,8 +182,7 @@ def less_than_evaluator(condition)
condition_value = condition['value']
user_provided_value = @user_attributes[condition['name']]

return nil if !Helpers::Validator.finite_number?(user_provided_value) ||
!Helpers::Validator.finite_number?(condition_value)
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)

user_provided_value < condition_value
end
Expand All @@ -133,20 +196,78 @@ def substring_evaluator(condition)
condition_value = condition['value']
user_provided_value = @user_attributes[condition['name']]

return nil unless user_provided_value.is_a?(String) && condition_value.is_a?(String)
unless condition_value.is_a?(String)
@logger.log(
Logger::WARN,
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
)
return nil
end

unless user_provided_value.is_a?(String)
@logger.log(
Logger::WARN,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
condition,
user_provided_value.class,
condition['name']
)
)
return nil
end

user_provided_value.include? condition_value
end

private

def value_valid_for_exact_conditions?(value)
# Returns true if the value is valid for exact conditions. Valid values include
# strings, booleans, and numbers that aren't NaN, -Infinity, or Infinity.
def valid_numeric_values?(user_value, condition_value, condition)
# Returns true if user and condition values are valid numeric.
# false otherwise.

unless Helpers::Validator.finite_number?(condition_value)
@logger.log(
Logger::WARN,
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
)
return false
end

unless user_value.is_a?(Numeric)
@logger.log(
Logger::WARN,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
condition,
user_value.class,
condition['name']
)
)
return false
end

return Helpers::Validator.finite_number?(value) if value.is_a? Numeric
unless Helpers::Validator.finite_number?(user_value)
@logger.log(
Logger::WARN,
format(
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
condition,
condition['name']
)
)
return false
end

true
end

def value_type_valid_for_exact_conditions?(value)
# Returns true if the value is valid for exact conditions. Valid values include
# strings or booleans or is a number.
# false otherwise.

(Helpers::Validator.boolean? value) || (value.is_a? String)
(Helpers::Validator.boolean? value) || (value.is_a? String) || value.is_a?(Numeric)
end
end
end
21 changes: 21 additions & 0 deletions lib/optimizely/helpers/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,27 @@ module Constants
ATTRIBUTE_VALID_TYPES = [FalseClass, Float, Integer, String, TrueClass].freeze

FINITE_NUMBER_LIMIT = 2**53

AUDIENCE_EVALUATION_LOGS = {
'AUDIENCE_EVALUATION_RESULT' => "Audience '%s' evaluated to %s.",
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.",
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s.",
'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \
"for user attribute '%s' is not in the range [-2^53, +2^53].",
'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \
"was passed for user attribute '%s'.",
'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \
"for user attribute '%s'.",
'UNEXPECTED_TYPE' => "Audience condition %s evaluated as UNKNOWN because a value of type '%s' " \
"was passed for user attribute '%s'.",
'UNKNOWN_CONDITION_TYPE' => 'Audience condition %s uses an unknown condition type. You may need ' \
'to upgrade to a newer release of the Optimizely SDK.',
'UNKNOWN_CONDITION_VALUE' => 'Audience condition %s has an unsupported condition value. You may need ' \
'to upgrade to a newer release of the Optimizely SDK.',
'UNKNOWN_MATCH_TYPE' => 'Audience condition %s uses an unknown match type. You may need ' \
'to upgrade to a newer release of the Optimizely SDK.'
}.freeze
end
end
end
4 changes: 3 additions & 1 deletion lib/optimizely/helpers/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,11 @@ def boolean?(value)
def same_types?(value_1, value_2)
# Returns true if given values are of same types.
# false otherwise.
# Numeric values are considered as same type.

return true if value_1.is_a?(Numeric) && value_2.is_a?(Numeric)

return true if boolean?(value_1) && boolean?(value_2)
return true if value_1.is_a?(Integer) && value_2.is_a?(Integer)

value_1.class == value_2.class
end
Expand Down
Loading

0 comments on commit c2a0931

Please sign in to comment.