-
Notifications
You must be signed in to change notification settings - Fork 21.4k
/
error_reporter.rb
117 lines (107 loc) · 3.83 KB
/
error_reporter.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# frozen_string_literal: true
module ActiveSupport
# +ActiveSupport::ErrorReporter+ is a common interface for error reporting services.
#
# To rescue and report any unhandled error, you can use the +handle+ method:
#
# Rails.error.handle do
# do_something!
# end
#
# If an error is raised, it will be reported and swallowed.
#
# Alternatively if you want to report the error but not swallow it, you can use +record+
#
# Rails.error.record do
# do_something!
# end
#
# Both methods can be restricted to only handle a specific exception class
#
# maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
#
# You can also pass some extra context information that may be used by the error subscribers:
#
# Rails.error.handle(context: { section: "admin" }) do
# # ...
# end
#
# Additionally a +severity+ can be passed along to communicate how important the error report is.
# +severity+ can be one of +:error+, +:warning+ or +:info+. Handled errors default to the +:warning+
# severity, and unhandled ones to +error+.
#
# Both +handle+ and +record+ pass through the return value from the block. In the case of +handle+
# rescuing an error, a fallback can be provided. The fallback must be a callable whose result will
# be returned when the block raises and is handled:
#
# user = Rails.error.handle(fallback: -> { User.anonymous }) do
# User.find_by(params)
# end
class ErrorReporter
SEVERITIES = %i(error warning info)
attr_accessor :logger
def initialize(*subscribers, logger: nil)
@subscribers = subscribers.flatten
@logger = logger
end
# Report any unhandled exception, and swallow it.
#
# Rails.error.handle do
# 1 + '1'
# end
#
def handle(error_class = StandardError, severity: :warning, context: {}, fallback: nil)
yield
rescue error_class => error
report(error, handled: true, severity: severity, context: context)
fallback.call if fallback
end
def record(error_class = StandardError, severity: :error, context: {})
yield
rescue error_class => error
report(error, handled: false, severity: severity, context: context)
raise
end
# Register a new error subscriber. The subscriber must respond to
#
# report(Exception, handled: Boolean, context: Hash)
#
# The +report+ method +should+ never raise an error.
def subscribe(subscriber)
unless subscriber.respond_to?(:report)
raise ArgumentError, "Error subscribers must respond to #report"
end
@subscribers << subscriber
end
# Update the execution context that is accessible to error subscribers
#
# Rails.error.set_context(section: "checkout", user_id: @user.id)
#
# See +ActiveSupport::ExecutionContext.set+
def set_context(...)
ActiveSupport::ExecutionContext.set(...)
end
# When the block based +handle+ and +record+ methods are not suitable, you can directly use +report+
#
# Rails.error.report(error, handled: true)
def report(error, handled:, severity: handled ? :warning : :error, context: {})
unless SEVERITIES.include?(severity)
raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
end
full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
@subscribers.each do |subscriber|
subscriber.report(error, handled: handled, severity: severity, context: full_context)
rescue => subscriber_error
if logger
logger.fatal(
"Error subscriber raised an error: #{subscriber_error.message} (#{subscriber_error.class})\n" +
subscriber_error.backtrace.join("\n")
)
else
raise
end
end
nil
end
end
end