optimizely-sdk 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/