optimizely-sdk 1.4.0 → 1.5.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.
- checksums.yaml +4 -4
- data/lib/optimizely.rb +223 -11
- data/lib/optimizely/bucketer.rb +19 -17
- data/lib/optimizely/decision_service.rb +245 -50
- data/lib/optimizely/event_builder.rb +33 -18
- data/lib/optimizely/exceptions.rb +9 -0
- data/lib/optimizely/helpers/constants.rb +7 -0
- data/lib/optimizely/helpers/variable_type.rb +56 -0
- data/lib/optimizely/notification_center.rb +148 -0
- data/lib/optimizely/project_config.rb +91 -37
- data/lib/optimizely/version.rb +2 -2
- metadata +5 -3
@@ -20,6 +20,9 @@ require_relative '../optimizely/helpers/event_tag_utils'
|
|
20
20
|
require 'securerandom'
|
21
21
|
|
22
22
|
module Optimizely
|
23
|
+
|
24
|
+
RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY = "optimizely_bucketing_id".freeze
|
25
|
+
|
23
26
|
class Event
|
24
27
|
# Representation of an event which can be sent to the Optimizely logging endpoint.
|
25
28
|
|
@@ -69,22 +72,33 @@ module Optimizely
|
|
69
72
|
attribute_value = attributes[attribute_key]
|
70
73
|
next if attribute_value.nil?
|
71
74
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
75
|
+
if attribute_key.eql? RESERVED_ATTRIBUTE_KEY_BUCKETING_ID
|
76
|
+
# TODO (Copied from PHP-SDK) (Alda): the type for bucketing ID attribute may change so
|
77
|
+
# that custom attributes are not overloaded
|
78
|
+
feature = {
|
79
|
+
entity_id: RESERVED_ATTRIBUTE_KEY_BUCKETING_ID,
|
80
|
+
key: RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY,
|
81
|
+
type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
|
82
|
+
value: attribute_value
|
83
|
+
}
|
84
|
+
else
|
85
|
+
# Skip attributes not in the datafile
|
86
|
+
attribute_id = @config.get_attribute_id(attribute_key)
|
87
|
+
next unless attribute_id
|
88
|
+
|
89
|
+
feature = {
|
90
|
+
entity_id: attribute_id,
|
91
|
+
key: attribute_key,
|
92
|
+
type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
|
93
|
+
value: attribute_value
|
94
|
+
}
|
82
95
|
|
83
|
-
visitor_attributes.push(feature)
|
84
96
|
end
|
85
|
-
|
97
|
+
visitor_attributes.push(feature)
|
98
|
+
end
|
99
|
+
end
|
86
100
|
|
87
|
-
|
101
|
+
common_params = {
|
88
102
|
account_id: @config.account_id,
|
89
103
|
project_id: @config.project_id,
|
90
104
|
visitors: [
|
@@ -93,11 +107,12 @@ module Optimizely
|
|
93
107
|
snapshots: [],
|
94
108
|
visitor_id: user_id
|
95
109
|
}
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
110
|
+
],
|
111
|
+
anonymize_ip: @config.anonymize_ip,
|
112
|
+
revision: @config.revision,
|
113
|
+
client_name: CLIENT_ENGINE,
|
114
|
+
client_version: VERSION
|
115
|
+
}
|
101
116
|
|
102
117
|
common_params
|
103
118
|
end
|
@@ -95,4 +95,13 @@ module Optimizely
|
|
95
95
|
super("Provided #{type} is in an invalid format.")
|
96
96
|
end
|
97
97
|
end
|
98
|
+
|
99
|
+
class InvalidNotificationType < Error
|
100
|
+
# Raised when an invalid notification type is provided
|
101
|
+
|
102
|
+
def initialize(msg = 'Provided notification type is invalid.')
|
103
|
+
super
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
98
107
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2017, Optimizely and contributors
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
require 'optimizely/logger'
|
17
|
+
|
18
|
+
module Optimizely
|
19
|
+
module Helpers
|
20
|
+
module VariableType
|
21
|
+
module_function
|
22
|
+
|
23
|
+
def cast_value_to_type(value, variable_type, logger)
|
24
|
+
# Attempts to cast the given value to the specified type
|
25
|
+
#
|
26
|
+
# value - The string value to cast
|
27
|
+
# variable_type - String variable type
|
28
|
+
#
|
29
|
+
# Returns the cast value or nil if not able to cast
|
30
|
+
return_value = nil
|
31
|
+
|
32
|
+
case variable_type
|
33
|
+
when "boolean"
|
34
|
+
return_value = value == "true"
|
35
|
+
when "double"
|
36
|
+
begin
|
37
|
+
return_value = Float(value)
|
38
|
+
rescue => e
|
39
|
+
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type '#{variable_type}': #{e.message}.")
|
40
|
+
end
|
41
|
+
when "integer"
|
42
|
+
begin
|
43
|
+
return_value = Integer(value)
|
44
|
+
rescue => e
|
45
|
+
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type '#{variable_type}': #{e.message}.")
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# default case is string
|
49
|
+
return_value = value
|
50
|
+
end
|
51
|
+
|
52
|
+
return return_value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2017, Optimizely and contributors
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
module Optimizely
|
17
|
+
class NotificationCenter
|
18
|
+
attr_reader :notifications
|
19
|
+
attr_reader :notification_id
|
20
|
+
|
21
|
+
NOTIFICATION_TYPES = {
|
22
|
+
ACTIVATE: 'ACTIVATE: experiment, user_id, attributes, variation, event',
|
23
|
+
TRACK: 'TRACK: event_key, user_id, attributes, event_tags, event'
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def initialize(logger, error_handler)
|
27
|
+
@notification_id = 1
|
28
|
+
@notifications = {}
|
29
|
+
NOTIFICATION_TYPES.values.each { |value| @notifications[value] = [] }
|
30
|
+
@logger = logger
|
31
|
+
@error_handler = error_handler
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_notification_listener(notification_type, notification_callback)
|
35
|
+
# Adds notification callback to the notification center
|
36
|
+
|
37
|
+
# Args:
|
38
|
+
# notification_type: one of the constants in NOTIFICATION_TYPES
|
39
|
+
# notification_callback: function to call when the event is sent
|
40
|
+
|
41
|
+
# Returns:
|
42
|
+
# notification ID used to remove the notification
|
43
|
+
|
44
|
+
return nil unless notification_type_valid?(notification_type)
|
45
|
+
|
46
|
+
unless notification_callback
|
47
|
+
@logger.log Logger::ERROR, 'Callback can not be empty.'
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
|
51
|
+
unless notification_callback.is_a? Method
|
52
|
+
@logger.log Logger::ERROR, 'Invalid notification callback given.'
|
53
|
+
return nil
|
54
|
+
end
|
55
|
+
|
56
|
+
@notifications[notification_type].each do |notification|
|
57
|
+
return -1 if notification[:callback] == notification_callback
|
58
|
+
end
|
59
|
+
@notifications[notification_type].push(notification_id: @notification_id, callback: notification_callback)
|
60
|
+
notification_id = @notification_id
|
61
|
+
@notification_id += 1
|
62
|
+
notification_id
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove_notification_listener(notification_id)
|
66
|
+
# Removes previously added notification callback
|
67
|
+
|
68
|
+
# Args:
|
69
|
+
# notification_id:
|
70
|
+
# Returns:
|
71
|
+
# The function returns true if found and removed, false otherwise
|
72
|
+
unless notification_id
|
73
|
+
@logger.log Logger::ERROR, 'Notification ID can not be empty.'
|
74
|
+
return nil
|
75
|
+
end
|
76
|
+
@notifications.each do |key, _array|
|
77
|
+
@notifications[key].each do |notification|
|
78
|
+
if notification_id == notification[:notification_id]
|
79
|
+
@notifications[key].delete(notification_id: notification_id, callback: notification[:callback])
|
80
|
+
return true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
false
|
85
|
+
end
|
86
|
+
|
87
|
+
def clear_notifications(notification_type)
|
88
|
+
# Removes notifications for a certain notification type
|
89
|
+
#
|
90
|
+
# Args:
|
91
|
+
# notification_type: one of the constants in NOTIFICATION_TYPES
|
92
|
+
|
93
|
+
return nil unless notification_type_valid?(notification_type)
|
94
|
+
|
95
|
+
@notifications[notification_type] = []
|
96
|
+
@logger.log Logger::INFO, "All callbacks for notification type #{notification_type} have been removed."
|
97
|
+
end
|
98
|
+
|
99
|
+
def clean_all_notifications
|
100
|
+
# Removes all notifications
|
101
|
+
@notifications.keys.each { |key| @notifications[key] = [] }
|
102
|
+
end
|
103
|
+
|
104
|
+
def send_notifications(notification_type, *args)
|
105
|
+
# Sends off the notification for the specific event. Uses var args to pass in a
|
106
|
+
# arbitrary list of parameters according to which notification type was sent
|
107
|
+
|
108
|
+
# Args:
|
109
|
+
# notification_type: one of the constants in NOTIFICATION_TYPES
|
110
|
+
# args: list of arguments to the callback
|
111
|
+
return nil unless notification_type_valid?(notification_type)
|
112
|
+
|
113
|
+
@notifications[notification_type].each do |notification|
|
114
|
+
begin
|
115
|
+
notification_callback = notification[:callback]
|
116
|
+
notification_callback.call(*args)
|
117
|
+
@logger.log Logger::INFO, "Notification #{notification_type} sent successfully."
|
118
|
+
rescue => e
|
119
|
+
@logger.log(Logger::ERROR, "Problem calling notify callback. Error: #{e}")
|
120
|
+
return nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def notification_type_valid?(notification_type)
|
128
|
+
# Validates notification type
|
129
|
+
|
130
|
+
# Args:
|
131
|
+
# notification_type: one of the constants in NOTIFICATION_TYPES
|
132
|
+
|
133
|
+
# Returns true if notification_type is valid, false otherwise
|
134
|
+
|
135
|
+
unless notification_type
|
136
|
+
@logger.log Logger::ERROR, 'Notification type can not be empty.'
|
137
|
+
return false
|
138
|
+
end
|
139
|
+
|
140
|
+
unless @notifications.include?(notification_type)
|
141
|
+
@logger.log Logger::ERROR, 'Invalid notification type.'
|
142
|
+
@error_handler.handle_error InvalidNotificationType
|
143
|
+
return false
|
144
|
+
end
|
145
|
+
true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -18,9 +18,7 @@ require 'json'
|
|
18
18
|
module Optimizely
|
19
19
|
|
20
20
|
V1_CONFIG_VERSION = '1'
|
21
|
-
V2_CONFIG_VERSION = '2'
|
22
21
|
|
23
|
-
SUPPORTED_VERSIONS = [V2_CONFIG_VERSION]
|
24
22
|
UNSUPPORTED_VERSIONS = [V1_CONFIG_VERSION]
|
25
23
|
|
26
24
|
class ProjectConfig
|
@@ -31,25 +29,33 @@ module Optimizely
|
|
31
29
|
attr_reader :error_handler
|
32
30
|
attr_reader :logger
|
33
31
|
|
34
|
-
attr_reader :parsing_succeeded
|
35
|
-
attr_reader :version
|
36
32
|
attr_reader :account_id
|
37
|
-
attr_reader :project_id
|
38
33
|
attr_reader :attributes
|
34
|
+
attr_reader :audiences
|
39
35
|
attr_reader :events
|
40
36
|
attr_reader :experiments
|
37
|
+
attr_reader :feature_flags
|
41
38
|
attr_reader :groups
|
39
|
+
attr_reader :parsing_succeeded
|
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
|
42
43
|
attr_reader :revision
|
43
|
-
attr_reader :
|
44
|
+
attr_reader :rollouts
|
45
|
+
attr_reader :version
|
44
46
|
|
45
47
|
attr_reader :attribute_key_map
|
46
48
|
attr_reader :audience_id_map
|
47
49
|
attr_reader :event_key_map
|
48
50
|
attr_reader :experiment_id_map
|
49
51
|
attr_reader :experiment_key_map
|
52
|
+
attr_reader :feature_flag_key_map
|
53
|
+
attr_reader :feature_variable_key_map
|
50
54
|
attr_reader :group_key_map
|
51
|
-
attr_reader :
|
55
|
+
attr_reader :rollout_id_map
|
56
|
+
attr_reader :rollout_experiment_id_map
|
52
57
|
attr_reader :variation_id_map
|
58
|
+
attr_reader :variation_id_to_variable_usage_map
|
53
59
|
attr_reader :variation_key_map
|
54
60
|
|
55
61
|
# Hash of user IDs to a Hash
|
@@ -75,13 +81,16 @@ module Optimizely
|
|
75
81
|
end
|
76
82
|
|
77
83
|
@account_id = config['accountId']
|
78
|
-
@
|
79
|
-
@
|
80
|
-
@events = config
|
84
|
+
@attributes = config.fetch('attributes', [])
|
85
|
+
@audiences = config.fetch('audiences', [])
|
86
|
+
@events = config.fetch('events', [])
|
81
87
|
@experiments = config['experiments']
|
82
|
-
@
|
83
|
-
@audiences = config['audiences']
|
88
|
+
@feature_flags = config.fetch('featureFlags', [])
|
84
89
|
@groups = config.fetch('groups', [])
|
90
|
+
@project_id = config['projectId']
|
91
|
+
@anonymize_ip = (config.has_key? 'anonymizeIP')? config['anonymizeIP'] :false
|
92
|
+
@revision = config['revision']
|
93
|
+
@rollouts = config.fetch('rollouts', [])
|
85
94
|
|
86
95
|
# Utility maps for quick lookup
|
87
96
|
@attribute_key_map = generate_key_map(@attributes, 'key')
|
@@ -102,9 +111,38 @@ module Optimizely
|
|
102
111
|
@variation_id_to_variable_usage_map = {}
|
103
112
|
@variation_id_to_experiment_map = {}
|
104
113
|
@experiment_key_map.each do |key, exp|
|
114
|
+
# Excludes experiments from rollouts
|
115
|
+
variations = exp.fetch('variations')
|
116
|
+
variations.each do |variation|
|
117
|
+
variation_id = variation['id']
|
118
|
+
@variation_id_to_experiment_map[variation_id] = exp
|
119
|
+
end
|
120
|
+
end
|
121
|
+
@rollout_id_map = generate_key_map(@rollouts, 'id')
|
122
|
+
# split out the experiment id map for rollouts
|
123
|
+
@rollout_experiment_id_map = {}
|
124
|
+
@rollout_id_map.each do |id, rollout|
|
125
|
+
exps = rollout.fetch('experiments')
|
126
|
+
@rollout_experiment_id_map = @rollout_experiment_id_map.merge(generate_key_map(exps, 'id'))
|
127
|
+
end
|
128
|
+
@all_experiments = @experiment_key_map.merge(@rollout_experiment_id_map)
|
129
|
+
@all_experiments.each do |key, exp|
|
105
130
|
variations = exp.fetch('variations')
|
106
131
|
@variation_id_map[key] = generate_key_map(variations, 'id')
|
107
132
|
@variation_key_map[key] = generate_key_map(variations, 'key')
|
133
|
+
|
134
|
+
variations.each do |variation|
|
135
|
+
variation_id = variation['id']
|
136
|
+
variation_variables = variation['variables']
|
137
|
+
unless variation_variables.nil?
|
138
|
+
@variation_id_to_variable_usage_map[variation_id] = generate_key_map(variation_variables, 'id')
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
@feature_flag_key_map = generate_key_map(@feature_flags, 'key')
|
143
|
+
@feature_variable_key_map = {}
|
144
|
+
@feature_flag_key_map.each do |key, feature_flag|
|
145
|
+
@feature_variable_key_map[key] = generate_key_map(feature_flag['variables'], 'key')
|
108
146
|
end
|
109
147
|
@parsing_succeeded = true
|
110
148
|
end
|
@@ -123,7 +161,7 @@ module Optimizely
|
|
123
161
|
#
|
124
162
|
# experiment_key - String key representing the experiment
|
125
163
|
#
|
126
|
-
# Returns Experiment
|
164
|
+
# Returns Experiment or nil if not found
|
127
165
|
|
128
166
|
experiment = @experiment_key_map[experiment_key]
|
129
167
|
return experiment if experiment
|
@@ -174,28 +212,6 @@ module Optimizely
|
|
174
212
|
nil
|
175
213
|
end
|
176
214
|
|
177
|
-
def get_variation_key_from_id(experiment_key, variation_id)
|
178
|
-
# Get variation key given experiment key and variation ID
|
179
|
-
#
|
180
|
-
# experiment_key - Key representing parent experiment of variation
|
181
|
-
# variation_id - ID of the variation
|
182
|
-
#
|
183
|
-
# Returns key of the variation
|
184
|
-
|
185
|
-
variation_id_map = @variation_id_map[experiment_key]
|
186
|
-
if variation_id_map
|
187
|
-
variation = variation_id_map[variation_id]
|
188
|
-
return variation['key'] if variation
|
189
|
-
@logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
|
190
|
-
@error_handler.handle_error InvalidVariationError
|
191
|
-
return nil
|
192
|
-
end
|
193
|
-
|
194
|
-
@logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
|
195
|
-
@error_handler.handle_error InvalidExperimentError
|
196
|
-
nil
|
197
|
-
end
|
198
|
-
|
199
215
|
def get_variation_from_id(experiment_key, variation_id)
|
200
216
|
# Get variation given experiment key and variation ID
|
201
217
|
#
|
@@ -298,7 +314,7 @@ module Optimizely
|
|
298
314
|
end
|
299
315
|
|
300
316
|
@logger.log(Logger::DEBUG, "Variation '#{variation_key}' is mapped to experiment '#{experiment_key}' and user '#{user_id}' in the forced variation map")
|
301
|
-
|
317
|
+
|
302
318
|
variation
|
303
319
|
end
|
304
320
|
|
@@ -339,7 +355,7 @@ module Optimizely
|
|
339
355
|
return false
|
340
356
|
end
|
341
357
|
|
342
|
-
unless @forced_variation_map.has_key? user_id
|
358
|
+
unless @forced_variation_map.has_key? user_id
|
343
359
|
@forced_variation_map[user_id] = {}
|
344
360
|
end
|
345
361
|
@forced_variation_map[user_id][experiment_id] = variation_id
|
@@ -383,6 +399,44 @@ module Optimizely
|
|
383
399
|
false
|
384
400
|
end
|
385
401
|
|
402
|
+
def get_feature_flag_from_key(feature_flag_key)
|
403
|
+
# Retrieves the feature flag with the given key
|
404
|
+
#
|
405
|
+
# feature_flag_key - String feature key
|
406
|
+
#
|
407
|
+
# Returns feature flag if found, otherwise nil
|
408
|
+
feature_flag = @feature_flag_key_map[feature_flag_key]
|
409
|
+
return feature_flag if feature_flag
|
410
|
+
@logger.log Logger::ERROR, "Feature flag key '#{feature_flag_key}' is not in datafile."
|
411
|
+
nil
|
412
|
+
end
|
413
|
+
|
414
|
+
def get_feature_variable(feature_flag, variable_key)
|
415
|
+
# Retrieves the variable with the given key for the given feature
|
416
|
+
#
|
417
|
+
# feature_flag - The feature flag for which we are retrieving the variable
|
418
|
+
# variable_key - String variable key
|
419
|
+
#
|
420
|
+
# Returns variable if found, otherwise nil
|
421
|
+
feature_flag_key = feature_flag['key']
|
422
|
+
variable = @feature_variable_key_map[feature_flag_key][variable_key]
|
423
|
+
return variable if variable
|
424
|
+
@logger.log Logger::ERROR, "No feature variable was found for key '#{variable_key}' in feature flag '#{feature_flag_key}'."
|
425
|
+
nil
|
426
|
+
end
|
427
|
+
|
428
|
+
def get_rollout_from_id(rollout_id)
|
429
|
+
# Retrieves the rollout with the given ID
|
430
|
+
#
|
431
|
+
# rollout_id - String rollout ID
|
432
|
+
#
|
433
|
+
# Returns the rollout if found, otherwise nil
|
434
|
+
rollout = @rollout_id_map[rollout_id]
|
435
|
+
return rollout if rollout
|
436
|
+
@logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
|
437
|
+
nil
|
438
|
+
end
|
439
|
+
|
386
440
|
private
|
387
441
|
|
388
442
|
def generate_key_map(array, key)
|