optimizely-sdk 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # Skip attributes not in the datafile
73
- attribute_id = @config.get_attribute_id(attribute_key)
74
- next unless attribute_id
75
-
76
- feature = {
77
- entity_id: attribute_id,
78
- key: attribute_key,
79
- type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
80
- value: attribute_value
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
- end
97
+ visitor_attributes.push(feature)
98
+ end
99
+ end
86
100
 
87
- common_params = {
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
- revision: @config.revision,
98
- client_name: CLIENT_ENGINE,
99
- client_version: VERSION
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
@@ -298,6 +298,13 @@ module Optimizely
298
298
  'revision'
299
299
  ]
300
300
  }
301
+
302
+ VARIABLE_TYPES = {
303
+ 'BOOLEAN' => 'boolean',
304
+ 'DOUBLE' => 'double',
305
+ 'INTEGER' => 'integer',
306
+ 'STRING' => 'string'
307
+ }
301
308
  end
302
309
  end
303
310
  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 :audiences
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 :audience_id_map
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
- @project_id = config['projectId']
79
- @attributes = config['attributes']
80
- @events = config['events']
84
+ @attributes = config.fetch('attributes', [])
85
+ @audiences = config.fetch('audiences', [])
86
+ @events = config.fetch('events', [])
81
87
  @experiments = config['experiments']
82
- @revision = config['revision']
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)