Skip to content

Commit c2a0931

Browse files
rashidspnchilada
authored andcommitted
feat(Audience Evaluation): Audience Logging (#152)
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
1 parent 4c8b534 commit c2a0931

7 files changed

+634
-178
lines changed

lib/optimizely/audience.rb

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
require 'json'
1919
require_relative './custom_attribute_condition_evaluator'
2020
require_relative 'condition_tree_evaluator'
21+
require_relative 'helpers/constants'
2122

2223
module Optimizely
2324
module Audience
@@ -35,12 +36,31 @@ def user_in_experiment?(config, experiment, attributes)
3536

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

39+
config.logger.log(
40+
Logger::DEBUG,
41+
format(
42+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCES_COMBINED'],
43+
experiment['key'],
44+
audience_conditions
45+
)
46+
)
47+
3848
# Return true if there are no audiences
39-
return true if audience_conditions.empty?
49+
if audience_conditions.empty?
50+
config.logger.log(
51+
Logger::INFO,
52+
format(
53+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
54+
experiment['key'],
55+
'TRUE'
56+
)
57+
)
58+
return true
59+
end
4060

4161
attributes ||= {}
4262

43-
custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes)
63+
custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes, config.logger)
4464

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

5373
audience_conditions = audience['conditions']
74+
config.logger.log(
75+
Logger::DEBUG,
76+
format(
77+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCE'],
78+
audience_id,
79+
audience_conditions
80+
)
81+
)
82+
5483
audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
55-
return ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
84+
result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
85+
result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
86+
config.logger.log(
87+
Logger::INFO,
88+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
89+
)
90+
result
5691
end
5792

58-
return true if ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)
93+
eval_result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)
94+
95+
eval_result ||= false
96+
97+
config.logger.log(
98+
Logger::INFO,
99+
format(
100+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
101+
experiment['key'],
102+
eval_result.to_s.upcase
103+
)
104+
)
59105

60-
false
106+
eval_result
61107
end
62108
end
63109
end

lib/optimizely/custom_attribute_condition_evaluator.rb

Lines changed: 139 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# See the License for the specific language governing permissions and
1616
# limitations under the License.
1717
#
18+
require_relative 'helpers/constants'
1819
require_relative 'helpers/validator'
1920

2021
module Optimizely
@@ -38,8 +39,9 @@ class CustomAttributeConditionEvaluator
3839

3940
attr_reader :user_attributes
4041

41-
def initialize(user_attributes)
42+
def initialize(user_attributes, logger)
4243
@user_attributes = user_attributes
44+
@logger = logger
4345
end
4446

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

54-
return nil unless leaf_condition['type'] == CUSTOM_ATTRIBUTE_CONDITION_TYPE
56+
unless leaf_condition['type'] == CUSTOM_ATTRIBUTE_CONDITION_TYPE
57+
@logger.log(
58+
Logger::WARN,
59+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_TYPE'], leaf_condition)
60+
)
61+
return nil
62+
end
5563

5664
condition_match = leaf_condition['match'] || EXACT_MATCH_TYPE
5765

58-
return nil unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match)
66+
if !@user_attributes.key?(leaf_condition['name']) && condition_match != EXISTS_MATCH_TYPE
67+
@logger.log(
68+
Logger::DEBUG,
69+
format(
70+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['MISSING_ATTRIBUTE_VALUE'],
71+
leaf_condition,
72+
leaf_condition['name']
73+
)
74+
)
75+
return nil
76+
end
77+
78+
if @user_attributes[leaf_condition['name']].nil? && condition_match != EXISTS_MATCH_TYPE
79+
@logger.log(
80+
Logger::DEBUG,
81+
format(
82+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['NULL_ATTRIBUTE_VALUE'],
83+
leaf_condition,
84+
leaf_condition['name']
85+
)
86+
)
87+
return nil
88+
end
89+
90+
unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match)
91+
@logger.log(
92+
Logger::WARN,
93+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_MATCH_TYPE'], leaf_condition)
94+
)
95+
return nil
96+
end
5997

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

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

76-
if user_provided_value.is_a?(Numeric) && condition_value.is_a?(Numeric)
77-
return true if condition_value.to_f == user_provided_value.to_f
114+
if !value_type_valid_for_exact_conditions?(condition_value) ||
115+
(condition_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(condition_value))
116+
@logger.log(
117+
Logger::WARN,
118+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
119+
)
120+
return nil
78121
end
79122

80-
return nil if !value_valid_for_exact_conditions?(user_provided_value) ||
81-
!value_valid_for_exact_conditions?(condition_value) ||
82-
!Helpers::Validator.same_types?(condition_value, user_provided_value)
123+
if !value_type_valid_for_exact_conditions?(user_provided_value) ||
124+
!Helpers::Validator.same_types?(condition_value, user_provided_value)
125+
@logger.log(
126+
Logger::WARN,
127+
format(
128+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
129+
condition,
130+
user_provided_value.class,
131+
condition['name']
132+
)
133+
)
134+
return nil
135+
end
136+
137+
if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
138+
@logger.log(
139+
Logger::WARN,
140+
format(
141+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
142+
condition,
143+
condition['name']
144+
)
145+
)
146+
return nil
147+
end
83148

84149
condition_value == user_provided_value
85150
end
@@ -103,8 +168,7 @@ def greater_than_evaluator(condition)
103168
condition_value = condition['value']
104169
user_provided_value = @user_attributes[condition['name']]
105170

106-
return nil if !Helpers::Validator.finite_number?(user_provided_value) ||
107-
!Helpers::Validator.finite_number?(condition_value)
171+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
108172

109173
user_provided_value > condition_value
110174
end
@@ -118,8 +182,7 @@ def less_than_evaluator(condition)
118182
condition_value = condition['value']
119183
user_provided_value = @user_attributes[condition['name']]
120184

121-
return nil if !Helpers::Validator.finite_number?(user_provided_value) ||
122-
!Helpers::Validator.finite_number?(condition_value)
185+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
123186

124187
user_provided_value < condition_value
125188
end
@@ -133,20 +196,78 @@ def substring_evaluator(condition)
133196
condition_value = condition['value']
134197
user_provided_value = @user_attributes[condition['name']]
135198

136-
return nil unless user_provided_value.is_a?(String) && condition_value.is_a?(String)
199+
unless condition_value.is_a?(String)
200+
@logger.log(
201+
Logger::WARN,
202+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
203+
)
204+
return nil
205+
end
206+
207+
unless user_provided_value.is_a?(String)
208+
@logger.log(
209+
Logger::WARN,
210+
format(
211+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
212+
condition,
213+
user_provided_value.class,
214+
condition['name']
215+
)
216+
)
217+
return nil
218+
end
137219

138220
user_provided_value.include? condition_value
139221
end
140222

141223
private
142224

143-
def value_valid_for_exact_conditions?(value)
144-
# Returns true if the value is valid for exact conditions. Valid values include
145-
# strings, booleans, and numbers that aren't NaN, -Infinity, or Infinity.
225+
def valid_numeric_values?(user_value, condition_value, condition)
226+
# Returns true if user and condition values are valid numeric.
227+
# false otherwise.
228+
229+
unless Helpers::Validator.finite_number?(condition_value)
230+
@logger.log(
231+
Logger::WARN,
232+
format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition)
233+
)
234+
return false
235+
end
236+
237+
unless user_value.is_a?(Numeric)
238+
@logger.log(
239+
Logger::WARN,
240+
format(
241+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
242+
condition,
243+
user_value.class,
244+
condition['name']
245+
)
246+
)
247+
return false
248+
end
146249

147-
return Helpers::Validator.finite_number?(value) if value.is_a? Numeric
250+
unless Helpers::Validator.finite_number?(user_value)
251+
@logger.log(
252+
Logger::WARN,
253+
format(
254+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'],
255+
condition,
256+
condition['name']
257+
)
258+
)
259+
return false
260+
end
261+
262+
true
263+
end
264+
265+
def value_type_valid_for_exact_conditions?(value)
266+
# Returns true if the value is valid for exact conditions. Valid values include
267+
# strings or booleans or is a number.
268+
# false otherwise.
148269

149-
(Helpers::Validator.boolean? value) || (value.is_a? String)
270+
(Helpers::Validator.boolean? value) || (value.is_a? String) || value.is_a?(Numeric)
150271
end
151272
end
152273
end

lib/optimizely/helpers/constants.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,27 @@ module Constants
331331
ATTRIBUTE_VALID_TYPES = [FalseClass, Float, Integer, String, TrueClass].freeze
332332

333333
FINITE_NUMBER_LIMIT = 2**53
334+
335+
AUDIENCE_EVALUATION_LOGS = {
336+
'AUDIENCE_EVALUATION_RESULT' => "Audience '%s' evaluated to %s.",
337+
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
338+
'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.",
339+
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s.",
340+
'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \
341+
"for user attribute '%s' is not in the range [-2^53, +2^53].",
342+
'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \
343+
"was passed for user attribute '%s'.",
344+
'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \
345+
"for user attribute '%s'.",
346+
'UNEXPECTED_TYPE' => "Audience condition %s evaluated as UNKNOWN because a value of type '%s' " \
347+
"was passed for user attribute '%s'.",
348+
'UNKNOWN_CONDITION_TYPE' => 'Audience condition %s uses an unknown condition type. You may need ' \
349+
'to upgrade to a newer release of the Optimizely SDK.',
350+
'UNKNOWN_CONDITION_VALUE' => 'Audience condition %s has an unsupported condition value. You may need ' \
351+
'to upgrade to a newer release of the Optimizely SDK.',
352+
'UNKNOWN_MATCH_TYPE' => 'Audience condition %s uses an unknown match type. You may need ' \
353+
'to upgrade to a newer release of the Optimizely SDK.'
354+
}.freeze
334355
end
335356
end
336357
end

lib/optimizely/helpers/validator.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,11 @@ def boolean?(value)
153153
def same_types?(value_1, value_2)
154154
# Returns true if given values are of same types.
155155
# false otherwise.
156+
# Numeric values are considered as same type.
157+
158+
return true if value_1.is_a?(Numeric) && value_2.is_a?(Numeric)
156159

157160
return true if boolean?(value_1) && boolean?(value_2)
158-
return true if value_1.is_a?(Integer) && value_2.is_a?(Integer)
159161

160162
value_1.class == value_2.class
161163
end

0 commit comments

Comments
 (0)