-
Notifications
You must be signed in to change notification settings - Fork 21.6k
/
error_reporter.rb
201 lines (187 loc) · 7 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# 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 handle only a specific error class:
#
# maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
#
class ErrorReporter
SEVERITIES = %i(error warning info)
DEFAULT_SOURCE = "application"
attr_accessor :logger
def initialize(*subscribers, logger: nil)
@subscribers = subscribers.flatten
@logger = logger
end
# Evaluates the given block, reporting and swallowing any unhandled error.
# If no error is raised, returns the return value of the block. Otherwise,
# returns the result of +fallback.call+, or +nil+ if +fallback+ is not
# specified.
#
# # Will report a TypeError to all subscribers and return nil.
# Rails.error.handle do
# 1 + '1'
# end
#
# Can be restricted to handle only specific error classes:
#
# maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
#
# ==== Options
#
# * +:severity+ - This value is passed along to subscribers to indicate how
# important the error report is. Can be +:error+, +:warning+, or +:info+.
# Defaults to +:warning+.
#
# * +:context+ - Extra information that is passed along to subscribers. For
# example:
#
# Rails.error.handle(context: { section: "admin" }) do
# # ...
# end
#
# * +:fallback+ - A callable that provides +handle+'s return value when an
# unhandled error is raised. For example:
#
# user = Rails.error.handle(fallback: -> { User.anonymous }) do
# User.find_by(params)
# end
#
# * +: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_classes, severity: :warning, context: {}, fallback: nil, source: DEFAULT_SOURCE)
error_classes = [StandardError] if error_classes.blank?
yield
rescue *error_classes => error
report(error, handled: true, severity: severity, context: context, source: source)
fallback.call if fallback
end
# Evaluates the given block, reporting and re-raising any unhandled error.
# If no error is raised, returns the return value of the block.
#
# # Will report a TypeError to all subscribers and re-raise it.
# Rails.error.record do
# 1 + '1'
# end
#
# Can be restricted to handle only specific error classes:
#
# tags = Rails.error.record(Redis::BaseError) { redis.get("tags") }
#
# ==== Options
#
# * +:severity+ - This value is passed along to subscribers to indicate how
# important the error report is. Can be +:error+, +:warning+, or +:info+.
# Defaults to +:error+.
#
# * +:context+ - Extra information that is passed along to subscribers. For
# example:
#
# Rails.error.record(context: { section: "admin" }) do
# # ...
# end
#
# * +: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_classes, severity: :error, context: {}, source: DEFAULT_SOURCE)
error_classes = [StandardError] if error_classes.blank?
yield
rescue *error_classes => error
report(error, handled: false, severity: severity, context: context, source: source)
raise
end
# Register a new error subscriber. The subscriber must respond to
#
# report(Exception, handled: Boolean, severity: (:error OR :warning OR :info), context: Hash, source: String)
#
# The +report+ method <b>should never</b> raise an error.
def subscribe(subscriber)
unless subscriber.respond_to?(:report)
raise ArgumentError, "Error subscribers must respond to #report"
end
@subscribers << subscriber
end
# Unregister an error subscriber. Accepts either a subscriber or a class.
#
# subscriber = MyErrorSubscriber.new
# Rails.error.subscribe(subscriber)
#
# Rails.error.unsubscribe(subscriber)
# # or
# Rails.error.unsubscribe(MyErrorSubscriber)
def unsubscribe(subscriber)
@subscribers.delete_if { |s| subscriber === s }
end
# Prevent a subscriber from being notified of errors for the
# duration of the block. You may pass in the subscriber itself, or its class.
#
# This can be helpful for error reporting service integrations, when they wish
# to handle any errors higher in the stack.
def disable(subscriber)
disabled_subscribers = (ActiveSupport::IsolatedExecutionState[self] ||= [])
disabled_subscribers << subscriber
begin
yield
ensure
disabled_subscribers.delete(subscriber)
end
end
# Update the execution context that is accessible to error subscribers. Any
# context passed to #handle, #record, or #report will be merged with the
# context set here.
#
# Rails.error.set_context(section: "checkout", user_id: @user.id)
#
def set_context(...)
ActiveSupport::ExecutionContext.set(...)
end
# Report an error directly to subscribers. You can use this method when the
# block-based #handle and #record methods are not suitable.
#
# Rails.error.report(error)
#
def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
return if error.instance_variable_defined?(:@__rails_error_reported)
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)
disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
@subscribers.each do |subscriber|
unless disabled_subscribers&.any? { |s| s === subscriber }
subscriber.report(error, handled: handled, severity: severity, context: full_context, source: source)
end
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
unless error.frozen?
error.instance_variable_set(:@__rails_error_reported, true)
end
nil
end
end
end