optimizely-sdk 3.3.1 → 3.5.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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2019, Optimizely and contributors
4
+ # Copyright 2019-2020, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -19,18 +19,21 @@ require_relative '../config/datafile_project_config'
19
19
  require_relative '../error_handler'
20
20
  require_relative '../exceptions'
21
21
  require_relative '../helpers/constants'
22
+ require_relative '../helpers/http_utils'
22
23
  require_relative '../logger'
23
24
  require_relative '../notification_center'
24
25
  require_relative '../project_config'
26
+ require_relative '../optimizely_config'
25
27
  require_relative 'project_config_manager'
26
28
  require_relative 'async_scheduler'
27
- require 'httparty'
29
+
28
30
  require 'json'
31
+
29
32
  module Optimizely
30
33
  class HTTPProjectConfigManager < ProjectConfigManager
31
34
  # Config manager that polls for the datafile and updated ProjectConfig based on an update interval.
32
35
 
33
- attr_reader :stopped
36
+ attr_reader :stopped, :optimizely_config
34
37
 
35
38
  # Initialize config manager. One of sdk_key or url has to be set to be able to use.
36
39
  #
@@ -48,6 +51,8 @@ module Optimizely
48
51
  # error_handler - Provides a handle_error method to handle exceptions.
49
52
  # skip_json_validation - Optional boolean param which allows skipping JSON schema
50
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.
51
56
  def initialize(
52
57
  sdk_key: nil,
53
58
  url: nil,
@@ -60,24 +65,31 @@ module Optimizely
60
65
  logger: nil,
61
66
  error_handler: nil,
62
67
  skip_json_validation: false,
63
- notification_center: nil
68
+ notification_center: nil,
69
+ datafile_access_token: nil,
70
+ proxy_config: nil
64
71
  )
65
72
  @logger = logger || NoOpLogger.new
66
73
  @error_handler = error_handler || NoOpErrorHandler.new
74
+ @access_token = datafile_access_token
67
75
  @datafile_url = get_datafile_url(sdk_key, url, url_template)
68
76
  @polling_interval = nil
69
77
  polling_interval(polling_interval)
70
78
  @blocking_timeout = nil
71
79
  blocking_timeout(blocking_timeout)
72
80
  @last_modified = nil
73
- @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
74
- @async_scheduler.start! if start_by_default == true
75
- @stopped = false
76
81
  @skip_json_validation = skip_json_validation
77
82
  @notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
78
83
  @config = datafile.nil? ? nil : DatafileProjectConfig.create(datafile, @logger, @error_handler, @skip_json_validation)
84
+ @optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
79
85
  @mutex = Mutex.new
80
86
  @resource = ConditionVariable.new
87
+ @async_scheduler = AsyncScheduler.new(method(:fetch_datafile_config), @polling_interval, auto_update, @logger)
88
+ # Start async scheduler in the end to avoid race condition where scheduler executes
89
+ # callback which makes use of variables not yet initialized by the main thread.
90
+ @async_scheduler.start! if start_by_default == true
91
+ @proxy_config = proxy_config
92
+ @stopped = false
81
93
  end
82
94
 
83
95
  def ready?
@@ -139,21 +151,20 @@ module Optimizely
139
151
  end
140
152
 
141
153
  def request_config
142
- @logger.log(
143
- Logger::DEBUG,
144
- "Fetching datafile from #{@datafile_url}"
145
- )
146
- begin
147
- headers = {
148
- 'Content-Type' => 'application/json'
149
- }
154
+ @logger.log(Logger::DEBUG, "Fetching datafile from #{@datafile_url}")
155
+ headers = {}
156
+ headers['Content-Type'] = 'application/json'
157
+ headers['If-Modified-Since'] = @last_modified if @last_modified
158
+ headers['Authorization'] = "Bearer #{@access_token}" unless @access_token.nil?
150
159
 
151
- headers[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']] = @last_modified if @last_modified
160
+ # Cleaning headers before logging to avoid exposing authorization token
161
+ cleansed_headers = {}
162
+ headers.each { |key, value| cleansed_headers[key] = key == 'Authorization' ? '********' : value }
163
+ @logger.log(Logger::DEBUG, "Datafile request headers: #{cleansed_headers}")
152
164
 
153
- response = HTTParty.get(
154
- @datafile_url,
155
- headers: headers,
156
- timeout: Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT']
165
+ begin
166
+ response = Helpers::HttpUtils.make_request(
167
+ @datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
157
168
  )
158
169
  rescue StandardError => e
159
170
  @logger.log(
@@ -163,6 +174,9 @@ module Optimizely
163
174
  return nil
164
175
  end
165
176
 
177
+ response_code = response.code.to_i
178
+ @logger.log(Logger::DEBUG, "Datafile response status code #{response_code}")
179
+
166
180
  # Leave datafile and config unchanged if it has not been modified.
167
181
  if response.code == '304'
168
182
  @logger.log(
@@ -172,9 +186,14 @@ module Optimizely
172
186
  return
173
187
  end
174
188
 
175
- @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
176
-
177
- config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation) if response.body
189
+ if response_code >= 200 && response_code < 400
190
+ @logger.log(Logger::DEBUG, 'Successfully fetched datafile, generating Project config')
191
+ config = DatafileProjectConfig.create(response.body, @logger, @error_handler, @skip_json_validation)
192
+ @last_modified = response[Helpers::Constants::HTTP_HEADERS['LAST_MODIFIED']]
193
+ @logger.log(Logger::DEBUG, "Saved last modified header value from response: #{@last_modified}.")
194
+ else
195
+ @logger.log(Logger::DEBUG, "Datafile fetch failed, status: #{response.code}, message: #{response.message}")
196
+ end
178
197
 
179
198
  config
180
199
  end
@@ -190,6 +209,7 @@ module Optimizely
190
209
  end
191
210
 
192
211
  @config = config
212
+ @optimizely_config = OptimizelyConfig.new(config).config
193
213
 
194
214
  @notification_center.send_notifications(NotificationCenter::NOTIFICATION_TYPES[:OPTIMIZELY_CONFIG_UPDATE])
195
215
 
@@ -279,17 +299,19 @@ module Optimizely
279
299
  # SDK key to determine URL from which to fetch the datafile.
280
300
  # Returns String representing URL to fetch datafile from.
281
301
  if sdk_key.nil? && url.nil?
282
- @logger.log(Logger::ERROR, 'Must provide at least one of sdk_key or url.')
283
- @error_handler.handle_error(InvalidInputsError)
302
+ error_msg = 'Must provide at least one of sdk_key or url.'
303
+ @logger.log(Logger::ERROR, error_msg)
304
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
284
305
  end
285
306
 
286
307
  unless url
287
- url_template ||= Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE']
308
+ url_template ||= @access_token.nil? ? Helpers::Constants::CONFIG_MANAGER['DATAFILE_URL_TEMPLATE'] : Helpers::Constants::CONFIG_MANAGER['AUTHENTICATED_DATAFILE_URL_TEMPLATE']
288
309
  begin
289
310
  return (url_template % sdk_key)
290
311
  rescue
291
- @logger.log(Logger::ERROR, "Invalid url_template #{url_template} provided.")
292
- @error_handler.handle_error(InvalidInputsError)
312
+ error_msg = "Invalid url_template #{url_template} provided."
313
+ @logger.log(Logger::ERROR, error_msg)
314
+ @error_handler.handle_error(InvalidInputsError.new(error_msg))
293
315
  end
294
316
  end
295
317
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2019, Optimizely and contributors
4
+ # Copyright 2019-2020, Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -17,11 +17,13 @@
17
17
  #
18
18
 
19
19
  require_relative '../config/datafile_project_config'
20
+ require_relative '../optimizely_config'
20
21
  require_relative 'project_config_manager'
22
+
21
23
  module Optimizely
22
24
  class StaticProjectConfigManager < ProjectConfigManager
23
25
  # Implementation of ProjectConfigManager interface.
24
- attr_reader :config
26
+ attr_reader :config, :optimizely_config
25
27
 
26
28
  def initialize(datafile, logger, error_handler, skip_json_validation)
27
29
  # Looks up and sets datafile and config based on response body.
@@ -38,6 +40,8 @@ module Optimizely
38
40
  error_handler,
39
41
  skip_json_validation
40
42
  )
43
+
44
+ @optimizely_config = @config.nil? ? nil : OptimizelyConfig.new(@config).config
41
45
  end
42
46
  end
43
47
  end
@@ -20,7 +20,7 @@ require_relative '../helpers/validator'
20
20
  module Optimizely
21
21
  class BatchEventProcessor < EventProcessor
22
22
  # BatchEventProcessor is a batched implementation of the Interface EventProcessor.
23
- # Events passed to the BatchEventProcessor are immediately added to a EventQueue.
23
+ # Events passed to the BatchEventProcessor are immediately added to an EventQueue.
24
24
  # The BatchEventProcessor maintains a single consumer thread that pulls events off of
25
25
  # the BlockingQueue and buffers them for either a configured batch size or for a
26
26
  # maximum duration before the resulting LogEvent is sent to the NotificationCenter.
@@ -30,13 +30,14 @@ module Optimizely
30
30
  DEFAULT_BATCH_SIZE = 10
31
31
  DEFAULT_BATCH_INTERVAL = 30_000 # interval in milliseconds
32
32
  DEFAULT_QUEUE_CAPACITY = 1000
33
+ DEFAULT_TIMEOUT_INTERVAL = 5 # interval in seconds
33
34
 
34
35
  FLUSH_SIGNAL = 'FLUSH_SIGNAL'
35
36
  SHUTDOWN_SIGNAL = 'SHUTDOWN_SIGNAL'
36
37
 
37
38
  def initialize(
38
39
  event_queue: SizedQueue.new(DEFAULT_QUEUE_CAPACITY),
39
- event_dispatcher: Optimizely::EventDispatcher.new,
40
+ event_dispatcher: nil,
40
41
  batch_size: DEFAULT_BATCH_SIZE,
41
42
  flush_interval: DEFAULT_BATCH_INTERVAL,
42
43
  logger: NoOpLogger.new,
@@ -44,7 +45,7 @@ module Optimizely
44
45
  )
45
46
  @event_queue = event_queue
46
47
  @logger = logger
47
- @event_dispatcher = event_dispatcher
48
+ @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger)
48
49
  @batch_size = if (batch_size.is_a? Integer) && positive_number?(batch_size)
49
50
  batch_size
50
51
  else
@@ -58,11 +59,9 @@ module Optimizely
58
59
  DEFAULT_BATCH_INTERVAL
59
60
  end
60
61
  @notification_center = notification_center
61
- @mutex = Mutex.new
62
- @received = ConditionVariable.new
63
62
  @current_batch = []
64
63
  @started = false
65
- start!
64
+ @stopped = false
66
65
  end
67
66
 
68
67
  def start!
@@ -71,76 +70,61 @@ module Optimizely
71
70
  return
72
71
  end
73
72
  @flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
74
- @thread = Thread.new { run }
73
+ @logger.log(Logger::INFO, 'Starting scheduler.')
74
+ if @wait_mutex.nil?
75
+ @wait_mutex = Mutex.new
76
+ @resource = ConditionVariable.new
77
+ end
78
+ @thread = Thread.new { run_queue }
75
79
  @started = true
80
+ @stopped = false
76
81
  end
77
82
 
78
83
  def flush
79
- @mutex.synchronize do
80
- @event_queue << FLUSH_SIGNAL
81
- @received.signal
82
- end
84
+ @event_queue << FLUSH_SIGNAL
85
+ @wait_mutex.synchronize { @resource.signal }
83
86
  end
84
87
 
85
88
  def process(user_event)
86
89
  @logger.log(Logger::DEBUG, "Received userEvent: #{user_event}")
87
90
 
88
- if !@started || !@thread.alive?
91
+ # if the processor has been explicitly stopped. Don't accept tasks
92
+ if @stopped
89
93
  @logger.log(Logger::WARN, 'Executor shutdown, not accepting tasks.')
90
94
  return
91
95
  end
92
96
 
93
- @mutex.synchronize do
94
- begin
95
- @event_queue << user_event
96
- @received.signal
97
- rescue Exception
98
- @logger.log(Logger::WARN, 'Payload not accepted by the queue.')
99
- return
100
- end
97
+ # start if the processor hasn't been started
98
+ start! unless @started
99
+
100
+ begin
101
+ @event_queue.push(user_event, true)
102
+ @wait_mutex.synchronize { @resource.signal }
103
+ rescue => e
104
+ @logger.log(Logger::WARN, 'Payload not accepted by the queue: ' + e.message)
105
+ return
101
106
  end
102
107
  end
103
108
 
104
109
  def stop!
105
110
  return unless @started
106
111
 
107
- @mutex.synchronize do
108
- @event_queue << SHUTDOWN_SIGNAL
109
- @received.signal
110
- end
111
-
112
+ @logger.log(Logger::INFO, 'Stopping scheduler.')
113
+ @event_queue << SHUTDOWN_SIGNAL
114
+ @wait_mutex.synchronize { @resource.signal }
115
+ @thread.join(DEFAULT_TIMEOUT_INTERVAL)
112
116
  @started = false
113
- @logger.log(Logger::WARN, 'Stopping scheduler.')
114
- @thread.exit
117
+ @stopped = true
115
118
  end
116
119
 
117
120
  private
118
121
 
119
- def run
120
- loop do
121
- if Helpers::DateTimeUtils.create_timestamp > @flushing_interval_deadline
122
- @logger.log(
123
- Logger::DEBUG,
124
- 'Deadline exceeded flushing current batch.'
125
- )
126
- flush_queue!
127
- end
128
-
129
- item = nil
130
-
131
- @mutex.synchronize do
132
- @received.wait(@mutex, 0.05)
133
- item = @event_queue.pop if @event_queue.length.positive?
134
- end
135
-
136
- if item.nil?
137
- sleep(0.05)
138
- next
139
- end
140
-
122
+ def process_queue
123
+ while @event_queue.length.positive?
124
+ item = @event_queue.pop
141
125
  if item == SHUTDOWN_SIGNAL
142
- @logger.log(Logger::INFO, 'Received shutdown signal.')
143
- break
126
+ @logger.log(Logger::DEBUG, 'Received shutdown signal.')
127
+ return false
144
128
  end
145
129
 
146
130
  if item == FLUSH_SIGNAL
@@ -151,15 +135,35 @@ module Optimizely
151
135
 
152
136
  add_to_batch(item) if item.is_a? Optimizely::UserEvent
153
137
  end
138
+ true
139
+ end
140
+
141
+ def run_queue
142
+ loop do
143
+ if Helpers::DateTimeUtils.create_timestamp >= @flushing_interval_deadline
144
+ @logger.log(Logger::DEBUG, 'Deadline exceeded flushing current batch.')
145
+
146
+ break unless process_queue
147
+
148
+ flush_queue!
149
+ @flushing_interval_deadline = Helpers::DateTimeUtils.create_timestamp + @flush_interval
150
+ end
151
+
152
+ break unless process_queue
153
+
154
+ # what is the current interval to flush in seconds
155
+ interval = (@flushing_interval_deadline - Helpers::DateTimeUtils.create_timestamp) * 0.001
156
+
157
+ next unless interval.positive?
158
+
159
+ @wait_mutex.synchronize { @resource.wait(@wait_mutex, interval) }
160
+ end
154
161
  rescue SignalException
155
- @logger.log(Logger::INFO, 'Interrupted while processing buffer.')
156
- rescue Exception => e
162
+ @logger.log(Logger::ERROR, 'Interrupted while processing buffer.')
163
+ rescue => e
157
164
  @logger.log(Logger::ERROR, "Uncaught exception processing buffer. #{e.message}")
158
165
  ensure
159
- @logger.log(
160
- Logger::INFO,
161
- 'Exiting processing loop. Attempting to flush pending events.'
162
- )
166
+ @logger.log(Logger::INFO, 'Exiting processing loop. Attempting to flush pending events.')
163
167
  flush_queue!
164
168
  end
165
169
 
@@ -168,6 +172,11 @@ module Optimizely
168
172
 
169
173
  log_event = Optimizely::EventFactory.create_log_event(@current_batch, @logger)
170
174
  begin
175
+ @logger.log(
176
+ Logger::INFO,
177
+ 'Flushing Queue.'
178
+ )
179
+
171
180
  @event_dispatcher.dispatch_event(log_event)
172
181
  @notification_center&.send_notifications(
173
182
  NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT],
@@ -192,7 +201,7 @@ module Optimizely
192
201
  @current_batch << user_event
193
202
  return unless @current_batch.length >= @batch_size
194
203
 
195
- @logger.log(Logger::DEBUG, 'Flushing on max batch size!')
204
+ @logger.log(Logger::DEBUG, 'Flushing on max batch size.')
196
205
  flush_queue!
197
206
  end
198
207
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Copyright 2016-2017, Optimizely and contributors
4
+ # Copyright 2016-2017, 2019-2020 Optimizely and contributors
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
@@ -15,7 +15,8 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
- require 'httparty'
18
+ require_relative 'exceptions'
19
+ require_relative 'helpers/http_utils'
19
20
 
20
21
  module Optimizely
21
22
  class NoOpEventDispatcher
@@ -28,26 +29,43 @@ module Optimizely
28
29
  # @api constants
29
30
  REQUEST_TIMEOUT = 10
30
31
 
32
+ def initialize(logger: nil, error_handler: nil, proxy_config: nil)
33
+ @logger = logger || NoOpLogger.new
34
+ @error_handler = error_handler || NoOpErrorHandler.new
35
+ @proxy_config = proxy_config
36
+ end
37
+
31
38
  # Dispatch the event being represented by the Event object.
32
39
  #
33
40
  # @param event - Event object
34
41
  def dispatch_event(event)
35
- if event.http_verb == :get
36
- begin
37
- HTTParty.get(event.url, headers: event.headers, query: event.params, timeout: REQUEST_TIMEOUT)
38
- rescue Timeout::Error => e
39
- return e
40
- end
41
- elsif event.http_verb == :post
42
- begin
43
- HTTParty.post(event.url,
44
- body: event.params.to_json,
45
- headers: event.headers,
46
- timeout: REQUEST_TIMEOUT)
47
- rescue Timeout::Error => e
48
- return e
49
- end
42
+ response = Helpers::HttpUtils.make_request(
43
+ event.url, event.http_verb, event.params.to_json, event.headers, REQUEST_TIMEOUT, @proxy_config
44
+ )
45
+
46
+ error_msg = "Event failed to dispatch with response code: #{response.code}"
47
+
48
+ case response.code.to_i
49
+ when 400...500
50
+ @logger.log(Logger::ERROR, error_msg)
51
+ @error_handler.handle_error(HTTPCallError.new("HTTP Client Error: #{response.code}"))
52
+
53
+ when 500...600
54
+ @logger.log(Logger::ERROR, error_msg)
55
+ @error_handler.handle_error(HTTPCallError.new("HTTP Server Error: #{response.code}"))
56
+ else
57
+ @logger.log(Logger::DEBUG, 'event successfully sent with response code ' + response.code.to_s)
50
58
  end
59
+ rescue Timeout::Error => e
60
+ @logger.log(Logger::ERROR, "Request Timed out. Error: #{e}")
61
+ @error_handler.handle_error(e)
62
+
63
+ # Returning Timeout error to retain existing behavior.
64
+ e
65
+ rescue StandardError => e
66
+ @logger.log(Logger::ERROR, "Event failed to dispatch. Error: #{e}")
67
+ @error_handler.handle_error(e)
68
+ nil
51
69
  end
52
70
  end
53
71
  end