optimizely-sdk 5.0.0.pre.beta → 5.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +202 -202
  3. data/lib/optimizely/audience.rb +127 -127
  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 +48 -32
  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 +340 -338
  10. data/lib/optimizely/config_manager/project_config_manager.rb +25 -25
  11. data/lib/optimizely/config_manager/static_project_config_manager.rb +55 -54
  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 -235
  18. data/lib/optimizely/event/entity/conversion_event.rb +44 -44
  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 -48
  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 -36
  27. data/lib/optimizely/event/entity/visitor_attribute.rb +38 -38
  28. data/lib/optimizely/event/event_factory.rb +156 -156
  29. data/lib/optimizely/event/event_processor.rb +25 -25
  30. data/lib/optimizely/event/forwarding_event_processor.rb +44 -44
  31. data/lib/optimizely/event/user_event_factory.rb +88 -88
  32. data/lib/optimizely/event_builder.rb +221 -221
  33. data/lib/optimizely/event_dispatcher.rb +69 -69
  34. data/lib/optimizely/exceptions.rb +193 -149
  35. data/lib/optimizely/helpers/constants.rb +459 -459
  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 +68 -68
  40. data/lib/optimizely/helpers/sdk_settings.rb +61 -61
  41. data/lib/optimizely/helpers/validator.rb +236 -238
  42. data/lib/optimizely/helpers/variable_type.rb +67 -67
  43. data/lib/optimizely/logger.rb +46 -46
  44. data/lib/optimizely/notification_center.rb +174 -174
  45. data/lib/optimizely/notification_center_registry.rb +71 -71
  46. data/lib/optimizely/odp/lru_cache.rb +114 -114
  47. data/lib/optimizely/odp/odp_config.rb +102 -102
  48. data/lib/optimizely/odp/odp_event.rb +75 -75
  49. data/lib/optimizely/odp/odp_event_api_manager.rb +70 -70
  50. data/lib/optimizely/odp/odp_event_manager.rb +286 -286
  51. data/lib/optimizely/odp/odp_manager.rb +159 -159
  52. data/lib/optimizely/odp/odp_segment_api_manager.rb +122 -122
  53. data/lib/optimizely/odp/odp_segment_manager.rb +97 -97
  54. data/lib/optimizely/optimizely_config.rb +273 -271
  55. data/lib/optimizely/optimizely_factory.rb +184 -186
  56. data/lib/optimizely/optimizely_user_context.rb +238 -238
  57. data/lib/optimizely/params.rb +31 -31
  58. data/lib/optimizely/project_config.rb +99 -99
  59. data/lib/optimizely/semantic_version.rb +166 -166
  60. data/lib/optimizely/user_condition_evaluator.rb +391 -391
  61. data/lib/optimizely/user_profile_service.rb +35 -35
  62. data/lib/optimizely/version.rb +21 -21
  63. data/lib/optimizely.rb +1262 -1262
  64. metadata +12 -10
@@ -1,338 +1,340 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright 2019-2020, 2022-2023, 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, :sdk_key
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 datafile with sdk_key is passed in.
41
- # datafile - Optional JSON string representing the project. If nil, sdk_key is required.
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
- @sdk_key = sdk_key || @config&.sdk_key
87
-
88
- raise MissingSdkKeyError if @sdk_key.nil?
89
-
90
- @mutex = Mutex.new
91
- @resource = ConditionVariable.new
92
- @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
93
- # Start async scheduler in the end to avoid race condition where scheduler executes
94
- # callback which makes use of variables not yet initialized by the main thread.
95
- @async_scheduler.start! if start_by_default == true
96
- @proxy_config = proxy_config
97
- @stopped = false
98
- end
99
-
100
- def ready?
101
- !@config.nil?
102
- end
103
-
104
- def start!
105
- if @stopped
106
- @logger.log(Logger::WARN, 'Not starting. Already stopped.')
107
- return
108
- end
109
-
110
- @async_scheduler.start!
111
- @stopped = false
112
- end
113
-
114
- def stop!
115
- if @stopped
116
- @logger.log(Logger::WARN, 'Not pausing. Manager has not been started.')
117
- return
118
- end
119
-
120
- @async_scheduler.stop!
121
- @config = nil
122
- @stopped = true
123
- end
124
-
125
- def config
126
- # Get Project Config.
127
-
128
- # if stopped is true, then simply return @config.
129
- # If the background datafile polling thread is running. and config has been initalized,
130
- # we simply return @config.
131
- # If it is not, we wait and block maximum for @blocking_timeout.
132
- # If thread is not running, we fetch the datafile and update config.
133
- return @config if @stopped
134
-
135
- if @async_scheduler.running
136
- return @config if ready?
137
-
138
- @mutex.synchronize do
139
- @resource.wait(@mutex, @blocking_timeout)
140
- return @config
141
- end
142
- end
143
-
144
- fetch_datafile_config
145
- @config
146
- end
147
-
148
- def optimizely_config
149
- @optimizely_config = OptimizelyConfig.new(@config).config if @optimizely_config.nil?
150
-
151
- @optimizely_config
152
- end
153
-
154
- private
155
-
156
- def fetch_datafile_config
157
- # Fetch datafile, handle response and send notification on config update.
158
- config = request_config
159
- return unless config
160
-
161
- set_config config
162
- end
163
-
164
- def request_config
165
- @logger.log(Logger::DEBUG, "Fetching datafile from #{@datafile_url}")
166
- headers = {}
167
- headers['Content-Type'] = 'application/json'
168
- headers['If-Modified-Since'] = @last_modified if @last_modified
169
- headers['Authorization'] = "Bearer #{@access_token}" unless @access_token.nil?
170
-
171
- # Cleaning headers before logging to avoid exposing authorization token
172
- cleansed_headers = {}
173
- headers.each { |key, value| cleansed_headers[key] = key == 'Authorization' ? '********' : value }
174
- @logger.log(Logger::DEBUG, "Datafile request headers: #{cleansed_headers}")
175
-
176
- begin
177
- response = Helpers::HttpUtils.make_request(
178
- @datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
179
- )
180
- rescue StandardError => e
181
- @logger.log(
182
- Logger::ERROR,
183
- "Fetching datafile from #{@datafile_url} failed. Error: #{e}"
184
- )
185
- return nil
186
- end
187
-
188
- response_code = response.code.to_i
189
- @logger.log(Logger::DEBUG, "Datafile response status code #{response_code}")
190
-
191
- # Leave datafile and config unchanged if it has not been modified.
192
- if response.code == '304'
193
- @logger.log(
194
- Logger::DEBUG,
195
- "Not updating config as datafile has not updated since #{@last_modified}."
196
- )
197
- return
198
- end
199
-
200
- if response_code >= 200 && response_code < 400
201
- @logger.log(Logger::DEBUG, 'Successfully fetched datafile, generating Project config')
202
- config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation)
203
- @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
204
- @logger.log(Logger::DEBUG, "Saved last modified header value from response: #{@last_modified}.")
205
- else
206
- @logger.log(Logger::DEBUG, "Datafile fetch failed, status: #{response.code}, message: #{response.message}")
207
- end
208
-
209
- config
210
- end
211
-
212
- def set_config(config)
213
- # Send notification if project config is updated.
214
- previous_revision = @config.revision if @config
215
- return if previous_revision == config.revision
216
-
217
- unless ready?
218
- @config = config
219
- @mutex.synchronize { @resource.signal }
220
- end
221
-
222
- @config = config
223
-
224
- # clearing old optimizely config so that a fresh one is generated on the next api call.
225
- @optimizely_config = nil
226
-
227
- @notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
228
-
229
- NotificationCenterRegistry
230
- .get_notification_center(@sdk_key, @logger)
231
- &.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
232
-
233
- @logger.log(Logger::DEBUG, 'Received new datafile and updated config. ' \
234
- "Old revision number: #{previous_revision}. New revision number: #{@config.revision}.")
235
- end
236
-
237
- def polling_interval(polling_interval)
238
- # Sets frequency at which datafile has to be polled and ProjectConfig updated.
239
- #
240
- # polling_interval - Time in seconds after which to update datafile.
241
-
242
- # If valid set given polling interval, default update interval otherwise.
243
-
244
- if polling_interval.nil?
245
- @logger.log(
246
- Logger::DEBUG,
247
- "Polling interval is not provided. 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.is_a? Numeric
254
- @logger.log(
255
- Logger::ERROR,
256
- "Polling interval '#{polling_interval}' has invalid type. 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
- unless polling_interval.positive? && polling_interval <= Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT']
263
- @logger.log(
264
- Logger::DEBUG,
265
- "Polling interval '#{polling_interval}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
266
- )
267
- @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
268
- return
269
- end
270
-
271
- @polling_interval = polling_interval
272
- end
273
-
274
- def blocking_timeout(blocking_timeout)
275
- # Sets time in seconds to block the config call until config has been initialized.
276
- #
277
- # blocking_timeout - Time in seconds to block the config call.
278
-
279
- # If valid set given timeout, default blocking_timeout otherwise.
280
-
281
- if blocking_timeout.nil?
282
- @logger.log(
283
- Logger::DEBUG,
284
- "Blocking timeout is not provided. 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.is_a? Integer
291
- @logger.log(
292
- Logger::ERROR,
293
- "Blocking timeout '#{blocking_timeout}' has invalid type. 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
- unless blocking_timeout.between?(Helpers::Constants::CONFIG_MANAGER['MIN_SECONDS_LIMIT'], Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT'])
300
- @logger.log(
301
- Logger::DEBUG,
302
- "Blocking timeout '#{blocking_timeout}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
303
- )
304
- @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
305
- return
306
- end
307
-
308
- @blocking_timeout = blocking_timeout
309
- end
310
-
311
- def get_datafile_url(sdk_key, url, url_template)
312
- # Determines URL from where to fetch the datafile.
313
- # sdk_key - Key uniquely identifying the datafile.
314
- # url - String representing URL from which to fetch the datafile.
315
- # url_template - String representing template which is filled in with
316
- # SDK key to determine URL from which to fetch the datafile.
317
- # Returns String representing URL to fetch datafile from.
318
- if sdk_key.nil? && url.nil?
319
- error_msg = 'Must provide at least one of sdk_key or url.'
320
- @logger.log(Logger::ERROR, error_msg)
321
- @error_handler.handle_error(InvalidInputsError.new(error_msg))
322
- end
323
-
324
- unless url
325
- url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
326
- begin
327
- return (url_template % sdk_key)
328
- rescue
329
- error_msg = "Invalid url_template #{url_template} provided."
330
- @logger.log(Logger::ERROR, error_msg)
331
- @error_handler.handle_error(InvalidInputsError.new(error_msg))
332
- end
333
- end
334
-
335
- url
336
- end
337
- end
338
- end
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019-2020, 2022-2023, 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, :sdk_key
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 datafile with sdk_key is passed in.
41
+ # datafile - Optional JSON string representing the project. If nil, sdk_key is required.
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
+ @sdk_key = sdk_key || @config&.sdk_key
87
+
88
+ raise MissingSdkKeyError if @sdk_key.nil?
89
+
90
+ @mutex = Mutex.new
91
+ @resource = ConditionVariable.new
92
+ @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
93
+ # Start async scheduler in the end to avoid race condition where scheduler executes
94
+ # callback which makes use of variables not yet initialized by the main thread.
95
+ @async_scheduler.start! if start_by_default == true
96
+ @proxy_config = proxy_config
97
+ @stopped = false
98
+ end
99
+
100
+ def ready?
101
+ !@config.nil?
102
+ end
103
+
104
+ def start!
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, @logger).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
+ NotificationCenterRegistry
225
+ .get_notification_center(@sdk_key, @logger)
226
+ &.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
227
+
228
+ @logger.log(Logger::DEBUG, 'Received new datafile and updated config. ' \
229
+ "Old revision number: #{previous_revision}. New revision number: #{@config.revision}.")
230
+ end
231
+
232
+ def polling_interval(polling_interval)
233
+ # Sets frequency at which datafile has to be polled and ProjectConfig updated.
234
+ #
235
+ # polling_interval - Time in seconds after which to update datafile.
236
+
237
+ # If valid set given polling interval, default update interval otherwise.
238
+
239
+ if polling_interval.nil?
240
+ @logger.log(
241
+ Logger::DEBUG,
242
+ "Polling interval is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
243
+ )
244
+ @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
245
+ return
246
+ end
247
+
248
+ unless polling_interval.is_a? Numeric
249
+ @logger.log(
250
+ Logger::ERROR,
251
+ "Polling interval '#{polling_interval}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
252
+ )
253
+ @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
254
+ return
255
+ end
256
+
257
+ unless polling_interval.positive? && polling_interval <= Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT']
258
+ @logger.log(
259
+ Logger::DEBUG,
260
+ "Polling interval '#{polling_interval}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']} seconds."
261
+ )
262
+ @polling_interval = Helpers::Constants::CONFIG_MANAGER['DEFAULT_UPDATE_INTERVAL']
263
+ return
264
+ end
265
+
266
+ if polling_interval < 30
267
+ @logger.log(
268
+ Logger::WARN,
269
+ 'Polling intervals below 30 seconds are not recommended.'
270
+ )
271
+ end
272
+
273
+ @polling_interval = polling_interval
274
+ end
275
+
276
+ def blocking_timeout(blocking_timeout)
277
+ # Sets time in seconds to block the config call until config has been initialized.
278
+ #
279
+ # blocking_timeout - Time in seconds to block the config call.
280
+
281
+ # If valid set given timeout, default blocking_timeout otherwise.
282
+
283
+ if blocking_timeout.nil?
284
+ @logger.log(
285
+ Logger::DEBUG,
286
+ "Blocking timeout is not provided. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
287
+ )
288
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
289
+ return
290
+ end
291
+
292
+ unless blocking_timeout.is_a? Integer
293
+ @logger.log(
294
+ Logger::ERROR,
295
+ "Blocking timeout '#{blocking_timeout}' has invalid type. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
296
+ )
297
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
298
+ return
299
+ end
300
+
301
+ unless blocking_timeout.between?(Helpers::Constants::CONFIG_MANAGER['MIN_SECONDS_LIMIT'], Helpers::Constants::CONFIG_MANAGER['MAX_SECONDS_LIMIT'])
302
+ @logger.log(
303
+ Logger::DEBUG,
304
+ "Blocking timeout '#{blocking_timeout}' has invalid range. Defaulting to #{Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']} seconds."
305
+ )
306
+ @blocking_timeout = Helpers::Constants::CONFIG_MANAGER['DEFAULT_BLOCKING_TIMEOUT']
307
+ return
308
+ end
309
+
310
+ @blocking_timeout = blocking_timeout
311
+ end
312
+
313
+ def get_datafile_url(sdk_key, url, url_template)
314
+ # Determines URL from where to fetch the datafile.
315
+ # sdk_key - Key uniquely identifying the datafile.
316
+ # url - String representing URL from which to fetch the datafile.
317
+ # url_template - String representing template which is filled in with
318
+ # SDK key to determine URL from which to fetch the datafile.
319
+ # Returns String representing URL to fetch datafile from.
320
+ if sdk_key.nil? && url.nil?
321
+ error_msg = 'Must provide at least one of sdk_key or url.'
322
+ @logger.log(Logger::ERROR, error_msg)
323
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
324
+ end
325
+
326
+ unless url
327
+ url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
328
+ begin
329
+ return (url_template % sdk_key)
330
+ rescue
331
+ error_msg = "Invalid url_template #{url_template} provided."
332
+ @logger.log(Logger::ERROR, error_msg)
333
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
334
+ end
335
+ end
336
+
337
+ url
338
+ end
339
+ end
340
+ end