Skip to content

Commit

Permalink
Allow ErrorReporter to handle several error classes
Browse files Browse the repository at this point in the history
`Rails.error.handle` and `Rails.error.record` are able to filter by list of serveral error classes now, for example like this:

```ruby
Rails.error.handle(ArgumentError, TypeError) do
  [1, 2, 3].first(x) # where `x` might be `-4` or `'4'`
end
```
  • Loading branch information
spickermann committed Oct 21, 2022
1 parent b96ddea commit 1a9b887
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 9 deletions.
11 changes: 11 additions & 0 deletions activesupport/CHANGELOG.md
@@ -1,3 +1,14 @@
* `Rails.error.handle` and `Rails.error.record` filter now by multiple error classes.

```ruby
Rails.error.handle(IOError, ArgumentError) do
1 + '1' # raises TypeError
end
1 + 1 # TypeErrors are not IOErrors or ArgumentError, so this will *not* be handled
```

*Martin Spickermann*

* `Class#subclasses` and `Class#descendants` now automatically filter reloaded classes.

Previously they could return old implementations of reloadable classes that have been
Expand Down
14 changes: 8 additions & 6 deletions activesupport/lib/active_support/error_reporter.rb
Expand Up @@ -42,7 +42,7 @@ def initialize(*subscribers, logger: nil)
# 1 + '1'
# end
#
# Can be restricted to handle only a specific error class:
# Can be restricted to handle only specific error classes:
#
# maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
#
Expand All @@ -69,9 +69,10 @@ def initialize(*subscribers, logger: nil)
# * +:source+ - This value is passed along to subscribers to indicate the
# source of the error. Subscribers can use this value to ignore certain
# errors. Defaults to <tt>"application"</tt>.
def handle(error_class = StandardError, severity: :warning, context: {}, fallback: nil, source: DEFAULT_SOURCE)
def handle(*error_classes, severity: :warning, context: {}, fallback: nil, source: DEFAULT_SOURCE)
error_classes = [StandardError] if error_classes.blank?
yield
rescue error_class => error
rescue *error_classes => error
report(error, handled: true, severity: severity, context: context, source: source)
fallback.call if fallback
end
Expand All @@ -84,7 +85,7 @@ def handle(error_class = StandardError, severity: :warning, context: {}, fallbac
# 1 + '1'
# end
#
# Can be restricted to handle only a specific error class:
# Can be restricted to handle only specific error classes:
#
# tags = Rails.error.record(Redis::BaseError) { redis.get("tags") }
#
Expand All @@ -104,9 +105,10 @@ def handle(error_class = StandardError, severity: :warning, context: {}, fallbac
# * +:source+ - This value is passed along to subscribers to indicate the
# source of the error. Subscribers can use this value to ignore certain
# errors. Defaults to <tt>"application"</tt>.
def record(error_class = StandardError, severity: :error, context: {}, source: DEFAULT_SOURCE)
def record(*error_classes, severity: :error, context: {}, source: DEFAULT_SOURCE)
error_classes = [StandardError] if error_classes.blank?
yield
rescue error_class => error
rescue *error_classes => error
report(error, handled: false, severity: severity, context: context, source: source)
raise
end
Expand Down
36 changes: 36 additions & 0 deletions activesupport/test/error_reporter_test.rb
Expand Up @@ -68,6 +68,23 @@ class ErrorReporterTest < ActiveSupport::TestCase
assert_equal [], @subscriber.events
end

test "#handle can be scoped to several exception classes" do
assert_raises ArgumentError do
@reporter.handle(NameError, NoMethodError) do
raise ArgumentError
end
end
assert_equal [], @subscriber.events
end

test "#handle swallows and reports matching errors" do
error = ArgumentError.new("Oops")
@reporter.handle(NameError, ArgumentError) do
raise error
end
assert_equal [[error, true, :warning, "application", {}]], @subscriber.events
end

test "#handle passes through the return value" do
result = @reporter.handle do
2 + 2
Expand Down Expand Up @@ -125,6 +142,25 @@ class ErrorReporterTest < ActiveSupport::TestCase
assert_equal [], @subscriber.events
end

test "#record can be scoped to several exception classes" do
assert_raises ArgumentError do
@reporter.record(NameError, NoMethodError) do
raise ArgumentError
end
end
assert_equal [], @subscriber.events
end

test "#record report any matching, unhandled error and re-raise them" do
error = ArgumentError.new("Oops")
assert_raises ArgumentError do
@reporter.record(NameError, ArgumentError) do
raise error
end
end
assert_equal [[error, false, :error, "application", {}]], @subscriber.events
end

test "#record passes through the return value" do
result = @reporter.record do
2 + 2
Expand Down
6 changes: 3 additions & 3 deletions guides/source/error_reporting.md
Expand Up @@ -131,9 +131,9 @@ Rails.error.handle(context: {user_id: user.id}, severity: :info) do
end
```

### Filtering by Error Class
### Filtering by Error Classes

With `Rails.error.handle` and `Rails.error.record`, you can also choose to only report errors of a certain class. For example:
With `Rails.error.handle` and `Rails.error.record`, you can also choose to only report errors of certain classes. For example:

```ruby
Rails.error.handle(IOError) do
Expand Down Expand Up @@ -176,4 +176,4 @@ module MySdk
end
```

If you register an error subscriber, but still have other error mechanisms like a Rack middleware, you may end up with errors reported multiple times. You should either remove your other mechanisms or adjust your report functionality so it skips reporting an exception it has seen before.
If you register an error subscriber, but still have other error mechanisms like a Rack middleware, you may end up with errors reported multiple times. You should either remove your other mechanisms or adjust your report functionality so it skips reporting an exception it has seen before.

0 comments on commit 1a9b887

Please sign in to comment.