optimizely-sdk 2.0.0.beta → 2.0.0.beta1

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.
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2017, 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
+ module Optimizely
19
+ class NotificationCenter
20
+ attr_reader :notifications
21
+ attr_reader :notification_id
22
+
23
+ NOTIFICATION_TYPES = {
24
+ ACTIVATE: 'ACTIVATE: experiment, user_id, attributes, variation, event',
25
+ TRACK: 'TRACK: event_key, user_id, attributes, event_tags, event'
26
+ }.freeze
27
+
28
+ def initialize(logger, error_handler)
29
+ @notification_id = 1
30
+ @notifications = {}
31
+ NOTIFICATION_TYPES.each_value { |value| @notifications[value] = [] }
32
+ @logger = logger
33
+ @error_handler = error_handler
34
+ end
35
+
36
+ def add_notification_listener(notification_type, notification_callback)
37
+ # Adds notification callback to the notification center
38
+
39
+ # Args:
40
+ # notification_type: one of the constants in NOTIFICATION_TYPES
41
+ # notification_callback: function to call when the event is sent
42
+
43
+ # Returns:
44
+ # notification ID used to remove the notification
45
+
46
+ return nil unless notification_type_valid?(notification_type)
47
+
48
+ unless notification_callback
49
+ @logger.log Logger::ERROR, 'Callback can not be empty.'
50
+ return nil
51
+ end
52
+
53
+ unless notification_callback.is_a? Method
54
+ @logger.log Logger::ERROR, 'Invalid notification callback given.'
55
+ return nil
56
+ end
57
+
58
+ @notifications[notification_type].each do |notification|
59
+ return -1 if notification[:callback] == notification_callback
60
+ end
61
+ @notifications[notification_type].push(notification_id: @notification_id, callback: notification_callback)
62
+ notification_id = @notification_id
63
+ @notification_id += 1
64
+ notification_id
65
+ end
66
+
67
+ def remove_notification_listener(notification_id)
68
+ # Removes previously added notification callback
69
+
70
+ # Args:
71
+ # notification_id:
72
+ # Returns:
73
+ # The function returns true if found and removed, false otherwise
74
+ unless notification_id
75
+ @logger.log Logger::ERROR, 'Notification ID can not be empty.'
76
+ return nil
77
+ end
78
+ @notifications.each_key do |key|
79
+ @notifications[key].each do |notification|
80
+ if notification_id == notification[:notification_id]
81
+ @notifications[key].delete(notification_id: notification_id, callback: notification[:callback])
82
+ return true
83
+ end
84
+ end
85
+ end
86
+ false
87
+ end
88
+
89
+ def clear_notifications(notification_type)
90
+ # Removes notifications for a certain notification type
91
+ #
92
+ # Args:
93
+ # notification_type: one of the constants in NOTIFICATION_TYPES
94
+
95
+ return nil unless notification_type_valid?(notification_type)
96
+
97
+ @notifications[notification_type] = []
98
+ @logger.log Logger::INFO, "All callbacks for notification type #{notification_type} have been removed."
99
+ end
100
+
101
+ def clean_all_notifications
102
+ # Removes all notifications
103
+ @notifications.each_key { |key| @notifications[key] = [] }
104
+ end
105
+
106
+ def send_notifications(notification_type, *args)
107
+ # Sends off the notification for the specific event. Uses var args to pass in a
108
+ # arbitrary list of parameters according to which notification type was sent
109
+
110
+ # Args:
111
+ # notification_type: one of the constants in NOTIFICATION_TYPES
112
+ # args: list of arguments to the callback
113
+ return nil unless notification_type_valid?(notification_type)
114
+
115
+ @notifications[notification_type].each do |notification|
116
+ begin
117
+ notification_callback = notification[:callback]
118
+ notification_callback.call(*args)
119
+ @logger.log Logger::INFO, "Notification #{notification_type} sent successfully."
120
+ rescue => e
121
+ @logger.log(Logger::ERROR, "Problem calling notify callback. Error: #{e}")
122
+ return nil
123
+ end
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def notification_type_valid?(notification_type)
130
+ # Validates notification type
131
+
132
+ # Args:
133
+ # notification_type: one of the constants in NOTIFICATION_TYPES
134
+
135
+ # Returns true if notification_type is valid, false otherwise
136
+
137
+ unless notification_type
138
+ @logger.log Logger::ERROR, 'Notification type can not be empty.'
139
+ return false
140
+ end
141
+
142
+ unless @notifications.include?(notification_type)
143
+ @logger.log Logger::ERROR, 'Invalid notification type.'
144
+ @error_handler.handle_error InvalidNotificationType
145
+ return false
146
+ end
147
+ true
148
+ end
149
+ end
150
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
- # Copyright 2016, Optimizely and contributors
4
+ # Copyright 2016-2017, Optimizely and contributors
3
5
  #
4
6
  # Licensed under the Apache License, Version 2.0 (the "License");
5
7
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,6 @@
1
- #
2
- # Copyright 2016-2017, Optimizely and contributors
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2016-2018, Optimizely and contributors
3
4
  #
4
5
  # Licensed under the Apache License, Version 2.0 (the "License");
5
6
  # you may not use this file except in compliance with the License.
@@ -16,14 +17,13 @@
16
17
  require 'json'
17
18
 
18
19
  module Optimizely
19
-
20
20
  V1_CONFIG_VERSION = '1'
21
21
 
22
- UNSUPPORTED_VERSIONS = [V1_CONFIG_VERSION]
22
+ UNSUPPORTED_VERSIONS = [V1_CONFIG_VERSION].freeze
23
23
 
24
24
  class ProjectConfig
25
25
  # Representation of the Optimizely project config.
26
- RUNNING_EXPERIMENT_STATUS = ['Running']
26
+ RUNNING_EXPERIMENT_STATUS = ['Running'].freeze
27
27
 
28
28
  # Gets project config attributes.
29
29
  attr_reader :error_handler
@@ -38,6 +38,8 @@ module Optimizely
38
38
  attr_reader :groups
39
39
  attr_reader :parsing_succeeded
40
40
  attr_reader :project_id
41
+ # Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
42
+ attr_reader :anonymize_ip
41
43
  attr_reader :revision
42
44
  attr_reader :rollouts
43
45
  attr_reader :version
@@ -56,21 +58,25 @@ module Optimizely
56
58
  attr_reader :variation_id_to_variable_usage_map
57
59
  attr_reader :variation_key_map
58
60
 
61
+ # Hash of user IDs to a Hash
62
+ # of experiments to variations. This contains all the forced variations
63
+ # set by the user by calling setForcedVariation (it is not the same as the
64
+ # whitelisting forcedVariations data structure in the Experiments class).
65
+ attr_reader :forced_variation_map
66
+
59
67
  def initialize(datafile, logger, error_handler)
60
68
  # ProjectConfig init method to fetch and set project config data
61
69
  #
62
70
  # datafile - JSON string representing the project
63
71
 
64
- config = JSON.load(datafile)
72
+ config = JSON.parse(datafile)
65
73
 
66
74
  @parsing_succeeded = false
67
75
  @error_handler = error_handler
68
76
  @logger = logger
69
77
  @version = config['version']
70
78
 
71
- if UNSUPPORTED_VERSIONS.include?(@version)
72
- return
73
- end
79
+ return if UNSUPPORTED_VERSIONS.include?(@version)
74
80
 
75
81
  @account_id = config['accountId']
76
82
  @attributes = config.fetch('attributes', [])
@@ -80,6 +86,7 @@ module Optimizely
80
86
  @feature_flags = config.fetch('featureFlags', [])
81
87
  @groups = config.fetch('groups', [])
82
88
  @project_id = config['projectId']
89
+ @anonymize_ip = config.key? 'anonymizeIP' ? config['anonymizeIP'] : false
83
90
  @revision = config['revision']
84
91
  @rollouts = config.fetch('rollouts', [])
85
92
 
@@ -98,9 +105,10 @@ module Optimizely
98
105
  @audience_id_map = generate_key_map(@audiences, 'id')
99
106
  @variation_id_map = {}
100
107
  @variation_key_map = {}
108
+ @forced_variation_map = {}
101
109
  @variation_id_to_variable_usage_map = {}
102
110
  @variation_id_to_experiment_map = {}
103
- @experiment_key_map.each do |key, exp|
111
+ @experiment_key_map.each_value do |exp|
104
112
  # Excludes experiments from rollouts
105
113
  variations = exp.fetch('variations')
106
114
  variations.each do |variation|
@@ -111,23 +119,23 @@ module Optimizely
111
119
  @rollout_id_map = generate_key_map(@rollouts, 'id')
112
120
  # split out the experiment id map for rollouts
113
121
  @rollout_experiment_id_map = {}
114
- @rollout_id_map.each do |id, rollout|
122
+ @rollout_id_map.each_value do |rollout|
115
123
  exps = rollout.fetch('experiments')
116
124
  @rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
117
125
  end
118
126
  @all_experiments = @experiment_key_map.merge(@rollout_experiment_id_map)
119
127
  @all_experiments.each do |key, exp|
120
128
  variations = exp.fetch('variations')
121
- @variation_id_map[key] = generate_key_map(variations, 'id')
122
- @variation_key_map[key] = generate_key_map(variations, 'key')
123
-
124
129
  variations.each do |variation|
125
130
  variation_id = variation['id']
131
+ variation['featureEnabled'] = variation['featureEnabled'] == true
126
132
  variation_variables = variation['variables']
127
133
  unless variation_variables.nil?
128
134
  @variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
129
135
  end
130
136
  end
137
+ @variation_id_map[key] = generate_key_map(variations, 'id')
138
+ @variation_key_map[key] = generate_key_map(variations, 'key')
131
139
  end
132
140
  @feature_flag_key_map = generate_key_map(@feature_flags, 'key')
133
141
  @feature_variable_key_map = {}
@@ -143,7 +151,7 @@ module Optimizely
143
151
  # experiment - Experiment
144
152
  #
145
153
  # Returns true if experiment is running
146
- return RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
154
+ RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
147
155
  end
148
156
 
149
157
  def get_experiment_from_key(experiment_key)
@@ -188,15 +196,15 @@ module Optimizely
188
196
  []
189
197
  end
190
198
 
191
- def get_audience_conditions_from_id(audience_id)
192
- # Get audience conditions for the provided audience ID
199
+ def get_audience_from_id(audience_id)
200
+ # Get audience for the provided audience ID
193
201
  #
194
202
  # audience_id - ID of the audience
195
203
  #
196
- # Returns conditions for the audience
204
+ # Returns the audience
197
205
 
198
206
  audience = @audience_id_map[audience_id]
199
- return audience['conditions'] if audience
207
+ return audience if audience
200
208
  @logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
201
209
  @error_handler.handle_error InvalidAudienceError
202
210
  nil
@@ -246,12 +254,12 @@ module Optimizely
246
254
  nil
247
255
  end
248
256
 
249
- def get_forced_variations(experiment_key)
250
- # Retrieves forced variations for a given experiment Key
257
+ def get_whitelisted_variations(experiment_key)
258
+ # Retrieves whitelisted variations for a given experiment Key
251
259
  #
252
260
  # experiment_key - String Key representing the experiment
253
261
  #
254
- # Returns forced variations for the experiment or nil
262
+ # Returns whitelisted variations for the experiment or nil
255
263
 
256
264
  experiment = @experiment_key_map[experiment_key]
257
265
  return experiment['forcedVariations'] if experiment
@@ -259,6 +267,102 @@ module Optimizely
259
267
  @error_handler.handle_error InvalidExperimentError
260
268
  end
261
269
 
270
+ def get_forced_variation(experiment_key, user_id)
271
+ # Gets the forced variation for the given user and experiment.
272
+ #
273
+ # experiment_key - String Key for experiment.
274
+ # user_id - String ID for user
275
+ #
276
+ # Returns Variation The variation which the given user and experiment should be forced into.
277
+
278
+ # check for nil and empty string user ID
279
+ if user_id.nil? || user_id.empty?
280
+ @logger.log(Logger::DEBUG, 'User ID is invalid')
281
+ return nil
282
+ end
283
+
284
+ unless @forced_variation_map.key? user_id
285
+ @logger.log(Logger::DEBUG, "User '#{user_id}' is not in the forced variation map.")
286
+ return nil
287
+ end
288
+
289
+ experiment_to_variation_map = @forced_variation_map[user_id]
290
+ experiment = get_experiment_from_key(experiment_key)
291
+ experiment_id = experiment['id'] if experiment
292
+ # check for nil and empty string experiment ID
293
+ if experiment_id.nil? || experiment_id.empty?
294
+ # this case is logged in get_experiment_from_key
295
+ return nil
296
+ end
297
+
298
+ unless experiment_to_variation_map.key? experiment_id
299
+ @logger.log(Logger::DEBUG, "No experiment '#{experiment_key}' mapped to user '#{user_id}' "\
300
+ 'in the forced variation map.')
301
+ return nil
302
+ end
303
+
304
+ variation_id = experiment_to_variation_map[experiment_id]
305
+ variation_key = ''
306
+ variation = get_variation_from_id(experiment_key, variation_id)
307
+ variation_key = variation['key'] if variation
308
+
309
+ # check if the variation exists in the datafile
310
+ if variation_key.empty?
311
+ # this case is logged in get_variation_from_id
312
+ return nil
313
+ end
314
+
315
+ @logger.log(Logger::DEBUG, "Variation '#{variation_key}' is mapped to experiment '#{experiment_key}' "\
316
+ "and user '#{user_id}' in the forced variation map")
317
+
318
+ variation
319
+ end
320
+
321
+ def set_forced_variation(experiment_key, user_id, variation_key)
322
+ # Sets a Hash of user IDs to a Hash of experiments to forced variations.
323
+ #
324
+ # experiment_key - String Key for experiment.
325
+ # user_id - String ID for user.
326
+ # variation_key - String Key for variation. If null, then clear the existing experiment-to-variation mapping.
327
+ #
328
+ # Returns a boolean value that indicates if the set completed successfully.
329
+
330
+ # check for null and empty string user ID
331
+ if user_id.nil? || user_id.empty?
332
+ @logger.log(Logger::DEBUG, 'User ID is invalid')
333
+ return false
334
+ end
335
+
336
+ experiment = get_experiment_from_key(experiment_key)
337
+ experiment_id = experiment['id'] if experiment
338
+ # check if the experiment exists in the datafile
339
+ return false if experiment_id.nil? || experiment_id.empty?
340
+
341
+ # clear the forced variation if the variation key is null
342
+ if variation_key.nil? || variation_key.empty?
343
+ @forced_variation_map[user_id].delete(experiment_id) if @forced_variation_map.key? user_id
344
+ @logger.log(Logger::DEBUG, "Variation mapped to experiment '#{experiment_key}' has been removed for user "\
345
+ "'#{user_id}'.")
346
+ return true
347
+ end
348
+
349
+ variation_id = get_variation_id_from_key(experiment_key, variation_key)
350
+
351
+ # check if the variation exists in the datafile
352
+ unless variation_id
353
+ # this case is logged in get_variation_id_from_key
354
+ return false
355
+ end
356
+
357
+ unless @forced_variation_map.key? user_id
358
+ @forced_variation_map[user_id] = {}
359
+ end
360
+ @forced_variation_map[user_id][experiment_id] = variation_id
361
+ @logger.log(Logger::DEBUG, "Set variation '#{variation_id}' for experiment '#{experiment_id}' and "\
362
+ "user '#{user_id}' in the forced variation map.")
363
+ true
364
+ end
365
+
262
366
  def get_attribute_id(attribute_key)
263
367
  attribute = @attribute_key_map[attribute_key]
264
368
  return attribute['id'] if attribute
@@ -290,7 +394,6 @@ module Optimizely
290
394
  return true if variation
291
395
  @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
292
396
  @error_handler.handle_error InvalidVariationError
293
- return false
294
397
  end
295
398
 
296
399
  false
@@ -318,7 +421,8 @@ module Optimizely
318
421
  feature_flag_key = feature_flag['key']
319
422
  variable = @feature_variable_key_map[feature_flag_key][variable_key]
320
423
  return variable if variable
321
- @logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag '#{feature_flag_key}'."
424
+ @logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag "\
425
+ "'#{feature_flag_key}'."
322
426
  nil
323
427
  end
324
428
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Copyright 2017, Optimizely and contributors
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Copyright 2016-2017, Optimizely and contributors
3
5
  #
@@ -14,5 +16,6 @@
14
16
  # limitations under the License.
15
17
  #
16
18
  module Optimizely
17
- VERSION = '2.0.0.beta'.freeze
19
+ CLIENT_ENGINE = 'ruby-sdk'
20
+ VERSION = '2.0.0.beta1'
18
21
  end