optimizely-sdk 3.3.1 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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