optimizely-sdk 5.0.0 → 5.0.1

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