optimizely-sdk 3.3.1 → 3.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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2019, Optimizely and contributors
4
+ # Copyright 2016-2020, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -18,6 +18,13 @@
18
18
  module Optimizely
19
19
  class Error < StandardError; end
20
20
 
21
+ class HTTPCallError < Error
22
+ # Raised when a 4xx or 5xx response code is recieved.
23
+ def initialize(msg = 'HTTP call resulted in a response with an error code.')
24
+ super
25
+ end
26
+ end
27
+
21
28
  class InvalidAudienceError < Error
22
29
  # Raised when an invalid audience is provided
23
30
 
@@ -74,14 +81,6 @@ module Optimizely
74
81
  end
75
82
  end
76
83
 
77
- class InvalidDatafileError < Error
78
- # Raised when a public method fails due to an invalid datafile
79
-
80
- def initialize(aborted_method)
81
- super("Provided datafile is in an invalid format. Aborting #{aborted_method}.")
82
- end
83
- end
84
-
85
84
  class InvalidDatafileVersionError < Error
86
85
  # Raised when a datafile with an unsupported version is provided
87
86
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2019, Optimizely and contributors
4
+ # Copyright 2016-2020, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -304,7 +304,8 @@ module Optimizely
304
304
  'BOOLEAN' => 'boolean',
305
305
  'DOUBLE' => 'double',
306
306
  'INTEGER' => 'integer',
307
- 'STRING' => 'string'
307
+ 'STRING' => 'string',
308
+ 'JSON' => 'json'
308
309
  }.freeze
309
310
 
310
311
  INPUT_VARIABLES = {
@@ -357,11 +358,13 @@ module Optimizely
357
358
  'AB_TEST' => 'ab-test',
358
359
  'FEATURE' => 'feature',
359
360
  'FEATURE_TEST' => 'feature-test',
360
- 'FEATURE_VARIABLE' => 'feature-variable'
361
+ 'FEATURE_VARIABLE' => 'feature-variable',
362
+ 'ALL_FEATURE_VARIABLES' => 'all-feature-variables'
361
363
  }.freeze
362
364
 
363
365
  CONFIG_MANAGER = {
364
366
  'DATAFILE_URL_TEMPLATE' => 'https://cdn.optimizely.com/datafiles/%s.json',
367
+ 'AUTHENTICATED_DATAFILE_URL_TEMPLATE' => 'https://config.optimizely.com/datafiles/auth/%s.json',
365
368
  # Default time in seconds to block the 'config' method call until 'config' instance has been initialized.
366
369
  'DEFAULT_BLOCKING_TIMEOUT' => 15,
367
370
  # Default config update interval of 5 minutes
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2020, 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
+
19
+ require 'net/http'
20
+
21
+ module Optimizely
22
+ module Helpers
23
+ module HttpUtils
24
+ module_function
25
+
26
+ def make_request(url, http_method, request_body = nil, headers = {}, read_timeout = nil, proxy_config = nil)
27
+ # makes http/https GET/POST request and returns response
28
+ #
29
+ uri = URI.parse(url)
30
+
31
+ if http_method == :get
32
+ request = Net::HTTP::Get.new(uri.request_uri)
33
+ elsif http_method == :post
34
+ request = Net::HTTP::Post.new(uri.request_uri)
35
+ request.body = request_body if request_body
36
+ else
37
+ return nil
38
+ end
39
+
40
+ # set headers
41
+ headers&.each do |key, val|
42
+ request[key] = val
43
+ end
44
+
45
+ # do not try to make request with proxy unless we have at least a host
46
+ http_class = if proxy_config&.host
47
+ Net::HTTP::Proxy(
48
+ proxy_config.host,
49
+ proxy_config.port,
50
+ proxy_config.username,
51
+ proxy_config.password
52
+ )
53
+ else
54
+ Net::HTTP
55
+ end
56
+
57
+ http = http_class.new(uri.host, uri.port)
58
+ http.read_timeout = read_timeout if read_timeout
59
+ http.use_ssl = uri.scheme == 'https'
60
+ http.request(request)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2017, Optimizely and contributors
4
+ # Copyright 2017, 2020, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -48,6 +48,13 @@ module Optimizely
48
48
  logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
49
49
  "'#{variable_type}': #{e.message}.")
50
50
  end
51
+ when 'json'
52
+ begin
53
+ return_value = JSON.parse(value)
54
+ rescue => e
55
+ logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
56
+ "'#{variable_type}': #{e.message}.")
57
+ end
51
58
  else
52
59
  # default case is string
53
60
  return_value = value
@@ -39,20 +39,28 @@ module Optimizely
39
39
 
40
40
  # Adds notification callback to the notification center
41
41
  #
42
- # @param notification_type - One of the constants in NOTIFICATION_TYPES
43
- # @param notification_callback - Function to call when the event is sent
42
+ # @param notification_type - One of the constants in NOTIFICATION_TYPES
43
+ # @param notification_callback [lambda, Method, Callable] (default: block) - Called when the event is sent
44
+ # @yield Block to be used as callback if callback omitted.
44
45
  #
45
46
  # @return [notification ID] Used to remove the notification
46
47
 
47
- def add_notification_listener(notification_type, notification_callback)
48
+ def add_notification_listener(notification_type, notification_callback = nil, &block)
48
49
  return nil unless notification_type_valid?(notification_type)
49
50
 
51
+ if notification_callback && block_given?
52
+ @logger.log Logger::ERROR, 'Callback and block are mutually exclusive.'
53
+ return nil
54
+ end
55
+
56
+ notification_callback ||= block
57
+
50
58
  unless notification_callback
51
59
  @logger.log Logger::ERROR, 'Callback can not be empty.'
52
60
  return nil
53
61
  end
54
62
 
55
- unless notification_callback.is_a? Method
63
+ unless notification_callback.respond_to? :call
56
64
  @logger.log Logger::ERROR, 'Invalid notification callback given.'
57
65
  return nil
58
66
  end
@@ -70,7 +78,7 @@ module Optimizely
70
78
  #
71
79
  # @param notification_id
72
80
  #
73
- # @return [Boolean] The function returns true if found and removed, false otherwise
81
+ # @return [Boolean] true if found and removed, false otherwise
74
82
 
75
83
  def remove_notification_listener(notification_id)
76
84
  unless notification_id
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019, Optimizely and contributors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Optimizely
19
+ class OptimizelyConfig
20
+ def initialize(project_config)
21
+ @project_config = project_config
22
+ end
23
+
24
+ def config
25
+ experiments_map_object = experiments_map
26
+ features_map = get_features_map(experiments_map_object)
27
+ {
28
+ 'experimentsMap' => experiments_map_object,
29
+ 'featuresMap' => features_map,
30
+ 'revision' => @project_config.revision
31
+ }
32
+ end
33
+
34
+ private
35
+
36
+ def experiments_map
37
+ feature_variables_map = @project_config.feature_flags.reduce({}) do |result_map, feature|
38
+ result_map.update(feature['id'] => feature['variables'])
39
+ end
40
+ @project_config.experiments.reduce({}) do |experiments_map, experiment|
41
+ experiments_map.update(
42
+ experiment['key'] => {
43
+ 'id' => experiment['id'],
44
+ 'key' => experiment['key'],
45
+ 'variationsMap' => experiment['variations'].reduce({}) do |variations_map, variation|
46
+ variation_object = {
47
+ 'id' => variation['id'],
48
+ 'key' => variation['key'],
49
+ 'variablesMap' => get_merged_variables_map(variation, experiment['id'], feature_variables_map)
50
+ }
51
+ variation_object['featureEnabled'] = variation['featureEnabled'] if @project_config.feature_experiment?(experiment['id'])
52
+ variations_map.update(variation['key'] => variation_object)
53
+ end
54
+ }
55
+ )
56
+ end
57
+ end
58
+
59
+ # Merges feature key and type from feature variables to variation variables.
60
+ def get_merged_variables_map(variation, experiment_id, feature_variables_map)
61
+ feature_ids = @project_config.experiment_feature_map[experiment_id]
62
+ return {} unless feature_ids
63
+
64
+ experiment_feature_variables = feature_variables_map[feature_ids[0]]
65
+ # temporary variation variables map to get values to merge.
66
+ temp_variables_id_map = {}
67
+ if variation['variables']
68
+ temp_variables_id_map = variation['variables'].reduce({}) do |variables_map, variable|
69
+ variables_map.update(
70
+ variable['id'] => {
71
+ 'id' => variable['id'],
72
+ 'value' => variable['value']
73
+ }
74
+ )
75
+ end
76
+ end
77
+ experiment_feature_variables.reduce({}) do |variables_map, feature_variable|
78
+ variation_variable = temp_variables_id_map[feature_variable['id']]
79
+ variable_value = variation['featureEnabled'] && variation_variable ? variation_variable['value'] : feature_variable['defaultValue']
80
+ variables_map.update(
81
+ feature_variable['key'] => {
82
+ 'id' => feature_variable['id'],
83
+ 'key' => feature_variable['key'],
84
+ 'type' => feature_variable['type'],
85
+ 'value' => variable_value
86
+ }
87
+ )
88
+ end
89
+ end
90
+
91
+ def get_features_map(all_experiments_map)
92
+ @project_config.feature_flags.reduce({}) do |features_map, feature|
93
+ features_map.update(
94
+ feature['key'] => {
95
+ 'id' => feature['id'],
96
+ 'key' => feature['key'],
97
+ 'experimentsMap' => feature['experimentIds'].reduce({}) do |experiments_map, experiment_id|
98
+ experiment_key = @project_config.experiment_id_map[experiment_id]['key']
99
+ experiments_map.update(experiment_key => all_experiments_map[experiment_key])
100
+ end,
101
+ 'variablesMap' => feature['variables'].reduce({}) do |variables, variable|
102
+ variables.update(
103
+ variable['key'] => {
104
+ 'id' => variable['id'],
105
+ 'key' => variable['key'],
106
+ 'type' => variable['type'],
107
+ 'value' => variable['defaultValue']
108
+ }
109
+ )
110
+ end
111
+ }
112
+ )
113
+ end
114
+ end
115
+ end
116
+ end
@@ -17,16 +17,18 @@
17
17
  #
18
18
 
19
19
  require 'optimizely'
20
+ require 'optimizely/error_handler'
20
21
  require 'optimizely/event_dispatcher'
21
22
  require 'optimizely/event/batch_event_processor'
23
+ require 'optimizely/logger'
24
+ require 'optimizely/notification_center'
25
+
22
26
  module Optimizely
23
27
  class OptimizelyFactory
24
- attr_reader :max_event_batch_size, :max_event_flush_interval
25
-
26
28
  # Convenience method for setting the maximum number of events contained within a batch.
27
29
  # @param batch_size Integer - Sets size of EventQueue.
28
30
  # @param logger - Optional LoggerInterface Provides a log method to log messages.
29
- def self.max_event_batch_size(batch_size, logger)
31
+ def self.max_event_batch_size(batch_size, logger = NoOpLogger.new)
30
32
  unless batch_size.is_a? Integer
31
33
  logger.log(
32
34
  Logger::ERROR,
@@ -48,7 +50,7 @@ module Optimizely
48
50
  # Convenience method for setting the maximum time interval in milliseconds between event dispatches.
49
51
  # @param flush_interval Numeric - Time interval between event dispatches.
50
52
  # @param logger - Optional LoggerInterface Provides a log method to log messages.
51
- def self.max_event_flush_interval(flush_interval, logger)
53
+ def self.max_event_flush_interval(flush_interval, logger = NoOpLogger.new)
52
54
  unless flush_interval.is_a? Numeric
53
55
  logger.log(
54
56
  Logger::ERROR,
@@ -67,12 +69,42 @@ module Optimizely
67
69
  @max_event_flush_interval = flush_interval
68
70
  end
69
71
 
72
+ # Convenience method for setting frequency at which datafile has to be polled and ProjectConfig updated.
73
+ #
74
+ # @param polling_interval Numeric - Time in seconds after which to update datafile.
75
+ def self.polling_interval(polling_interval)
76
+ @polling_interval = polling_interval
77
+ end
78
+
79
+ # Convenience method for setting timeout to block the config call until config has been initialized.
80
+ #
81
+ # @param blocking_timeout Numeric - Time in seconds.
82
+ def self.blocking_timeout(blocking_timeout)
83
+ @blocking_timeout = blocking_timeout
84
+ end
85
+
70
86
  # Returns a new optimizely instance.
71
87
  #
72
88
  # @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
73
89
  # @param fallback datafile - Optional JSON string datafile.
74
90
  def self.default_instance(sdk_key, datafile = nil)
75
- Optimizely::Project.new(datafile, nil, nil, nil, nil, nil, sdk_key)
91
+ error_handler = NoOpErrorHandler.new
92
+ logger = NoOpLogger.new
93
+ notification_center = NotificationCenter.new(logger, error_handler)
94
+
95
+ config_manager = Optimizely::HTTPProjectConfigManager.new(
96
+ sdk_key: sdk_key,
97
+ polling_interval: @polling_interval,
98
+ blocking_timeout: @blocking_timeout,
99
+ datafile: datafile,
100
+ logger: logger,
101
+ error_handler: error_handler,
102
+ notification_center: notification_center
103
+ )
104
+
105
+ Optimizely::Project.new(
106
+ datafile, nil, logger, error_handler, nil, nil, sdk_key, config_manager, notification_center
107
+ )
76
108
  end
77
109
 
78
110
  # Returns a new optimizely instance.
@@ -108,10 +140,27 @@ module Optimizely
108
140
  config_manager = nil,
109
141
  notification_center = nil
110
142
  )
143
+
144
+ error_handler ||= NoOpErrorHandler.new
145
+ logger ||= NoOpLogger.new
146
+ notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(logger, error_handler)
147
+
111
148
  event_processor = BatchEventProcessor.new(
112
149
  event_dispatcher: event_dispatcher || EventDispatcher.new,
113
150
  batch_size: @max_event_batch_size,
114
151
  flush_interval: @max_event_flush_interval,
152
+ logger: logger,
153
+ notification_center: notification_center
154
+ )
155
+
156
+ config_manager ||= Optimizely::HTTPProjectConfigManager.new(
157
+ sdk_key: sdk_key,
158
+ polling_interval: @polling_interval,
159
+ blocking_timeout: @blocking_timeout,
160
+ datafile: datafile,
161
+ logger: logger,
162
+ error_handler: error_handler,
163
+ skip_json_validation: skip_json_validation,
115
164
  notification_center: notification_center
116
165
  )
117
166
 
@@ -17,5 +17,5 @@
17
17
  #
18
18
  module Optimizely
19
19
  CLIENT_ENGINE = 'ruby-sdk'
20
- VERSION = '3.3.1'
20
+ VERSION = '3.5.0'
21
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optimizely-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Optimizely
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-10 00:00:00.000000000 Z
11
+ date: 2020-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,20 +94,6 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: httparty
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '0.11'
104
- type: :runtime
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: '0.11'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: json-schema
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -149,6 +135,7 @@ files:
149
135
  - lib/optimizely/bucketer.rb
150
136
  - lib/optimizely/condition_tree_evaluator.rb
151
137
  - lib/optimizely/config/datafile_project_config.rb
138
+ - lib/optimizely/config/proxy_config.rb
152
139
  - lib/optimizely/config_manager/async_scheduler.rb
153
140
  - lib/optimizely/config_manager/http_project_config_manager.rb
154
141
  - lib/optimizely/config_manager/project_config_manager.rb
@@ -178,10 +165,12 @@ files:
178
165
  - lib/optimizely/helpers/date_time_utils.rb
179
166
  - lib/optimizely/helpers/event_tag_utils.rb
180
167
  - lib/optimizely/helpers/group.rb
168
+ - lib/optimizely/helpers/http_utils.rb
181
169
  - lib/optimizely/helpers/validator.rb
182
170
  - lib/optimizely/helpers/variable_type.rb
183
171
  - lib/optimizely/logger.rb
184
172
  - lib/optimizely/notification_center.rb
173
+ - lib/optimizely/optimizely_config.rb
185
174
  - lib/optimizely/optimizely_factory.rb
186
175
  - lib/optimizely/params.rb
187
176
  - lib/optimizely/project_config.rb
@@ -206,8 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
195
  - !ruby/object:Gem::Version
207
196
  version: '0'
208
197
  requirements: []
209
- rubyforge_project:
210
- rubygems_version: 2.5.1
198
+ rubygems_version: 3.0.3
211
199
  signing_key:
212
200
  specification_version: 4
213
201
  summary: Ruby SDK for Optimizely's testing framework