optimizely-sdk 5.0.0 → 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 +558 -558
  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 -340
  10. data/lib/optimizely/config_manager/project_config_manager.rb +25 -25
  11. data/lib/optimizely/config_manager/static_project_config_manager.rb +55 -55
  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 -193
  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 -236
  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 -273
  55. data/lib/optimizely/optimizely_factory.rb +184 -184
  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 +7 -5
@@ -1,340 +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
- @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
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