optimizely-sdk 2.0.0.beta → 2.0.0.beta1

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