optimizely-sdk 3.3.1 → 3.5.0

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