optimizely-sdk 2.1.1 → 3.0.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.
@@ -166,6 +166,7 @@ module Optimizely
166
166
  variation_id = get_variation(experiment_key, user_id, attributes)
167
167
 
168
168
  next unless variation_id
169
+
169
170
  variation = @config.variation_id_map[experiment_key][variation_id]
170
171
  @config.logger.log(
171
172
  Logger::INFO,
@@ -236,6 +237,7 @@ module Optimizely
236
237
  # Evaluate if user satisfies the traffic allocation for this rollout rule
237
238
  variation = @bucketer.bucket(rollout_rule, bucketing_id, user_id)
238
239
  return Decision.new(rollout_rule, variation, DECISION_SOURCE_ROLLOUT) unless variation.nil?
240
+
239
241
  break
240
242
  end
241
243
 
@@ -254,6 +256,7 @@ module Optimizely
254
256
  end
255
257
  variation = @bucketer.bucket(everyone_else_experiment, bucketing_id, user_id)
256
258
  return Decision.new(everyone_else_experiment, variation, DECISION_SOURCE_ROLLOUT) unless variation.nil?
259
+
257
260
  nil
258
261
  end
259
262
 
@@ -303,6 +306,7 @@ module Optimizely
303
306
 
304
307
  decision = user_profile[:experiment_bucket_map][experiment_id]
305
308
  return nil unless decision
309
+
306
310
  variation_id = decision[:variation_id]
307
311
  return variation_id if @config.variation_id_exists?(experiment_id, variation_id)
308
312
 
@@ -362,17 +366,18 @@ module Optimizely
362
366
  #
363
367
  # user_id - String user ID
364
368
  # attributes - Hash user attributes
365
- # By default, the bucketing ID should be the user ID
366
- bucketing_id = user_id
369
+ # Returns String representing bucketing ID if it is a String type in attributes else return user ID
367
370
 
368
- # If the bucketing ID key is defined in attributes, then use that in place of the userID
369
- if attributes && attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']].is_a?(String)
370
- unless attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']].empty?
371
- bucketing_id = attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']]
372
- @config.logger.log(Logger::DEBUG, "Setting the bucketing ID '#{bucketing_id}'")
373
- end
371
+ return user_id unless attributes
372
+
373
+ bucketing_id = attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']]
374
+
375
+ if bucketing_id
376
+ return bucketing_id if bucketing_id.is_a?(String)
377
+
378
+ @config.logger.log(Logger::WARN, 'Bucketing ID attribute is not a string. Defaulted to user ID.')
374
379
  end
375
- bucketing_id
380
+ user_id
376
381
  end
377
382
  end
378
383
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2018, Optimizely and contributors
4
+ # Copyright 2016-2019, 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,10 +15,12 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
- require_relative './audience'
19
- require_relative './params'
20
- require_relative './version'
21
- require_relative '../optimizely/helpers/event_tag_utils'
18
+ require_relative 'audience'
19
+ require_relative 'helpers/constants'
20
+ require_relative 'helpers/event_tag_utils'
21
+ require_relative 'params'
22
+ require_relative 'version'
23
+
22
24
  require 'securerandom'
23
25
 
24
26
  module Optimizely
@@ -74,9 +76,9 @@ module Optimizely
74
76
  visitor_attributes = []
75
77
 
76
78
  attributes&.keys&.each do |attribute_key|
77
- # Omit null attribute values
79
+ # Omit attribute values that are not supported by the log endpoint.
78
80
  attribute_value = attributes[attribute_key]
79
- unless attribute_value.nil?
81
+ if Helpers::Validator.attribute_valid?(attribute_key, attribute_value)
80
82
  attribute_id = @config.get_attribute_id attribute_key
81
83
  if attribute_id
82
84
  visitor_attributes.push(
@@ -111,6 +113,7 @@ module Optimizely
111
113
  anonymize_ip: @config.anonymize_ip,
112
114
  revision: @config.revision,
113
115
  client_name: CLIENT_ENGINE,
116
+ enrich_decisions: true,
114
117
  client_version: VERSION
115
118
  }
116
119
 
@@ -140,19 +143,18 @@ module Optimizely
140
143
  Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
141
144
  end
142
145
 
143
- def create_conversion_event(event_key, user_id, attributes, event_tags, experiment_variation_map)
146
+ def create_conversion_event(event, user_id, attributes, event_tags)
144
147
  # Create conversion Event to be sent to the logging endpoint.
145
148
  #
146
- # event_key - +String+ Event key representing the event which needs to be recorded.
149
+ # event - +Object+ Event which needs to be recorded.
147
150
  # user_id - +String+ ID for user.
148
151
  # attributes - +Hash+ representing user attributes and values which need to be recorded.
149
152
  # event_tags - +Hash+ representing metadata associated with the event.
150
- # experiment_variation_map - +Map+ of experiment ID to the ID of the variation that the user is bucketed into.
151
153
  #
152
154
  # Returns +Event+ encapsulating the conversion event.
153
155
 
154
156
  event_params = get_common_params(user_id, attributes)
155
- conversion_params = get_conversion_params(event_key, event_tags, experiment_variation_map)
157
+ conversion_params = get_conversion_params(event, event_tags)
156
158
  event_params[:visitors][0][:snapshots] = [conversion_params]
157
159
 
158
160
  Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
@@ -188,31 +190,20 @@ module Optimizely
188
190
  impression_event_params
189
191
  end
190
192
 
191
- def get_conversion_params(event_key, event_tags, experiment_variation_map)
193
+ def get_conversion_params(event, event_tags)
192
194
  # Creates object of params specific to conversion events
193
195
  #
194
- # event_key - +String+ Key representing the event which needs to be recorded
196
+ # event - +Object+ Event which needs to be recorded.
195
197
  # event_tags - +Hash+ Values associated with the event.
196
- # experiment_variation_map - +Hash+ Map of experiment IDs to bucketed variation IDs
197
198
  #
198
199
  # Returns +Hash+ Conversion event params
199
200
 
200
201
  single_snapshot = {}
201
- single_snapshot[:decisions] = []
202
- experiment_variation_map.each do |experiment_id, variation_id|
203
- next unless variation_id
204
- single_snapshot[:decisions].push(
205
- campaign_id: @config.experiment_id_map[experiment_id]['layerId'],
206
- experiment_id: experiment_id,
207
- variation_id: variation_id
208
- )
209
- end
210
-
211
202
  event_object = {
212
- entity_id: @config.event_key_map[event_key]['id'],
203
+ entity_id: event['id'],
213
204
  timestamp: create_timestamp,
214
205
  uuid: create_uuid,
215
- key: event_key
206
+ key: event['key']
216
207
  }
217
208
 
218
209
  if event_tags
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2018, Optimizely and contributors
4
+ # Copyright 2016-2019, 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.
@@ -327,6 +327,31 @@ module Optimizely
327
327
  'v3' => '3',
328
328
  'v4' => '4'
329
329
  }.freeze
330
+
331
+ ATTRIBUTE_VALID_TYPES = [FalseClass, Float, Integer, String, TrueClass].freeze
332
+
333
+ 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
330
355
  end
331
356
  end
332
357
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2018, Optimizely and contributors
4
+ # Copyright 2016-2019, 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.
@@ -34,6 +34,21 @@ module Optimizely
34
34
  attributes.is_a?(Hash)
35
35
  end
36
36
 
37
+ def attribute_valid?(attribute_key, attribute_value)
38
+ # Determines if provided attribute_key and attribute_value are valid.
39
+ #
40
+ # attribute_key - Variable which needs to be validated.
41
+ # attribute_value - Variable which needs to be validated.
42
+ #
43
+ # Returns boolean depending on validity of attribute_key and attribute_value.
44
+
45
+ return false unless attribute_key.is_a?(String) || attribute_key.is_a?(Symbol)
46
+
47
+ return true if (boolean? attribute_value) || (attribute_value.is_a? String)
48
+
49
+ finite_number?(attribute_value)
50
+ end
51
+
37
52
  def event_tags_valid?(event_tags)
38
53
  # Determines if provided event tags are valid.
39
54
  #
@@ -106,16 +121,54 @@ module Optimizely
106
121
  # Returns boolean True if all of the values are valid, False otherwise.
107
122
 
108
123
  return false unless variables.respond_to?(:each) && !variables.empty?
124
+
109
125
  is_valid = true
126
+ if variables.include? :user_id
127
+ # Empty str is a valid user ID.
128
+ unless variables[:user_id].is_a?(String)
129
+ is_valid = false
130
+ logger.log(level, "#{Constants::INPUT_VARIABLES['USER_ID']} is invalid")
131
+ end
132
+ variables.delete :user_id
133
+ end
134
+
110
135
  variables.each do |key, value|
111
136
  next if value.is_a?(String) && !value.empty?
137
+
112
138
  is_valid = false
113
- if logger_valid?(logger) && level
114
- logger.log(level, "#{Optimizely::Helpers::Constants::INPUT_VARIABLES[key.to_s.upcase]} is invalid")
115
- end
139
+ next unless logger_valid?(logger) && level
140
+
141
+ logger.log(level, "#{Constants::INPUT_VARIABLES[key.to_s.upcase]} is invalid")
116
142
  end
117
143
  is_valid
118
144
  end
145
+
146
+ def boolean?(value)
147
+ # Returns true if given value type is boolean.
148
+ # false otherwise.
149
+
150
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
151
+ end
152
+
153
+ def same_types?(value_1, value_2)
154
+ # Returns true if given values are of same types.
155
+ # 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)
159
+
160
+ return true if boolean?(value_1) && boolean?(value_2)
161
+
162
+ value_1.class == value_2.class
163
+ end
164
+
165
+ def finite_number?(value)
166
+ # Returns true if the given value is a number, enforces
167
+ # absolute limit of 2^53 and restricts NaN, Infinity, -Infinity.
168
+ # false otherwise.
169
+
170
+ value.is_a?(Numeric) && value.to_f.finite? && value.abs <= Constants::FINITE_NUMBER_LIMIT
171
+ end
119
172
  end
120
173
  end
121
174
  end
@@ -42,6 +42,7 @@ module Optimizely
42
42
 
43
43
  def add_notification_listener(notification_type, notification_callback)
44
44
  return nil unless notification_type_valid?(notification_type)
45
+
45
46
  unless notification_callback
46
47
  @logger.log Logger::ERROR, 'Callback can not be empty.'
47
48
  return nil
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2016-2018, Optimizely and contributors
3
+ # Copyright 2016-2019, 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.
@@ -31,6 +31,7 @@ module Optimizely
31
31
  attr_reader :account_id
32
32
  attr_reader :attributes
33
33
  attr_reader :audiences
34
+ attr_reader :typed_audiences
34
35
  attr_reader :events
35
36
  attr_reader :experiments
36
37
  attr_reader :feature_flags
@@ -79,6 +80,7 @@ module Optimizely
79
80
  @account_id = config['accountId']
80
81
  @attributes = config.fetch('attributes', [])
81
82
  @audiences = config.fetch('audiences', [])
83
+ @typed_audiences = config.fetch('typedAudiences', [])
82
84
  @events = config.fetch('events', [])
83
85
  @experiments = config['experiments']
84
86
  @feature_flags = config.fetch('featureFlags', [])
@@ -102,6 +104,7 @@ module Optimizely
102
104
  @experiment_key_map = generate_key_map(@experiments, 'key')
103
105
  @experiment_id_map = generate_key_map(@experiments, 'id')
104
106
  @audience_id_map = generate_key_map(@audiences, 'id')
107
+ @audience_id_map = @audience_id_map.merge(generate_key_map(@typed_audiences, 'id')) unless @typed_audiences.empty?
105
108
  @variation_id_map = {}
106
109
  @variation_key_map = {}
107
110
  @forced_variation_map = {}
@@ -129,9 +132,9 @@ module Optimizely
129
132
  variation_id = variation['id']
130
133
  variation['featureEnabled'] = variation['featureEnabled'] == true
131
134
  variation_variables = variation['variables']
132
- unless variation_variables.nil?
133
- @variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
134
- end
135
+ next if variation_variables.nil?
136
+
137
+ @variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
135
138
  end
136
139
  @variation_id_map[key] = generate_key_map(variations, 'id')
137
140
  @variation_key_map[key] = generate_key_map(variations, 'key')
@@ -161,6 +164,7 @@ module Optimizely
161
164
 
162
165
  experiment = @experiment_key_map[experiment_key]
163
166
  return experiment if experiment
167
+
164
168
  @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
165
169
  @error_handler.handle_error InvalidExperimentError
166
170
  nil
@@ -175,23 +179,25 @@ module Optimizely
175
179
 
176
180
  experiment = @experiment_id_map[experiment_id]
177
181
  return experiment['key'] unless experiment.nil?
182
+
178
183
  @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
179
184
  @error_handler.handle_error InvalidExperimentError
180
185
  nil
181
186
  end
182
187
 
183
- def get_experiment_ids_for_event(event_key)
184
- # Get experiment IDs for the provided event key.
188
+ def get_event_from_key(event_key)
189
+ # Get event for the provided event key.
185
190
  #
186
- # event_key - Event key for which experiment IDs are to be retrieved.
191
+ # event_key - Event key for which event is to be determined.
187
192
  #
188
- # Returns array of all experiment IDs for the event.
193
+ # Returns Event corresponding to the provided event key.
189
194
 
190
195
  event = @event_key_map[event_key]
191
- return event['experimentIds'] if event
196
+ return event if event
197
+
192
198
  @logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
193
199
  @error_handler.handle_error InvalidEventError
194
- []
200
+ nil
195
201
  end
196
202
 
197
203
  def get_audience_from_id(audience_id)
@@ -203,6 +209,7 @@ module Optimizely
203
209
 
204
210
  audience = @audience_id_map[audience_id]
205
211
  return audience if audience
212
+
206
213
  @logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
207
214
  @error_handler.handle_error InvalidAudienceError
208
215
  nil
@@ -220,6 +227,7 @@ module Optimizely
220
227
  if variation_id_map
221
228
  variation = variation_id_map[variation_id]
222
229
  return variation if variation
230
+
223
231
  @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
224
232
  @error_handler.handle_error InvalidVariationError
225
233
  return nil
@@ -242,6 +250,7 @@ module Optimizely
242
250
  if variation_key_map
243
251
  variation = variation_key_map[variation_key]
244
252
  return variation['id'] if variation
253
+
245
254
  @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
246
255
  @error_handler.handle_error InvalidVariationError
247
256
  return nil
@@ -261,6 +270,7 @@ module Optimizely
261
270
 
262
271
  experiment = @experiment_key_map[experiment_key]
263
272
  return experiment['forcedVariations'] if experiment
273
+
264
274
  @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
265
275
  @error_handler.handle_error InvalidExperimentError
266
276
  end
@@ -322,12 +332,9 @@ module Optimizely
322
332
  #
323
333
  # Returns a boolean value that indicates if the set completed successfully.
324
334
 
325
- return false unless Optimizely::Helpers::Validator.inputs_valid?(
326
- {
327
- user_id: user_id,
328
- experiment_key: experiment_key
329
- }, @logger, Logger::DEBUG
330
- )
335
+ input_values = {experiment_key: experiment_key, user_id: user_id}
336
+ input_values[:variation_key] = variation_key unless variation_key.nil?
337
+ return false unless Optimizely::Helpers::Validator.inputs_valid?(input_values, @logger, Logger::DEBUG)
331
338
 
332
339
  experiment = get_experiment_from_key(experiment_key)
333
340
  experiment_id = experiment['id'] if experiment
@@ -335,7 +342,7 @@ module Optimizely
335
342
  return false if experiment_id.nil? || experiment_id.empty?
336
343
 
337
344
  # clear the forced variation if the variation key is null
338
- if variation_key.nil? || variation_key.empty?
345
+ if variation_key.nil?
339
346
  @forced_variation_map[user_id].delete(experiment_id) if @forced_variation_map.key? user_id
340
347
  @logger.log(Logger::DEBUG, "Variation mapped to experiment '#{experiment_key}' has been removed for user "\
341
348
  "'#{user_id}'.")
@@ -350,9 +357,7 @@ module Optimizely
350
357
  return false
351
358
  end
352
359
 
353
- unless @forced_variation_map.key? user_id
354
- @forced_variation_map[user_id] = {}
355
- end
360
+ @forced_variation_map[user_id] = {} unless @forced_variation_map.key? user_id
356
361
  @forced_variation_map[user_id][experiment_id] = variation_id
357
362
  @logger.log(Logger::DEBUG, "Set variation '#{variation_id}' for experiment '#{experiment_id}' and "\
358
363
  "user '#{user_id}' in the forced variation map.")
@@ -377,6 +382,7 @@ module Optimizely
377
382
  return attribute['id']
378
383
  end
379
384
  return attribute_key if has_reserved_prefix
385
+
380
386
  @logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
381
387
  @error_handler.handle_error InvalidAttributeError
382
388
  nil
@@ -395,6 +401,7 @@ module Optimizely
395
401
  if variation_id_map
396
402
  variation = variation_id_map[variation_id]
397
403
  return true if variation
404
+
398
405
  @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
399
406
  @error_handler.handle_error InvalidVariationError
400
407
  end
@@ -410,6 +417,7 @@ module Optimizely
410
417
  # Returns feature flag if found, otherwise nil
411
418
  feature_flag = @feature_flag_key_map[feature_flag_key]
412
419
  return feature_flag if feature_flag
420
+
413
421
  @logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
414
422
  nil
415
423
  end
@@ -424,6 +432,7 @@ module Optimizely
424
432
  feature_flag_key = feature_flag['key']
425
433
  variable = @feature_variable_key_map[feature_flag_key][variable_key]
426
434
  return variable if variable
435
+
427
436
  @logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
428
437
  "'#{feature_flag_key}'."
429
438
  nil
@@ -437,6 +446,7 @@ module Optimizely
437
446
  # Returns the rollout if found, otherwise nil
438
447
  rollout = @rollout_id_map[rollout_id]
439
448
  return rollout if rollout
449
+
440
450
  @logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
441
451
  nil
442
452
  end