optimizely-sdk 2.1.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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