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.
- checksums.yaml +4 -4
- data/lib/optimizely.rb +166 -73
- data/lib/optimizely/audience.rb +6 -3
- data/lib/optimizely/bucketer.rb +21 -16
- data/lib/optimizely/condition.rb +4 -2
- data/lib/optimizely/decision_service.rb +141 -141
- data/lib/optimizely/error_handler.rb +5 -5
- data/lib/optimizely/event_builder.rb +158 -147
- data/lib/optimizely/event_dispatcher.rb +7 -6
- data/lib/optimizely/exceptions.rb +12 -1
- data/lib/optimizely/helpers/constants.rb +64 -63
- data/lib/optimizely/helpers/event_tag_utils.rb +86 -11
- data/lib/optimizely/helpers/group.rb +3 -1
- data/lib/optimizely/helpers/validator.rb +9 -1
- data/lib/optimizely/helpers/variable_type.rb +11 -7
- data/lib/optimizely/logger.rb +5 -5
- data/lib/optimizely/notification_center.rb +150 -0
- data/lib/optimizely/params.rb +3 -1
- data/lib/optimizely/project_config.rb +128 -24
- data/lib/optimizely/user_profile_service.rb +2 -0
- data/lib/optimizely/version.rb +4 -1
- metadata +15 -16
@@ -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
|
data/lib/optimizely/params.rb
CHANGED
@@ -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
|
-
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
192
|
-
# Get audience
|
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
|
204
|
+
# Returns the audience
|
197
205
|
|
198
206
|
audience = @audience_id_map[audience_id]
|
199
|
-
return 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
|
250
|
-
# Retrieves
|
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
|
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
|
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
|
|
data/lib/optimizely/version.rb
CHANGED
@@ -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
|
-
|
19
|
+
CLIENT_ENGINE = 'ruby-sdk'
|
20
|
+
VERSION = '2.0.0.beta1'
|
18
21
|
end
|