optimizely-sdk 5.0.0 → 5.1.0
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.
- checksums.yaml +4 -4
- data/LICENSE +202 -202
- data/lib/optimizely/audience.rb +127 -127
- data/lib/optimizely/bucketer.rb +156 -156
- data/lib/optimizely/condition_tree_evaluator.rb +123 -123
- data/lib/optimizely/config/datafile_project_config.rb +558 -558
- data/lib/optimizely/config/proxy_config.rb +34 -34
- data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
- data/lib/optimizely/config_manager/http_project_config_manager.rb +340 -340
- data/lib/optimizely/config_manager/project_config_manager.rb +25 -25
- data/lib/optimizely/config_manager/static_project_config_manager.rb +55 -55
- data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
- data/lib/optimizely/decide/optimizely_decision.rb +60 -60
- data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
- data/lib/optimizely/decision_service.rb +589 -563
- data/lib/optimizely/error_handler.rb +39 -39
- data/lib/optimizely/event/batch_event_processor.rb +235 -235
- data/lib/optimizely/event/entity/conversion_event.rb +44 -44
- data/lib/optimizely/event/entity/decision.rb +38 -38
- data/lib/optimizely/event/entity/event_batch.rb +86 -86
- data/lib/optimizely/event/entity/event_context.rb +50 -50
- data/lib/optimizely/event/entity/impression_event.rb +48 -48
- data/lib/optimizely/event/entity/snapshot.rb +33 -33
- data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
- data/lib/optimizely/event/entity/user_event.rb +22 -22
- data/lib/optimizely/event/entity/visitor.rb +36 -36
- data/lib/optimizely/event/entity/visitor_attribute.rb +38 -38
- data/lib/optimizely/event/event_factory.rb +156 -156
- data/lib/optimizely/event/event_processor.rb +25 -25
- data/lib/optimizely/event/forwarding_event_processor.rb +44 -44
- data/lib/optimizely/event/user_event_factory.rb +88 -88
- data/lib/optimizely/event_builder.rb +221 -221
- data/lib/optimizely/event_dispatcher.rb +69 -69
- data/lib/optimizely/exceptions.rb +193 -193
- data/lib/optimizely/helpers/constants.rb +459 -459
- data/lib/optimizely/helpers/date_time_utils.rb +30 -30
- data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
- data/lib/optimizely/helpers/group.rb +31 -31
- data/lib/optimizely/helpers/http_utils.rb +68 -68
- data/lib/optimizely/helpers/sdk_settings.rb +61 -61
- data/lib/optimizely/helpers/validator.rb +236 -236
- data/lib/optimizely/helpers/variable_type.rb +67 -67
- data/lib/optimizely/logger.rb +46 -46
- data/lib/optimizely/notification_center.rb +174 -174
- data/lib/optimizely/notification_center_registry.rb +71 -71
- data/lib/optimizely/odp/lru_cache.rb +114 -114
- data/lib/optimizely/odp/odp_config.rb +102 -102
- data/lib/optimizely/odp/odp_event.rb +75 -75
- data/lib/optimizely/odp/odp_event_api_manager.rb +70 -70
- data/lib/optimizely/odp/odp_event_manager.rb +286 -286
- data/lib/optimizely/odp/odp_manager.rb +159 -159
- data/lib/optimizely/odp/odp_segment_api_manager.rb +122 -122
- data/lib/optimizely/odp/odp_segment_manager.rb +97 -97
- data/lib/optimizely/optimizely_config.rb +273 -273
- data/lib/optimizely/optimizely_factory.rb +183 -184
- data/lib/optimizely/optimizely_user_context.rb +238 -238
- data/lib/optimizely/params.rb +31 -31
- data/lib/optimizely/project_config.rb +99 -99
- data/lib/optimizely/semantic_version.rb +166 -166
- data/lib/optimizely/user_condition_evaluator.rb +391 -391
- data/lib/optimizely/user_profile_service.rb +35 -35
- data/lib/optimizely/user_profile_tracker.rb +64 -0
- data/lib/optimizely/version.rb +21 -21
- data/lib/optimizely.rb +1326 -1262
- metadata +8 -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
|