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.
@@ -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)