Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,22 +227,6 @@ config.action_dispatch.rescue_responses.merge!(
Now when an you raise an `AuthenticationError` in one of your actions, the
status code of the response will be 401.

### Error Reporting
If you use an external error tracking software like Sentry or Honeybadger you'll
want to report all errors to that service.

You can do so by passing in a `Proc` to the `error_reporter` option. The first
argument provided to the Proc will be the error. The second argument will be the
`error_id` if you have one. See the section below for more details on error IDs.

```ruby
handle_api_errors(
error_reporter: Proc.new do |error, error_id|
Raven.capture_exception(error, error_id: error_id)
end
)
```

### Error IDs
Sometimes it's helpful to include IDs with your error responses so that you can
correlate a specific error with a record in your logs or bug tracking software.
Expand All @@ -269,6 +253,40 @@ These will result in:
}
```

### Error Reporting
If you use an external error tracking software like Sentry or Honeybadger, you'll
want to report all errors to that service.

#### Out of the Box Error Reporting
There are a few supported error reporter options that you can select.

##### Raven/Sentry
```ruby
handle_api_errors(error_reporter: :raven)
# Or
handle_api_errors(error_reporter: :sentry)
```

##### Honeybadger
```ruby
handle_api_errors(error_reporter: :honeybadger)
```

__NOTE:__ If you use the `:error_id` option, the error error reporter will tag
the exception with the error ID when reporting the error.

#### Custom Reporting
If none of the out of the box options work for you, you can pass in a proc which
will receive the error and the error_id as arguments.

```ruby
handle_api_errors(
error_reporter: Proc.new do |error, error_id|
# Do something with the `error` here.
end
)
```

### Setting Content Type
The api_error_handler will set the content type of your error based on the
`format` option you pick. However, you can override this by setting the
Expand Down
5 changes: 3 additions & 2 deletions lib/api_error_handler.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require_relative "./api_error_handler/version"
require_relative "./api_error_handler/action_controller"
require_relative "./api_error_handler/error_id_generator"
require_relative "./api_error_handler/error_reporter"
Dir[File.join(__dir__, 'api_error_handler', 'serializers', "*.rb")].each { |file| require file }

module ApiErrorHandler
Expand All @@ -20,7 +21,7 @@ module ApiErrorHandler

def handle_api_errors(options = {})
format = options.fetch(:format, :json)
error_reporter = options[:error_reporter]
error_reporter = ErrorReporter.new(options[:error_reporter])
serializer_options = SERIALIZER_OPTIONS.merge(
options.slice(*SERIALIZER_OPTIONS.keys)
)
Expand All @@ -32,7 +33,7 @@ def handle_api_errors(options = {})
status = ActionDispatch::ExceptionWrapper.rescue_responses[error.class.to_s]

error_id = ErrorIdGenerator.run(options[:error_id])
error_reporter.call(error, error_id) if error_reporter
error_reporter.report(error, error_id: error_id)

serializer = serializer_class.new(error, status)
response_body = serializer.serialize(
Expand Down
36 changes: 36 additions & 0 deletions lib/api_error_handler/error_reporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require "logger"
require_relative "./errors"

module ApiErrorHandler
class ErrorReporter
def initialize(strategy)
@strategy = strategy
end

def report(error, error_id: nil)
if @strategy.nil?
true
elsif @strategy.instance_of?(Proc)
@strategy.call(error, error_id)
elsif @strategy == :honeybadger
raise_dependency_error(missing_constant: "Honeybadger") unless defined?(Honeybadger)

context = error_id ? { error_id: error_id } : {}
Honeybadger.notify(error, context: context)
elsif @strategy == :raven || @strategy == :sentry
raise_dependency_error(missing_constant: "Raven") unless defined?(Raven)

extra = error_id ? { error_id: error_id } : {}
Raven.capture_exception(error, extra: extra)
else
raise(InvalidOptionError, "`#{@strategy.inspect}` is an invalid argument for the `:error_id` option.")
end
end

private

def raise_dependency_error(missing_constant:)
raise MissingDependencyError, "You selected the #{@strategy.inspect} error reporter option but the #{missing_constant} constant is not defined. If you wish to use this error reporting option you must have the #{@strategy} client gem installed."
end
end
end
1 change: 1 addition & 0 deletions lib/api_error_handler/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ module ApiErrorHandler
class Error < StandardError; end

class InvalidOptionError < Error; end
class MissingDependencyError < Error; end
end
78 changes: 78 additions & 0 deletions spec/api_error_handler/error_reporter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
require_relative "../../lib/api_error_handler/error_reporter"

RSpec.describe ApiErrorHandler::ErrorReporter do
let(:error) { RuntimeError.new("Error message") }

it "Raises an InvalidOptionError if you provide an bad option" do
reporter = described_class.new(:asdf)

expect do
reporter.report(error)
end.to raise_error(ApiErrorHandler::InvalidOptionError)
end

context "using the `nil` strategy" do
let(:reporter) { described_class.new(nil) }

it "Does nothing if the strategy is `nil`" do
reporter.report(error)
end
end

context "using a Proc strategy" do
it "Calls the Proc with the error and error_id" do
strategy = proc do |e, error_id|
expect(e).to eq(error)
expect(error_id).to eq("123")
end

reporter = described_class.new(strategy)

reporter.report(error, error_id: "123")
end
end

context "using the :honeybadger strategy" do
let(:reporter) { described_class.new(:honeybadger) }

it "Raises an error if the Honeybadger constant is not defined" do
expect { reporter.report(error) }.to raise_error(ApiErrorHandler::MissingDependencyError)
end

it "Reports to Honeybadger with an error id" do
stub_const("Honeybadger", double)
expect(Honeybadger).to receive(:notify).with(error, context: { error_id: "456" })

reporter.report(error, error_id: "456")
end

it "Reports to Honeybadger without an error id" do
stub_const("Honeybadger", double)
expect(Honeybadger).to receive(:notify).with(error, context: {})

reporter.report(error)
end
end

context "using the :raven/:sentry strategy" do
let(:reporter) { described_class.new(:sentry) }

it "Raises an error if the Raven constant is not defined" do
expect { reporter.report(error) }.to raise_error(ApiErrorHandler::MissingDependencyError)
end

it "Reports to Honeybadger with an error id" do
stub_const("Raven", double)
expect(Raven).to receive(:capture_exception).with(error, extra: { error_id: "456" })

reporter.report(error, error_id: "456")
end

it "Reports to Honeybadger without an error id" do
stub_const("Raven", double)
expect(Raven).to receive(:capture_exception).with(error, extra: { })

reporter.report(error)
end
end
end