optimizely-sdk 3.10.1 → 4.0.1

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +202 -202
  3. data/lib/optimizely/audience.rb +127 -97
  4. data/lib/optimizely/bucketer.rb +156 -156
  5. data/lib/optimizely/condition_tree_evaluator.rb +123 -123
  6. data/lib/optimizely/config/datafile_project_config.rb +539 -552
  7. data/lib/optimizely/config/proxy_config.rb +34 -34
  8. data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
  9. data/lib/optimizely/config_manager/http_project_config_manager.rb +330 -329
  10. data/lib/optimizely/config_manager/project_config_manager.rb +24 -24
  11. data/lib/optimizely/config_manager/static_project_config_manager.rb +53 -52
  12. data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
  13. data/lib/optimizely/decide/optimizely_decision.rb +60 -60
  14. data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
  15. data/lib/optimizely/decision_service.rb +563 -563
  16. data/lib/optimizely/error_handler.rb +39 -39
  17. data/lib/optimizely/event/batch_event_processor.rb +235 -234
  18. data/lib/optimizely/event/entity/conversion_event.rb +44 -43
  19. data/lib/optimizely/event/entity/decision.rb +38 -38
  20. data/lib/optimizely/event/entity/event_batch.rb +86 -86
  21. data/lib/optimizely/event/entity/event_context.rb +50 -50
  22. data/lib/optimizely/event/entity/impression_event.rb +48 -47
  23. data/lib/optimizely/event/entity/snapshot.rb +33 -33
  24. data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
  25. data/lib/optimizely/event/entity/user_event.rb +22 -22
  26. data/lib/optimizely/event/entity/visitor.rb +36 -35
  27. data/lib/optimizely/event/entity/visitor_attribute.rb +38 -37
  28. data/lib/optimizely/event/event_factory.rb +156 -155
  29. data/lib/optimizely/event/event_processor.rb +25 -25
  30. data/lib/optimizely/event/forwarding_event_processor.rb +44 -43
  31. data/lib/optimizely/event/user_event_factory.rb +88 -88
  32. data/lib/optimizely/event_builder.rb +221 -228
  33. data/lib/optimizely/event_dispatcher.rb +71 -71
  34. data/lib/optimizely/exceptions.rb +135 -139
  35. data/lib/optimizely/helpers/constants.rb +415 -397
  36. data/lib/optimizely/helpers/date_time_utils.rb +30 -30
  37. data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
  38. data/lib/optimizely/helpers/group.rb +31 -31
  39. data/lib/optimizely/helpers/http_utils.rb +65 -64
  40. data/lib/optimizely/helpers/validator.rb +183 -183
  41. data/lib/optimizely/helpers/variable_type.rb +67 -67
  42. data/lib/optimizely/logger.rb +46 -45
  43. data/lib/optimizely/notification_center.rb +174 -176
  44. data/lib/optimizely/optimizely_config.rb +271 -272
  45. data/lib/optimizely/optimizely_factory.rb +181 -181
  46. data/lib/optimizely/optimizely_user_context.rb +204 -179
  47. data/lib/optimizely/params.rb +31 -31
  48. data/lib/optimizely/project_config.rb +99 -91
  49. data/lib/optimizely/semantic_version.rb +166 -166
  50. data/lib/optimizely/{custom_attribute_condition_evaluator.rb → user_condition_evaluator.rb} +391 -369
  51. data/lib/optimizely/user_profile_service.rb +35 -35
  52. data/lib/optimizely/version.rb +21 -21
  53. data/lib/optimizely.rb +1130 -1145
  54. metadata +15 -14
@@ -1,329 +1,330 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright 2019-2020, 2022, 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
- require_relative '../config/datafile_project_config'
19
- require_relative '../error_handler'
20
- require_relative '../exceptions'
21
- require_relative '../helpers/constants'
22
- require_relative '../helpers/http_utils'
23
- require_relative '../logger'
24
- require_relative '../notification_center'
25
- require_relative '../project_config'
26
- require_relative '../optimizely_config'
27
- require_relative 'project_config_manager'
28
- require_relative 'async_scheduler'
29
-
30
- require 'json'
31
-
32
- module Optimizely
33
- class HTTPProjectConfigManager < ProjectConfigManager
34
- # Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
35
-
36
- attr_reader :stopped
37
-
38
- # Initialize config manager. One of sdk_key or url has to be set to be able to use.
39
- #
40
- # sdk_key - Optional string uniquely identifying the datafile. It's required unless a URL is passed in.
41
- # datafile: Optional JSON string representing the project.
42
- # polling_interval - Optional floating point number representing time interval in seconds
43
- # at which to request datafile and set ProjectConfig.
44
- # blocking_timeout - Optional Time in seconds to block the config call until config object has been initialized.
45
- # auto_update - Boolean indicates to run infinitely or only once.
46
- # start_by_default - Boolean indicates to start by default AsyncScheduler.
47
- # url - Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key.
48
- # url_template - Optional string template which in conjunction with sdk_key
49
- # determines URL from where to fetch the datafile.
50
- # logger - Provides a logger instance.
51
- # error_handler - Provides a handle_error method to handle exceptions.
52
- # skip_json_validation - Optional boolean param which allows skipping JSON schema
53
- # validation upon object invocation. By default JSON schema validation will be performed.
54
- # datafile_access_token - access token used to fetch private datafiles
55
- # proxy_config - Optional proxy config instancea to configure making web requests through a proxy server.
56
- def initialize(
57
- sdk_key: nil,
58
- url: nil,
59
- url_template: nil,
60
- polling_interval: nil,
61
- blocking_timeout: nil,
62
- auto_update: true,
63
- start_by_default: true,
64
- datafile: nil,
65
- logger: nil,
66
- error_handler: nil,
67
- skip_json_validation: false,
68
- notification_center: nil,
69
- datafile_access_token: nil,
70
- proxy_config: nil
71
- )
72
- @logger = logger || NoOpLogger.new
73
- @error_handler = error_handler || NoOpErrorHandler.new
74
- @access_token = datafile_access_token
75
- @datafile_url = get_datafile_url(sdk_key, url, url_template)
76
- @polling_interval = nil
77
- polling_interval(polling_interval)
78
- @blocking_timeout = nil
79
- blocking_timeout(blocking_timeout)
80
- @last_modified = nil
81
- @skip_json_validation = skip_json_validation
82
- @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
83
- @optimizely_config = nil
84
- @config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
85
- @mutex = Mutex.new
86
- @resource = ConditionVariable.new
87
- @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
88
- # Start async scheduler in the end to avoid race condition where scheduler executes
89
- # callback which makes use of variables not yet initialized by the main thread.
90
- @async_scheduler.start! if start_by_default == true
91
- @proxy_config = proxy_config
92
- @stopped = false
93
- end
94
-
95
- def ready?
96
- !@config.nil?
97
- end
98
-
99
- def start!
100
- if @stopped
101
- @logger.log(Logger::WARN, 'Not starting. Already stopped.')
102
- return
103
- end
104
-
105
- @async_scheduler.start!
106
- @stopped = false
107
- end
108
-
109
- def stop!
110
- if @stopped
111
- @logger.log(Logger::WARN, 'Not pausing. Manager has not been started.')
112
- return
113
- end
114
-
115
- @async_scheduler.stop!
116
- @config = nil
117
- @stopped = true
118
- end
119
-
120
- def config
121
- # Get Project Config.
122
-
123
- # if stopped is true, then simply return @config.
124
- # If the background datafile polling thread is running. and config has been initalized,
125
- # we simply return @config.
126
- # If it is not, we wait and block maximum for @blocking_timeout.
127
- # If thread is not running, we fetch the datafile and update config.
128
- return @config if @stopped
129
-
130
- if @async_scheduler.running
131
- return @config if ready?
132
-
133
- @mutex.synchronize do
134
- @resource.wait(@mutex, @blocking_timeout)
135
- return @config
136
- end
137
- end
138
-
139
- fetch_datafile_config
140
- @config
141
- end
142
-
143
- def optimizely_config
144
- @optimizely_config = OptimizelyConfig.new(@config).config if @optimizely_config.nil?
145
-
146
- @optimizely_config
147
- end
148
-
149
- private
150
-
151
- def fetch_datafile_config
152
- # Fetch datafile, handle response and send notification on config update.
153
- config = request_config
154
- return unless config
155
-
156
- set_config config
157
- end
158
-
159
- def request_config
160
- @logger.log(Logger::DEBUG, "Fetching datafile from #{@datafile_url}")
161
- headers = {}
162
- headers['Content-Type'] = 'application/json'
163
- headers['If-Modified-Since'] = @last_modified if @last_modified
164
- headers['Authorization'] = "Bearer #{@access_token}" unless @access_token.nil?
165
-
166
- # Cleaning headers before logging to avoid exposing authorization token
167
- cleansed_headers = {}
168
- headers.each { |key, value| cleansed_headers[key] = key == 'Authorization' ? '********' : value }
169
- @logger.log(Logger::DEBUG, "Datafile request headers: #{cleansed_headers}")
170
-
171
- begin
172
- response = Helpers::HttpUtils.make_request(
173
- @datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
174
- )
175
- rescue StandardError => e
176
- @logger.log(
177
- Logger::ERROR,
178
- "Fetching datafile from #{@datafile_url} failed. Error: #{e}"
179
- )
180
- return nil
181
- end
182
-
183
- response_code = response.code.to_i
184
- @logger.log(Logger::DEBUG, "Datafile response status code #{response_code}")
185
-
186
- # Leave datafile and config unchanged if it has not been modified.
187
- if response.code == '304'
188
- @logger.log(
189
- Logger::DEBUG,
190
- "Not updating config as datafile has not updated since #{@last_modified}."
191
- )
192
- return
193
- end
194
-
195
- if response_code >= 200 && response_code < 400
196
- @logger.log(Logger::DEBUG, 'Successfully fetched datafile, generating Project config')
197
- config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation)
198
- @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
199
- @logger.log(Logger::DEBUG, "Saved last modified header value from response: #{@last_modified}.")
200
- else
201
- @logger.log(Logger::DEBUG, "Datafile fetch failed, status: #{response.code}, message: #{response.message}")
202
- end
203
-
204
- config
205
- end
206
-
207
- def set_config(config)
208
- # Send notification if project config is updated.
209
- previous_revision = @config.revision if @config
210
- return if previous_revision == config.revision
211
-
212
- unless ready?
213
- @config = config
214
- @mutex.synchronize { @resource.signal }
215
- end
216
-
217
- @config = config
218
-
219
- # clearing old optimizely config so that a fresh one is generated on the next api call.
220
- @optimizely_config = nil
221
-
222
- @notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
223
-
224
- @logger.log(Logger::DEBUG, 'Received new datafile and updated config. ' \
225
- "Old revision number: #{previous_revision}. New revision number: #{@config.revision}.")
226
- end
227
-
228
- def polling_interval(polling_interval)
229
- # Sets frequency at which datafile has to be polled and ProjectConfig updated.
230
- #
231
- # polling_interval - Time in seconds after which to update datafile.
232
-
233
- # If valid set given polling interval, default update interval otherwise.
234
-
235
- if polling_interval.nil?
236
- @logger.log(
237
- Logger::DEBUG,
238
- "Polling interval is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
239
- )
240
- @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
241
- return
242
- end
243
-
244
- unless polling_interval.is_a? Numeric
245
- @logger.log(
246
- Logger::ERROR,
247
- "Polling interval '#{polling_interval}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
248
- )
249
- @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
250
- return
251
- end
252
-
253
- unless polling_interval.positive? && polling_interval <= Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT']
254
- @logger.log(
255
- Logger::DEBUG,
256
- "Polling interval '#{polling_interval}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
257
- )
258
- @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
259
- return
260
- end
261
-
262
- @polling_interval = polling_interval
263
- end
264
-
265
- def blocking_timeout(blocking_timeout)
266
- # Sets time in seconds to block the config call until config has been initialized.
267
- #
268
- # blocking_timeout - Time in seconds to block the config call.
269
-
270
- # If valid set given timeout, default blocking_timeout otherwise.
271
-
272
- if blocking_timeout.nil?
273
- @logger.log(
274
- Logger::DEBUG,
275
- "Blocking timeout is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
276
- )
277
- @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
278
- return
279
- end
280
-
281
- unless blocking_timeout.is_a? Integer
282
- @logger.log(
283
- Logger::ERROR,
284
- "Blocking timeout '#{blocking_timeout}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
285
- )
286
- @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
287
- return
288
- end
289
-
290
- unless blocking_timeout.between?(Helpers::Constants::CONFIG_MANAGER['MIN_SECONDS_LIMIT'], Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT'])
291
- @logger.log(
292
- Logger::DEBUG,
293
- "Blocking timeout '#{blocking_timeout}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
294
- )
295
- @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
296
- return
297
- end
298
-
299
- @blocking_timeout = blocking_timeout
300
- end
301
-
302
- def get_datafile_url(sdk_key, url, url_template)
303
- # Determines URL from where to fetch the datafile.
304
- # sdk_key - Key uniquely identifying the datafile.
305
- # url - String representing URL from which to fetch the datafile.
306
- # url_template - String representing template which is filled in with
307
- # SDK key to determine URL from which to fetch the datafile.
308
- # Returns String representing URL to fetch datafile from.
309
- if sdk_key.nil? && url.nil?
310
- error_msg = 'Must provide at least one of sdk_key or url.'
311
- @logger.log(Logger::ERROR, error_msg)
312
- @error_handler.handle_error(InvalidInputsError.new(error_msg))
313
- end
314
-
315
- unless url
316
- url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
317
- begin
318
- return (url_template % sdk_key)
319
- rescue
320
- error_msg = "Invalid url_template #{url_template} provided."
321
- @logger.log(Logger::ERROR, error_msg)
322
- @error_handler.handle_error(InvalidInputsError.new(error_msg))
323
- end
324
- end
325
-
326
- url
327
- end
328
- end
329
- end
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019-2020, 2022, 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
+ require_relative '../config/datafile_project_config'
19
+ require_relative '../error_handler'
20
+ require_relative '../exceptions'
21
+ require_relative '../helpers/constants'
22
+ require_relative '../helpers/http_utils'
23
+ require_relative '../logger'
24
+ require_relative '../notification_center'
25
+ require_relative '../project_config'
26
+ require_relative '../optimizely_config'
27
+ require_relative 'project_config_manager'
28
+ require_relative 'async_scheduler'
29
+
30
+ require 'json'
31
+
32
+ module Optimizely
33
+ class HTTPProjectConfigManager < ProjectConfigManager
34
+ # Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
35
+
36
+ attr_reader :stopped
37
+
38
+ # Initialize config manager. One of sdk_key or url has to be set to be able to use.
39
+ #
40
+ # sdk_key - Optional string uniquely identifying the datafile. It's required unless a URL is passed in.
41
+ # datafile: Optional JSON string representing the project.
42
+ # polling_interval - Optional floating point number representing time interval in seconds
43
+ # at which to request datafile and set ProjectConfig.
44
+ # blocking_timeout - Optional Time in seconds to block the config call until config object has been initialized.
45
+ # auto_update - Boolean indicates to run infinitely or only once.
46
+ # start_by_default - Boolean indicates to start by default AsyncScheduler.
47
+ # url - Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key.
48
+ # url_template - Optional string template which in conjunction with sdk_key
49
+ # determines URL from where to fetch the datafile.
50
+ # logger - Provides a logger instance.
51
+ # error_handler - Provides a handle_error method to handle exceptions.
52
+ # skip_json_validation - Optional boolean param which allows skipping JSON schema
53
+ # validation upon object invocation. By default JSON schema validation will be performed.
54
+ # datafile_access_token - access token used to fetch private datafiles
55
+ # proxy_config - Optional proxy config instancea to configure making web requests through a proxy server.
56
+ def initialize(
57
+ sdk_key: nil,
58
+ url: nil,
59
+ url_template: nil,
60
+ polling_interval: nil,
61
+ blocking_timeout: nil,
62
+ auto_update: true,
63
+ start_by_default: true,
64
+ datafile: nil,
65
+ logger: nil,
66
+ error_handler: nil,
67
+ skip_json_validation: false,
68
+ notification_center: nil,
69
+ datafile_access_token: nil,
70
+ proxy_config: nil
71
+ )
72
+ super()
73
+ @logger = logger || NoOpLogger.new
74
+ @error_handler = error_handler || NoOpErrorHandler.new
75
+ @access_token = datafile_access_token
76
+ @datafile_url = get_datafile_url(sdk_key, url, url_template)
77
+ @polling_interval = nil
78
+ polling_interval(polling_interval)
79
+ @blocking_timeout = nil
80
+ blocking_timeout(blocking_timeout)
81
+ @last_modified = nil
82
+ @skip_json_validation = skip_json_validation
83
+ @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
84
+ @optimizely_config = nil
85
+ @config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
86
+ @mutex = Mutex.new
87
+ @resource = ConditionVariable.new
88
+ @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
89
+ # Start async scheduler in the end to avoid race condition where scheduler executes
90
+ # callback which makes use of variables not yet initialized by the main thread.
91
+ @async_scheduler.start! if start_by_default == true
92
+ @proxy_config = proxy_config
93
+ @stopped = false
94
+ end
95
+
96
+ def ready?
97
+ !@config.nil?
98
+ end
99
+
100
+ def start!
101
+ if @stopped
102
+ @logger.log(Logger::WARN, 'Not starting. Already stopped.')
103
+ return
104
+ end
105
+
106
+ @async_scheduler.start!
107
+ @stopped = false
108
+ end
109
+
110
+ def stop!
111
+ if @stopped
112
+ @logger.log(Logger::WARN, 'Not pausing. Manager has not been started.')
113
+ return
114
+ end
115
+
116
+ @async_scheduler.stop!
117
+ @config = nil
118
+ @stopped = true
119
+ end
120
+
121
+ def config
122
+ # Get Project Config.
123
+
124
+ # if stopped is true, then simply return @config.
125
+ # If the background datafile polling thread is running. and config has been initalized,
126
+ # we simply return @config.
127
+ # If it is not, we wait and block maximum for @blocking_timeout.
128
+ # If thread is not running, we fetch the datafile and update config.
129
+ return @config if @stopped
130
+
131
+ if @async_scheduler.running
132
+ return @config if ready?
133
+
134
+ @mutex.synchronize do
135
+ @resource.wait(@mutex, @blocking_timeout)
136
+ return @config
137
+ end
138
+ end
139
+
140
+ fetch_datafile_config
141
+ @config
142
+ end
143
+
144
+ def optimizely_config
145
+ @optimizely_config = OptimizelyConfig.new(@config).config if @optimizely_config.nil?
146
+
147
+ @optimizely_config
148
+ end
149
+
150
+ private
151
+
152
+ def fetch_datafile_config
153
+ # Fetch datafile, handle response and send notification on config update.
154
+ config = request_config
155
+ return unless config
156
+
157
+ set_config config
158
+ end
159
+
160
+ def request_config
161
+ @logger.log(Logger::DEBUG, "Fetching datafile from #{@datafile_url}")
162
+ headers = {}
163
+ headers['Content-Type'] = 'application/json'
164
+ headers['If-Modified-Since'] = @last_modified if @last_modified
165
+ headers['Authorization'] = "Bearer #{@access_token}" unless @access_token.nil?
166
+
167
+ # Cleaning headers before logging to avoid exposing authorization token
168
+ cleansed_headers = {}
169
+ headers.each { |key, value| cleansed_headers[key] = key == 'Authorization' ? '********' : value }
170
+ @logger.log(Logger::DEBUG, "Datafile request headers: #{cleansed_headers}")
171
+
172
+ begin
173
+ response = Helpers::HttpUtils.make_request(
174
+ @datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
175
+ )
176
+ rescue StandardError => e
177
+ @logger.log(
178
+ Logger::ERROR,
179
+ "Fetching datafile from #{@datafile_url} failed. Error: #{e}"
180
+ )
181
+ return nil
182
+ end
183
+
184
+ response_code = response.code.to_i
185
+ @logger.log(Logger::DEBUG, "Datafile response status code #{response_code}")
186
+
187
+ # Leave datafile and config unchanged if it has not been modified.
188
+ if response.code == '304'
189
+ @logger.log(
190
+ Logger::DEBUG,
191
+ "Not updating config as datafile has not updated since #{@last_modified}."
192
+ )
193
+ return
194
+ end
195
+
196
+ if response_code >= 200 && response_code < 400
197
+ @logger.log(Logger::DEBUG, 'Successfully fetched datafile, generating Project config')
198
+ config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation)
199
+ @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
200
+ @logger.log(Logger::DEBUG, "Saved last modified header value from response: #{@last_modified}.")
201
+ else
202
+ @logger.log(Logger::DEBUG, "Datafile fetch failed, status: #{response.code}, message: #{response.message}")
203
+ end
204
+
205
+ config
206
+ end
207
+
208
+ def set_config(config)
209
+ # Send notification if project config is updated.
210
+ previous_revision = @config.revision if @config
211
+ return if previous_revision == config.revision
212
+
213
+ unless ready?
214
+ @config = config
215
+ @mutex.synchronize { @resource.signal }
216
+ end
217
+
218
+ @config = config
219
+
220
+ # clearing old optimizely config so that a fresh one is generated on the next api call.
221
+ @optimizely_config = nil
222
+
223
+ @notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
224
+
225
+ @logger.log(Logger::DEBUG, 'Received new datafile and updated config. ' \
226
+ "Old revision number: #{previous_revision}. New revision number: #{@config.revision}.")
227
+ end
228
+
229
+ def polling_interval(polling_interval)
230
+ # Sets frequency at which datafile has to be polled and ProjectConfig updated.
231
+ #
232
+ # polling_interval - Time in seconds after which to update datafile.
233
+
234
+ # If valid set given polling interval, default update interval otherwise.
235
+
236
+ if polling_interval.nil?
237
+ @logger.log(
238
+ Logger::DEBUG,
239
+ "Polling interval is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
240
+ )
241
+ @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
242
+ return
243
+ end
244
+
245
+ unless polling_interval.is_a? Numeric
246
+ @logger.log(
247
+ Logger::ERROR,
248
+ "Polling interval '#{polling_interval}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
249
+ )
250
+ @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
251
+ return
252
+ end
253
+
254
+ unless polling_interval.positive? && polling_interval <= Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT']
255
+ @logger.log(
256
+ Logger::DEBUG,
257
+ "Polling interval '#{polling_interval}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
258
+ )
259
+ @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
260
+ return
261
+ end
262
+
263
+ @polling_interval = polling_interval
264
+ end
265
+
266
+ def blocking_timeout(blocking_timeout)
267
+ # Sets time in seconds to block the config call until config has been initialized.
268
+ #
269
+ # blocking_timeout - Time in seconds to block the config call.
270
+
271
+ # If valid set given timeout, default blocking_timeout otherwise.
272
+
273
+ if blocking_timeout.nil?
274
+ @logger.log(
275
+ Logger::DEBUG,
276
+ "Blocking timeout is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
277
+ )
278
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
279
+ return
280
+ end
281
+
282
+ unless blocking_timeout.is_a? Integer
283
+ @logger.log(
284
+ Logger::ERROR,
285
+ "Blocking timeout '#{blocking_timeout}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
286
+ )
287
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
288
+ return
289
+ end
290
+
291
+ unless blocking_timeout.between?(Helpers::Constants::CONFIG_MANAGER['MIN_SECONDS_LIMIT'], Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT'])
292
+ @logger.log(
293
+ Logger::DEBUG,
294
+ "Blocking timeout '#{blocking_timeout}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
295
+ )
296
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
297
+ return
298
+ end
299
+
300
+ @blocking_timeout = blocking_timeout
301
+ end
302
+
303
+ def get_datafile_url(sdk_key, url, url_template)
304
+ # Determines URL from where to fetch the datafile.
305
+ # sdk_key - Key uniquely identifying the datafile.
306
+ # url - String representing URL from which to fetch the datafile.
307
+ # url_template - String representing template which is filled in with
308
+ # SDK key to determine URL from which to fetch the datafile.
309
+ # Returns String representing URL to fetch datafile from.
310
+ if sdk_key.nil? && url.nil?
311
+ error_msg = 'Must provide at least one of sdk_key or url.'
312
+ @logger.log(Logger::ERROR, error_msg)
313
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
314
+ end
315
+
316
+ unless url
317
+ url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
318
+ begin
319
+ return (url_template % sdk_key)
320
+ rescue
321
+ error_msg = "Invalid url_template #{url_template} provided."
322
+ @logger.log(Logger::ERROR, error_msg)
323
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
324
+ end
325
+ end
326
+
327
+ url
328
+ end
329
+ end
330
+ end