diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 9b726582..6863256f 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -70,7 +70,7 @@ def initialize( ) @logger = logger || NoOpLogger.new @error_handler = error_handler || NoOpErrorHandler.new - @event_dispatcher = event_dispatcher || EventDispatcher.new + @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler) @user_profile_service = user_profile_service begin @@ -701,7 +701,7 @@ def validate_instantiation_options return if Helpers::Validator.event_dispatcher_valid?(@event_dispatcher) - @event_dispatcher = EventDispatcher.new + @event_dispatcher = EventDispatcher.new(logger: @logger, error_handler: @error_handler) raise InvalidInputError, 'event_dispatcher' end diff --git a/lib/optimizely/event/batch_event_processor.rb b/lib/optimizely/event/batch_event_processor.rb index 975531b8..f82f6af1 100644 --- a/lib/optimizely/event/batch_event_processor.rb +++ b/lib/optimizely/event/batch_event_processor.rb @@ -37,7 +37,7 @@ class BatchEventProcessor < EventProcessor def initialize( event_queue: SizedQueue.new(DEFAULT_QUEUE_CAPACITY), - event_dispatcher: Optimizely::EventDispatcher.new, + event_dispatcher: nil, batch_size: DEFAULT_BATCH_SIZE, flush_interval: DEFAULT_BATCH_INTERVAL, logger: NoOpLogger.new, @@ -45,7 +45,7 @@ def initialize( ) @event_queue = event_queue @logger = logger - @event_dispatcher = event_dispatcher + @event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger) @batch_size = if (batch_size.is_a? Integer) && positive_number?(batch_size) batch_size else diff --git a/lib/optimizely/event_dispatcher.rb b/lib/optimizely/event_dispatcher.rb index 4fed08d8..3178185d 100644 --- a/lib/optimizely/event_dispatcher.rb +++ b/lib/optimizely/event_dispatcher.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2016-2017, Optimizely and contributors +# Copyright 2016-2017, 2019, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +require_relative 'exceptions' + require 'httparty' module Optimizely @@ -28,26 +30,46 @@ class EventDispatcher # @api constants REQUEST_TIMEOUT = 10 + def initialize(logger: nil, error_handler: nil) + @logger = logger || NoOpLogger.new + @error_handler = error_handler || NoOpErrorHandler.new + end + # Dispatch the event being represented by the Event object. # # @param event - Event object def dispatch_event(event) if event.http_verb == :get - begin - HTTParty.get(event.url, headers: event.headers, query: event.params, timeout: REQUEST_TIMEOUT) - rescue Timeout::Error => e - return e - end + response = HTTParty.get(event.url, headers: event.headers, query: event.params, timeout: REQUEST_TIMEOUT) + elsif event.http_verb == :post - begin - HTTParty.post(event.url, - body: event.params.to_json, - headers: event.headers, - timeout: REQUEST_TIMEOUT) - rescue Timeout::Error => e - return e - end + response = HTTParty.post(event.url, + body: event.params.to_json, + headers: event.headers, + timeout: REQUEST_TIMEOUT) end + + error_msg = "Event failed to dispatch with response code: #{response.code}" + + case response.code + when 400...500 + @logger.log(Logger::ERROR, error_msg) + @error_handler.handle_error(HTTPCallError.new("HTTP Client Error: #{response.code}")) + + when 500...600 + @logger.log(Logger::ERROR, error_msg) + @error_handler.handle_error(HTTPCallError.new("HTTP Server Error: #{response.code}")) + end + rescue Timeout::Error => e + @logger.log(Logger::ERROR, "Request Timed out. Error: #{e}") + @error_handler.handle_error(e) + + # Returning Timeout error to retain existing behavior. + e + rescue StandardError => e + @logger.log(Logger::ERROR, "Event failed to dispatch. Error: #{e}") + @error_handler.handle_error(e) + nil end end end diff --git a/lib/optimizely/exceptions.rb b/lib/optimizely/exceptions.rb index 5026844a..0b793901 100644 --- a/lib/optimizely/exceptions.rb +++ b/lib/optimizely/exceptions.rb @@ -18,6 +18,13 @@ module Optimizely class Error < StandardError; end + class HTTPCallError < Error + # Raised when a 4xx or 5xx response code is recieved. + def initialize(msg = 'HTTP call resulted in a response with an error code.') + super + end + end + class InvalidAudienceError < Error # Raised when an invalid audience is provided diff --git a/spec/event_dispatcher_spec.rb b/spec/event_dispatcher_spec.rb index 53d14193..4008516c 100644 --- a/spec/event_dispatcher_spec.rb +++ b/spec/event_dispatcher_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # -# Copyright 2016-2017, Optimizely and contributors +# Copyright 2016-2017, 2019, Optimizely and contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,11 +16,13 @@ # limitations under the License. # require 'spec_helper' -require 'webmock' -require 'optimizely/event_builder' require 'optimizely/event_dispatcher' +require 'optimizely/exceptions' describe Optimizely::EventDispatcher do + let(:error_handler) { spy(Optimizely::NoOpErrorHandler.new) } + let(:spy_logger) { spy('logger') } + before(:context) do @url = 'https://www.optimizely.com' @params = { @@ -34,6 +36,9 @@ before(:example) do @event_dispatcher = Optimizely::EventDispatcher.new + @customized_event_dispatcher = Optimizely::EventDispatcher.new( + logger: spy_logger, error_handler: error_handler + ) end it 'should properly dispatch V2 (POST) events' do @@ -64,7 +69,7 @@ expect(a_request(:get, get_url)).to have_been_made.once end - it 'should properly dispatch V2 (GET) events' do + it 'should properly dispatch V2 (GET) events with timeout exception' do get_url = @url + '?a=111001&g=111028&n=test_event&u=test_user' stub_request(:get, get_url) event = Optimizely::Event.new(:get, get_url, @params, @post_headers) @@ -74,4 +79,86 @@ expect(result).to eq(timeout_error) end + + it 'should log and handle Timeout error' do + get_url = @url + '?a=111001&g=111028&n=test_event&u=test_user' + stub_request(:post, get_url) + event = Optimizely::Event.new(:post, get_url, @params, @post_headers) + timeout_error = Timeout::Error.new + allow(HTTParty).to receive(:post).with(any_args).and_raise(timeout_error) + result = @customized_event_dispatcher.dispatch_event(event) + + expect(result).to eq(timeout_error) + expect(spy_logger).to have_received(:log).with( + Logger::ERROR, 'Request Timed out. Error: Timeout::Error' + ).once + + expect(error_handler).to have_received(:handle_error).once.with(Timeout::Error) + end + + it 'should log and handle any standard error' do + get_url = @url + '?a=111001&g=111028&n=test_event&u=test_user' + stub_request(:post, get_url) + event = Optimizely::Event.new(:post, get_url, @params, @post_headers) + error = ArgumentError + allow(HTTParty).to receive(:post).with(any_args).and_raise(error) + result = @customized_event_dispatcher.dispatch_event(event) + + expect(result).to eq(nil) + expect(spy_logger).to have_received(:log).with( + Logger::ERROR, 'Event failed to dispatch. Error: ArgumentError' + ).once + + expect(error_handler).to have_received(:handle_error).once.with(ArgumentError) + end + + it 'should log and handle any response with status code 4xx' do + stub_request(:post, @url).to_return(status: 499) + event = Optimizely::Event.new(:post, @url, @params, @post_headers) + + @customized_event_dispatcher.dispatch_event(event) + + expect(spy_logger).to have_received(:log).with( + Logger::ERROR, 'Event failed to dispatch with response code: 499' + ).once + + error = Optimizely::HTTPCallError.new('HTTP Client Error: 499') + expect(error_handler).to have_received(:handle_error).once.with(error) + end + + it 'should log and handle any response with status code 5xx' do + stub_request(:post, @url).to_return(status: 500) + event = Optimizely::Event.new(:post, @url, @params, @post_headers) + + @customized_event_dispatcher.dispatch_event(event) + + expect(spy_logger).to have_received(:log).with( + Logger::ERROR, 'Event failed to dispatch with response code: 500' + ).once + + error = Optimizely::HTTPCallError.new('HTTP Server Error: 500') + expect(error_handler).to have_received(:handle_error).once.with(error) + end + + it 'should do nothing on response with status code 3xx' do + stub_request(:post, @url).to_return(status: 399) + event = Optimizely::Event.new(:post, @url, @params, @post_headers) + + response = @customized_event_dispatcher.dispatch_event(event) + + expect(response).to be_nil + expect(spy_logger).not_to have_received(:log) + expect(error_handler).not_to have_received(:handle_error) + end + + it 'should do nothing on response with status code 600' do + stub_request(:post, @url).to_return(status: 600) + event = Optimizely::Event.new(:post, @url, @params, @post_headers) + + response = @customized_event_dispatcher.dispatch_event(event) + + expect(response).to be_nil + expect(spy_logger).not_to have_received(:log) + expect(error_handler).not_to have_received(:handle_error) + end end