optimizely-sdk 3.5.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c89ea6be1aa10ab05c2d47e74ebe054987e6fc0e08b85417a63b579d80f2fac4
4
- data.tar.gz: 1f35f5d10017780db25f46e270d64010d0dc5e245bd4b183ae1da8ee1dfb060d
3
+ metadata.gz: 4d8204dce32c85bea56820607c0268b363fb86e7506ce685a2675645b4ef581b
4
+ data.tar.gz: dce7cd63fde5e24e179a74f8f14c6a22ed389028faac38a373af105683b596f0
5
5
  SHA512:
6
- metadata.gz: d2fd93c8952496d3feb12ec4dd8fb8ced95f7123a94447b646b14e157cdaf82af65adfc193943a6efc7ff77069d4600d5006728c876f9ff0d06d9116ad2c58a1
7
- data.tar.gz: a5ffc4c7db14a01e4bc7741eca6c636b6f41e755a7256fae64703e5241bd6ade20190c8f0b83bdc37d7cb18277439659ce09faa68388c7e74db2d6d3463d470a
6
+ metadata.gz: 5cc88f9802b38a2eb29354507303c6dcdcf6a402f721d24689f1e42a52a79319568974cf3584c6bf94398ff5f1410aacf49a1adf7aa27ef9105f3e7b23d39e3d
7
+ data.tar.gz: a6f0a7f052bb851ce760c4c88e0fe0dea18c14b42d5bcf457ac9cfefe3eaa2027f1b847c2ab0c133233618502cfe9207b1e82a5b1d65d0d4a678b58f5ea0ff88
@@ -806,15 +806,16 @@ module Optimizely
806
806
  "Got variable value '#{variable_value}' for variable '#{variable['key']}' of feature flag '#{feature_flag_key}'.")
807
807
  else
808
808
  @logger.log(Logger::DEBUG,
809
- "Variable '#{variable['key']}' is not used in variation '#{variation['key']}'. Returning the default variable value '#{variable_value}'.")
809
+ "Variable value is not defined. Returning the default variable value '#{variable_value}' for variable '#{variable['key']}'.")
810
+
810
811
  end
811
812
  else
812
813
  @logger.log(Logger::DEBUG,
813
- "Feature '#{feature_flag_key}' for variation '#{variation['key']}' is not enabled. Returning the default variable value '#{variable_value}'.")
814
+ "Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'. Returning the default variable value '#{variable_value}'.")
814
815
  end
815
816
  else
816
817
  @logger.log(Logger::INFO,
817
- "User '#{user_id}' was not bucketed into any variation for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
818
+ "User '#{user_id}' was not bucketed into experiment or rollout for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
818
819
  end
819
820
  variable_value
820
821
  end
@@ -24,23 +24,32 @@ module Optimizely
24
24
  module Audience
25
25
  module_function
26
26
 
27
- def user_in_experiment?(config, experiment, attributes, logger)
28
- # Determine for given experiment if user satisfies the audiences for the experiment.
27
+ def user_meets_audience_conditions?(config, experiment, attributes, logger, logging_hash = nil, logging_key = nil)
28
+ # Determine for given experiment/rollout rule if user satisfies the audience conditions.
29
29
  #
30
30
  # config - Representation of the Optimizely project config.
31
- # experiment - Experiment for which visitor is to be bucketed.
31
+ # experiment - Experiment/Rollout rule in which user is to be bucketed.
32
32
  # attributes - Hash representing user attributes which will be used in determining if
33
33
  # the audience conditions are met.
34
+ # logger - Provides a logger instance.
35
+ # logging_hash - Optional string representing logs hash inside Helpers::Constants.
36
+ # This defaults to 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'.
37
+ # logging_key - Optional string to be logged as an identifier of experiment under evaluation.
38
+ # This defaults to experiment['key'].
34
39
  #
35
40
  # Returns boolean representing if user satisfies audience conditions for the audiences or not.
41
+ logging_hash ||= 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'
42
+ logging_key ||= experiment['key']
43
+
44
+ logs_hash = Object.const_get "Optimizely::Helpers::Constants::#{logging_hash}"
36
45
 
37
46
  audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
38
47
 
39
48
  logger.log(
40
49
  Logger::DEBUG,
41
50
  format(
42
- Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCES_COMBINED'],
43
- experiment['key'],
51
+ logs_hash['EVALUATING_AUDIENCES_COMBINED'],
52
+ logging_key,
44
53
  audience_conditions
45
54
  )
46
55
  )
@@ -50,8 +59,8 @@ module Optimizely
50
59
  logger.log(
51
60
  Logger::INFO,
52
61
  format(
53
- Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
54
- experiment['key'],
62
+ logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'],
63
+ logging_key,
55
64
  'TRUE'
56
65
  )
57
66
  )
@@ -74,7 +83,7 @@ module Optimizely
74
83
  logger.log(
75
84
  Logger::DEBUG,
76
85
  format(
77
- Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCE'],
86
+ logs_hash['EVALUATING_AUDIENCE'],
78
87
  audience_id,
79
88
  audience_conditions
80
89
  )
@@ -85,7 +94,7 @@ module Optimizely
85
94
  result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
86
95
  logger.log(
87
96
  Logger::DEBUG,
88
- format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
97
+ format(logs_hash['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
89
98
  )
90
99
  result
91
100
  end
@@ -97,8 +106,8 @@ module Optimizely
97
106
  logger.log(
98
107
  Logger::INFO,
99
108
  format(
100
- Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'],
101
- experiment['key'],
109
+ logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'],
110
+ logging_key,
102
111
  eval_result.to_s.upcase
103
112
  )
104
113
  )
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2017, 2019 Optimizely and contributors
4
+ # Copyright 2016-2017, 2019-2020 Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -39,7 +39,7 @@ module Optimizely
39
39
  # Determines ID of variation to be shown for a given experiment key and user ID.
40
40
  #
41
41
  # project_config - Instance of ProjectConfig
42
- # experiment - Experiment for which visitor is to be bucketed.
42
+ # experiment - Experiment or Rollout rule for which visitor is to be bucketed.
43
43
  # bucketing_id - String A customer-assigned value used to generate the bucketing key
44
44
  # user_id - String ID for user.
45
45
  #
@@ -47,6 +47,7 @@ module Optimizely
47
47
  return nil if experiment.nil?
48
48
 
49
49
  # check if experiment is in a group; if so, check if user is bucketed into specified experiment
50
+ # this will not affect evaluation of rollout rules.
50
51
  experiment_id = experiment['id']
51
52
  experiment_key = experiment['key']
52
53
  group_id = experiment['groupId']
@@ -82,11 +83,6 @@ module Optimizely
82
83
  variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
83
84
  if variation_id && variation_id != ''
84
85
  variation = project_config.get_variation_from_id(experiment_key, variation_id)
85
- variation_key = variation ? variation['key'] : nil
86
- @logger.log(
87
- Logger::INFO,
88
- "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
89
- )
90
86
  return variation
91
87
  end
92
88
 
@@ -98,7 +94,6 @@ module Optimizely
98
94
  )
99
95
  end
100
96
 
101
- @logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
102
97
  nil
103
98
  end
104
99
 
@@ -24,6 +24,7 @@ module Optimizely
24
24
  RUNNING_EXPERIMENT_STATUS = ['Running'].freeze
25
25
  RESERVED_ATTRIBUTE_PREFIX = '$opt_'
26
26
 
27
+ attr_reader :datafile
27
28
  attr_reader :account_id
28
29
  attr_reader :attributes
29
30
  attr_reader :audiences
@@ -62,6 +63,7 @@ module Optimizely
62
63
 
63
64
  config = JSON.parse(datafile)
64
65
 
66
+ @datafile = datafile
65
67
  @error_handler = error_handler
66
68
  @logger = logger
67
69
  @version = config['version']
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2019, Optimizely and contributors
4
+ # Copyright 2019-2020, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -15,8 +15,10 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
+ require_relative 'exceptions'
18
19
  require_relative 'helpers/constants'
19
20
  require_relative 'helpers/validator'
21
+ require_relative 'semantic_version'
20
22
 
21
23
  module Optimizely
22
24
  class CustomAttributeConditionEvaluator
@@ -26,15 +28,29 @@ module Optimizely
26
28
  EXACT_MATCH_TYPE = 'exact'
27
29
  EXISTS_MATCH_TYPE = 'exists'
28
30
  GREATER_THAN_MATCH_TYPE = 'gt'
31
+ GREATER_EQUAL_MATCH_TYPE = 'ge'
29
32
  LESS_THAN_MATCH_TYPE = 'lt'
33
+ LESS_EQUAL_MATCH_TYPE = 'le'
30
34
  SUBSTRING_MATCH_TYPE = 'substring'
35
+ SEMVER_EQ = 'semver_eq'
36
+ SEMVER_GE = 'semver_ge'
37
+ SEMVER_GT = 'semver_gt'
38
+ SEMVER_LE = 'semver_le'
39
+ SEMVER_LT = 'semver_lt'
31
40
 
32
41
  EVALUATORS_BY_MATCH_TYPE = {
33
42
  EXACT_MATCH_TYPE => :exact_evaluator,
34
43
  EXISTS_MATCH_TYPE => :exists_evaluator,
35
44
  GREATER_THAN_MATCH_TYPE => :greater_than_evaluator,
45
+ GREATER_EQUAL_MATCH_TYPE => :greater_than_or_equal_evaluator,
36
46
  LESS_THAN_MATCH_TYPE => :less_than_evaluator,
37
- SUBSTRING_MATCH_TYPE => :substring_evaluator
47
+ LESS_EQUAL_MATCH_TYPE => :less_than_or_equal_evaluator,
48
+ SUBSTRING_MATCH_TYPE => :substring_evaluator,
49
+ SEMVER_EQ => :semver_equal_evaluator,
50
+ SEMVER_GE => :semver_greater_than_or_equal_evaluator,
51
+ SEMVER_GT => :semver_greater_than_evaluator,
52
+ SEMVER_LE => :semver_less_than_or_equal_evaluator,
53
+ SEMVER_LT => :semver_less_than_evaluator
38
54
  }.freeze
39
55
 
40
56
  attr_reader :user_attributes
@@ -95,7 +111,35 @@ module Optimizely
95
111
  return nil
96
112
  end
97
113
 
98
- send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
114
+ begin
115
+ send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
116
+ rescue InvalidAttributeType
117
+ condition_name = leaf_condition['name']
118
+ user_value = @user_attributes[condition_name]
119
+
120
+ @logger.log(
121
+ Logger::WARN,
122
+ format(
123
+ Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
124
+ leaf_condition,
125
+ user_value.class,
126
+ condition_name
127
+ )
128
+ )
129
+ return nil
130
+ rescue InvalidSemanticVersion
131
+ condition_name = leaf_condition['name']
132
+
133
+ @logger.log(
134
+ Logger::WARN,
135
+ format(
136
+ Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INVALID_SEMANTIC_VERSION'],
137
+ leaf_condition,
138
+ condition_name
139
+ )
140
+ )
141
+ return nil
142
+ end
99
143
  end
100
144
 
101
145
  def exact_evaluator(condition)
@@ -122,16 +166,7 @@ module Optimizely
122
166
 
123
167
  if !value_type_valid_for_exact_conditions?(user_provided_value) ||
124
168
  !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
169
+ raise InvalidAttributeType
135
170
  end
136
171
 
137
172
  if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
@@ -173,6 +208,20 @@ module Optimizely
173
208
  user_provided_value > condition_value
174
209
  end
175
210
 
211
+ def greater_than_or_equal_evaluator(condition)
212
+ # Evaluate the given greater than or equal match condition for the given user attributes.
213
+ # Returns boolean true if the user attribute value is greater than or equal to the condition value,
214
+ # false if the user attribute value is less than the condition value,
215
+ # nil if the condition value isn't a number or the user attribute value isn't a number.
216
+
217
+ condition_value = condition['value']
218
+ user_provided_value = @user_attributes[condition['name']]
219
+
220
+ return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
221
+
222
+ user_provided_value >= condition_value
223
+ end
224
+
176
225
  def less_than_evaluator(condition)
177
226
  # Evaluate the given less than match condition for the given user attributes.
178
227
  # Returns boolean true if the user attribute value is less than the condition value,
@@ -187,6 +236,20 @@ module Optimizely
187
236
  user_provided_value < condition_value
188
237
  end
189
238
 
239
+ def less_than_or_equal_evaluator(condition)
240
+ # Evaluate the given less than or equal match condition for the given user attributes.
241
+ # Returns boolean true if the user attribute value is less than or equal to the condition value,
242
+ # false if the user attribute value is greater than the condition value,
243
+ # nil if the condition value isn't a number or the user attribute value isn't a number.
244
+
245
+ condition_value = condition['value']
246
+ user_provided_value = @user_attributes[condition['name']]
247
+
248
+ return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
249
+
250
+ user_provided_value <= condition_value
251
+ end
252
+
190
253
  def substring_evaluator(condition)
191
254
  # Evaluate the given substring match condition for the given user attributes.
192
255
  # Returns boolean true if the condition value is a substring of the user attribute value,
@@ -204,22 +267,66 @@ module Optimizely
204
267
  return nil
205
268
  end
206
269
 
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
270
+ raise InvalidAttributeType unless user_provided_value.is_a?(String)
219
271
 
220
272
  user_provided_value.include? condition_value
221
273
  end
222
274
 
275
+ def semver_equal_evaluator(condition)
276
+ # Evaluate the given semantic version equal match target version for the user version.
277
+ # Returns boolean true if the user version is equal to the target version,
278
+ # false if the user version is not equal to the target version
279
+
280
+ target_version = condition['value']
281
+ user_version = @user_attributes[condition['name']]
282
+
283
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version).zero?
284
+ end
285
+
286
+ def semver_greater_than_evaluator(condition)
287
+ # Evaluate the given semantic version greater than match target version for the user version.
288
+ # Returns boolean true if the user version is greater than the target version,
289
+ # false if the user version is less than or equal to the target version
290
+
291
+ target_version = condition['value']
292
+ user_version = @user_attributes[condition['name']]
293
+
294
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version).positive?
295
+ end
296
+
297
+ def semver_greater_than_or_equal_evaluator(condition)
298
+ # Evaluate the given semantic version greater than or equal to match target version for the user version.
299
+ # Returns boolean true if the user version is greater than or equal to the target version,
300
+ # false if the user version is less than the target version
301
+
302
+ target_version = condition['value']
303
+ user_version = @user_attributes[condition['name']]
304
+
305
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version) >= 0
306
+ end
307
+
308
+ def semver_less_than_evaluator(condition)
309
+ # Evaluate the given semantic version less than match target version for the user version.
310
+ # Returns boolean true if the user version is less than the target version,
311
+ # false if the user version is greater than or equal to the target version
312
+
313
+ target_version = condition['value']
314
+ user_version = @user_attributes[condition['name']]
315
+
316
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version).negative?
317
+ end
318
+
319
+ def semver_less_than_or_equal_evaluator(condition)
320
+ # Evaluate the given semantic version less than or equal to match target version for the user version.
321
+ # Returns boolean true if the user version is less than or equal to the target version,
322
+ # false if the user version is greater than the target version
323
+
324
+ target_version = condition['value']
325
+ user_version = @user_attributes[condition['name']]
326
+
327
+ SemanticVersion.compare_user_version_with_target_version(target_version, user_version) <= 0
328
+ end
329
+
223
330
  private
224
331
 
225
332
  def valid_numeric_values?(user_value, condition_value, condition)
@@ -234,18 +341,7 @@ module Optimizely
234
341
  return false
235
342
  end
236
343
 
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
344
+ raise InvalidAttributeType unless user_value.is_a?(Numeric)
249
345
 
250
346
  unless Helpers::Validator.finite_number?(user_value)
251
347
  @logger.log(
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2017-2019, Optimizely and contributors
4
+ # Copyright 2017-2020, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -94,7 +94,7 @@ module Optimizely
94
94
  end
95
95
 
96
96
  # Check audience conditions
97
- unless Audience.user_in_experiment?(project_config, experiment, attributes, @logger)
97
+ unless Audience.user_meets_audience_conditions?(project_config, experiment, attributes, @logger)
98
98
  @logger.log(
99
99
  Logger::INFO,
100
100
  "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
@@ -106,6 +106,16 @@ module Optimizely
106
106
  variation = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
107
107
  variation_id = variation ? variation['id'] : nil
108
108
 
109
+ if variation_id
110
+ variation_key = variation['key']
111
+ @logger.log(
112
+ Logger::INFO,
113
+ "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
114
+ )
115
+ else
116
+ @logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
117
+ end
118
+
109
119
  # Persist bucketing decision
110
120
  save_user_profile(user_profile, experiment_id, variation_id)
111
121
  variation_id
@@ -125,21 +135,9 @@ module Optimizely
125
135
  decision = get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes)
126
136
  return decision unless decision.nil?
127
137
 
128
- feature_flag_key = feature_flag['key']
129
138
  decision = get_variation_for_feature_rollout(project_config, feature_flag, user_id, attributes)
130
- if decision
131
- @logger.log(
132
- Logger::INFO,
133
- "User '#{user_id}' is bucketed into a rollout for feature flag '#{feature_flag_key}'."
134
- )
135
- return decision
136
- end
137
- @logger.log(
138
- Logger::INFO,
139
- "User '#{user_id}' is not bucketed into a rollout for feature flag '#{feature_flag_key}'."
140
- )
141
139
 
142
- nil
140
+ decision
143
141
  end
144
142
 
145
143
  def get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes = nil)
@@ -178,10 +176,7 @@ module Optimizely
178
176
  next unless variation_id
179
177
 
180
178
  variation = project_config.variation_id_map[experiment_key][variation_id]
181
- @logger.log(
182
- Logger::INFO,
183
- "The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
184
- )
179
+
185
180
  return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST'])
186
181
  end
187
182
 
@@ -231,20 +226,23 @@ module Optimizely
231
226
  # Go through each experiment in order and try to get the variation for the user
232
227
  number_of_rules.times do |index|
233
228
  rollout_rule = rollout_rules[index]
234
- audience_id = rollout_rule['audienceIds'][0]
235
- audience = project_config.get_audience_from_id(audience_id)
236
- audience_name = audience['name']
229
+ logging_key = index + 1
237
230
 
238
231
  # Check that user meets audience conditions for targeting rule
239
- unless Audience.user_in_experiment?(project_config, rollout_rule, attributes, @logger)
232
+ unless Audience.user_meets_audience_conditions?(project_config, rollout_rule, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
240
233
  @logger.log(
241
234
  Logger::DEBUG,
242
- "User '#{user_id}' does not meet the conditions to be in rollout rule for audience '#{audience_name}'."
235
+ "User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
243
236
  )
244
237
  # move onto the next targeting rule
245
238
  next
246
239
  end
247
240
 
241
+ @logger.log(
242
+ Logger::DEBUG,
243
+ "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
244
+ )
245
+
248
246
  # Evaluate if user satisfies the traffic allocation for this rollout rule
249
247
  variation = @bucketer.bucket(project_config, rollout_rule, bucketing_id, user_id)
250
248
  return Decision.new(rollout_rule, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?
@@ -254,17 +252,20 @@ module Optimizely
254
252
 
255
253
  # get last rule which is the everyone else rule
256
254
  everyone_else_experiment = rollout_rules[number_of_rules]
255
+ logging_key = 'Everyone Else'
257
256
  # Check that user meets audience conditions for last rule
258
- unless Audience.user_in_experiment?(project_config, everyone_else_experiment, attributes, @logger)
259
- audience_id = everyone_else_experiment['audienceIds'][0]
260
- audience = project_config.get_audience_from_id(audience_id)
261
- audience_name = audience['name']
257
+ unless Audience.user_meets_audience_conditions?(project_config, everyone_else_experiment, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
262
258
  @logger.log(
263
259
  Logger::DEBUG,
264
- "User '#{user_id}' does not meet the conditions to be in rollout rule for audience '#{audience_name}'."
260
+ "User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
265
261
  )
266
262
  return nil
267
263
  end
264
+
265
+ @logger.log(
266
+ Logger::DEBUG,
267
+ "User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
268
+ )
268
269
  variation = @bucketer.bucket(project_config, everyone_else_experiment, bucketing_id, user_id)
269
270
  return Decision.new(everyone_else_experiment, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?
270
271
 
@@ -120,4 +120,20 @@ module Optimizely
120
120
  super("Optimizely instance is not valid. Failing '#{aborted_method}'.")
121
121
  end
122
122
  end
123
+
124
+ class InvalidAttributeType < Error
125
+ # Raised when an attribute is not provided in expected type.
126
+
127
+ def initialize(msg = 'Provided attribute value is not in the expected data type.')
128
+ super
129
+ end
130
+ end
131
+
132
+ class InvalidSemanticVersion < Error
133
+ # Raised when an invalid value is provided as semantic version.
134
+
135
+ def initialize(msg = 'Provided semantic version is invalid.')
136
+ super
137
+ end
138
+ end
123
139
  end
@@ -335,11 +335,11 @@ module Optimizely
335
335
 
336
336
  AUDIENCE_EVALUATION_LOGS = {
337
337
  'AUDIENCE_EVALUATION_RESULT' => "Audience '%s' evaluated to %s.",
338
- 'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
339
338
  'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.",
340
- 'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s.",
341
339
  'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \
342
340
  "for user attribute '%s' is not in the range [-2^53, +2^53].",
341
+ 'INVALID_SEMANTIC_VERSION' => 'Audience condition %s evaluated as UNKNOWN because an invalid semantic version ' \
342
+ "was passed for user attribute '%s'.",
343
343
  'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \
344
344
  "was passed for user attribute '%s'.",
345
345
  'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \
@@ -354,6 +354,16 @@ module Optimizely
354
354
  'to upgrade to a newer release of the Optimizely SDK.'
355
355
  }.freeze
356
356
 
357
+ EXPERIMENT_AUDIENCE_EVALUATION_LOGS = {
358
+ 'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
359
+ 'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s."
360
+ }.merge(AUDIENCE_EVALUATION_LOGS).freeze
361
+
362
+ ROLLOUT_AUDIENCE_EVALUATION_LOGS = {
363
+ 'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for rule '%s' collectively evaluated to %s.",
364
+ 'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for rule '%s': %s."
365
+ }.merge(AUDIENCE_EVALUATION_LOGS).freeze
366
+
357
367
  DECISION_NOTIFICATION_TYPES = {
358
368
  'AB_TEST' => 'ab-test',
359
369
  'FEATURE' => 'feature',
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019, Optimizely and contributors
3
+ # Copyright 2019-2020, Optimizely and contributors
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ module Optimizely
25
25
  experiments_map_object = experiments_map
26
26
  features_map = get_features_map(experiments_map_object)
27
27
  {
28
+ 'datafile' => @project_config.datafile,
28
29
  'experimentsMap' => experiments_map_object,
29
30
  'featuresMap' => features_map,
30
31
  'revision' => @project_config.revision
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2016-2019, Optimizely and contributors
3
+ # Copyright 2016-2020, Optimizely and contributors
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ module Optimizely
20
20
  # ProjectConfig is an interface capturing the experiment, variation and feature definitions.
21
21
  # The default implementation of ProjectConfig can be found in DatafileProjectConfig.
22
22
 
23
+ def datafile; end
24
+
23
25
  def account_id; end
24
26
 
25
27
  def attributes; end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2020, Optimizely and contributors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require_relative 'exceptions'
20
+
21
+ module Optimizely
22
+ module SemanticVersion
23
+ # Semantic Version Operators
24
+ SEMVER_PRE_RELEASE = '-'
25
+ SEMVER_BUILD = '+'
26
+
27
+ module_function
28
+
29
+ def pre_release?(target)
30
+ # Method to check if the given version is a prerelease
31
+ #
32
+ # target - String representing semantic version
33
+ #
34
+ # Returns true if the given version is a prerelease
35
+ # false if it doesn't
36
+
37
+ raise unless target.is_a? String
38
+
39
+ prerelease_index = target.index(SEMVER_PRE_RELEASE)
40
+ build_index = target.index(SEMVER_BUILD)
41
+
42
+ return false if prerelease_index.nil?
43
+ return true if build_index.nil?
44
+
45
+ # when both operators are present prerelease should precede the build operator
46
+ prerelease_index < build_index
47
+ end
48
+
49
+ def build?(target)
50
+ # Method to check if the given version is a build
51
+ #
52
+ # target - String representing semantic version
53
+ #
54
+ # Returns true if the given version is a build
55
+ # false if it doesn't
56
+
57
+ raise unless target.is_a? String
58
+
59
+ prerelease_index = target.index(SEMVER_PRE_RELEASE)
60
+ build_index = target.index(SEMVER_BUILD)
61
+
62
+ return false if build_index.nil?
63
+ return true if prerelease_index.nil?
64
+
65
+ # when both operators are present build should precede the prerelease operator
66
+ build_index < prerelease_index
67
+ end
68
+
69
+ def split_semantic_version(target)
70
+ # Method to split the given version.
71
+ #
72
+ # target - String representing semantic version
73
+ #
74
+ # Returns List The array of version split into smaller parts i.e major, minor, patch etc,
75
+ # Exception if the given version is invalid.
76
+
77
+ target_prefix = target
78
+ target_suffix = ''
79
+ target_parts = []
80
+
81
+ raise InvalidSemanticVersion if target.include? ' '
82
+
83
+ if pre_release?(target)
84
+ target_parts = target.split(SEMVER_PRE_RELEASE, 2)
85
+ elsif build? target
86
+ target_parts = target.split(SEMVER_BUILD, 2)
87
+ end
88
+
89
+ unless target_parts.empty?
90
+ target_prefix = target_parts[0].to_s
91
+ target_suffix = target_parts[1..-1]
92
+ end
93
+
94
+ # expect a version string of the form x.y.z
95
+ dot_count = target_prefix.count('.')
96
+ raise InvalidSemanticVersion if dot_count > 2
97
+
98
+ target_version_parts = target_prefix.split('.')
99
+ raise InvalidSemanticVersion if target_version_parts.length != dot_count + 1
100
+
101
+ target_version_parts.each do |part|
102
+ raise InvalidSemanticVersion unless Helpers::Validator.string_numeric? part
103
+ end
104
+
105
+ target_version_parts.concat(target_suffix) if target_suffix.is_a?(Array)
106
+
107
+ target_version_parts
108
+ end
109
+
110
+ def compare_user_version_with_target_version(target_version, user_version)
111
+ # Compares target and user versions
112
+ #
113
+ # target_version - String representing target version
114
+ # user_version - String representing user version
115
+
116
+ # Returns boolean 0 if user version is equal to target version,
117
+ # 1 if user version is greater than target version,
118
+ # -1 if user version is less than target version.
119
+
120
+ raise InvalidAttributeType unless target_version.is_a? String
121
+ raise InvalidAttributeType unless user_version.is_a? String
122
+
123
+ is_target_version_prerelease = pre_release?(target_version)
124
+ is_user_version_prerelease = pre_release?(user_version)
125
+
126
+ target_version_parts = split_semantic_version(target_version)
127
+ user_version_parts = split_semantic_version(user_version)
128
+ user_version_parts_len = user_version_parts.length if user_version_parts
129
+
130
+ # Up to the precision of targetedVersion, expect version to match exactly.
131
+ target_version_parts.each_with_index do |_item, idx|
132
+ if user_version_parts_len <= idx
133
+ # even if they are equal at this point. if the target is a prerelease
134
+ # then user version must be greater than the pre release.
135
+ return 1 if is_target_version_prerelease
136
+
137
+ return -1
138
+
139
+ elsif !Helpers::Validator.string_numeric? user_version_parts[idx]
140
+ # compare strings
141
+ if user_version_parts[idx] < target_version_parts[idx]
142
+ return 1 if is_target_version_prerelease && !is_user_version_prerelease
143
+
144
+ return -1
145
+
146
+ elsif user_version_parts[idx] > target_version_parts[idx]
147
+ return -1 if is_user_version_prerelease && !is_target_version_prerelease
148
+
149
+ return 1
150
+ end
151
+
152
+ else
153
+ user_version_part = user_version_parts[idx].to_i
154
+ target_version_part = target_version_parts[idx].to_i
155
+
156
+ return 1 if user_version_part > target_version_part
157
+ return -1 if user_version_part < target_version_part
158
+ end
159
+ end
160
+
161
+ return -1 if is_user_version_prerelease && !is_target_version_prerelease
162
+
163
+ 0
164
+ end
165
+ end
166
+ end
@@ -17,5 +17,5 @@
17
17
  #
18
18
  module Optimizely
19
19
  CLIENT_ENGINE = 'ruby-sdk'
20
- VERSION = '3.5.0'
20
+ VERSION = '3.6.0'
21
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optimizely-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.0
4
+ version: 3.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Optimizely
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-09 00:00:00.000000000 Z
11
+ date: 2020-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -174,6 +174,7 @@ files:
174
174
  - lib/optimizely/optimizely_factory.rb
175
175
  - lib/optimizely/params.rb
176
176
  - lib/optimizely/project_config.rb
177
+ - lib/optimizely/semantic_version.rb
177
178
  - lib/optimizely/user_profile_service.rb
178
179
  - lib/optimizely/version.rb
179
180
  homepage: https://www.optimizely.com/