-
Notifications
You must be signed in to change notification settings - Fork 175
/
bugsnag.rb
332 lines (281 loc) · 10.9 KB
/
bugsnag.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
require "rubygems"
require "thread"
require "bugsnag/version"
require "bugsnag/configuration"
require "bugsnag/meta_data"
require "bugsnag/report"
require "bugsnag/cleaner"
require "bugsnag/helpers"
require "bugsnag/session_tracker"
require "bugsnag/delivery"
require "bugsnag/delivery/synchronous"
require "bugsnag/delivery/thread_queue"
# Rack is not bundled with the other integrations
# as it doesn't auto-configure when loaded
require "bugsnag/integrations/rack"
require "bugsnag/middleware/rack_request"
require "bugsnag/middleware/warden_user"
require "bugsnag/middleware/clearance_user"
require "bugsnag/middleware/callbacks"
require "bugsnag/middleware/rails3_request"
require "bugsnag/middleware/sidekiq"
require "bugsnag/middleware/mailman"
require "bugsnag/middleware/rake"
require "bugsnag/middleware/callbacks"
require "bugsnag/middleware/classify_error"
require "bugsnag/middleware/delayed_job"
require "bugsnag/breadcrumbs/validator"
require "bugsnag/breadcrumbs/breadcrumb"
require "bugsnag/breadcrumbs/breadcrumbs"
# rubocop:todo Metrics/ModuleLength
module Bugsnag
LOCK = Mutex.new
INTEGRATIONS = [:resque, :sidekiq, :mailman, :delayed_job, :shoryuken, :que, :mongo]
NIL_EXCEPTION_DESCRIPTION = "'nil' was notified as an exception"
class << self
##
# Configure the Bugsnag notifier application-wide settings.
#
# Yields a configuration object to use to set application settings.
def configure(validate_api_key=true)
yield(configuration) if block_given?
check_key_valid if validate_api_key
check_endpoint_setup
register_at_exit
end
##
# Explicitly notify of an exception.
#
# Optionally accepts a block to append metadata to the yielded report.
def notify(exception, auto_notify=false, &block)
unless auto_notify.is_a? TrueClass or auto_notify.is_a? FalseClass
configuration.warn("Adding metadata/severity using a hash is no longer supported, please use block syntax instead")
auto_notify = false
end
return unless should_deliver_notification?(exception, auto_notify)
exception = NIL_EXCEPTION_DESCRIPTION if exception.nil?
report = Report.new(exception, configuration, auto_notify)
# If this is an auto_notify we yield the block before the any middleware is run
yield(report) if block_given? && auto_notify
if report.ignore?
configuration.debug("Not notifying #{report.exceptions.last[:errorClass]} due to ignore being signified in auto_notify block")
return
end
# Run internal middleware
configuration.internal_middleware.run(report)
if report.ignore?
configuration.debug("Not notifying #{report.exceptions.last[:errorClass]} due to ignore being signified in internal middlewares")
return
end
# Store before_middleware severity reason for future reference
initial_severity = report.severity
initial_reason = report.severity_reason
# Run users middleware
configuration.middleware.run(report) do
if report.ignore?
configuration.debug("Not notifying #{report.exceptions.last[:errorClass]} due to ignore being signified in user provided middleware")
return
end
# If this is not an auto_notify then the block was provided by the user. This should be the last
# block that is run as it is the users "most specific" block.
yield(report) if block_given? && !auto_notify
if report.ignore?
configuration.debug("Not notifying #{report.exceptions.last[:errorClass]} due to ignore being signified in user provided block")
return
end
# Test whether severity has been changed and ensure severity_reason is consistant in auto_notify case
if report.severity != initial_severity
report.severity_reason = {
:type => Report::USER_CALLBACK_SET_SEVERITY
}
else
report.severity_reason = initial_reason
end
deliver_notification(report)
end
end
##
# Registers an at_exit function to automatically catch errors on exit
def register_at_exit
return if at_exit_handler_installed?
@exit_handler_added = true
at_exit do
if $!
Bugsnag.notify($!, true) do |report|
report.severity = 'error'
report.severity_reason = {
:type => Bugsnag::Report::UNHANDLED_EXCEPTION
}
end
end
end
end
##
# Checks if an at_exit handler has been added
def at_exit_handler_installed?
@exit_handler_added ||= false
end
# Configuration getters
##
# Returns the client's Configuration object, or creates one if not yet created.
# @return [Configuration]
def configuration
@configuration = nil unless defined?(@configuration)
@configuration || LOCK.synchronize { @configuration ||= Bugsnag::Configuration.new }
end
##
# Returns the client's SessionTracker object, or creates one if not yet created.
def session_tracker
@session_tracker = nil unless defined?(@session_tracker)
@session_tracker || LOCK.synchronize { @session_tracker ||= Bugsnag::SessionTracker.new}
end
##
# Starts a session.
#
# Allows Bugsnag to track error rates across releases.
def start_session
session_tracker.start_session
end
##
# Allow access to "before notify" callbacks as an array.
#
# These callbacks will be called whenever an error notification is being made.
def before_notify_callbacks
Bugsnag.configuration.request_data[:before_callbacks] ||= []
end
# Attempts to load all integrations through auto-discovery
def load_integrations
require "bugsnag/integrations/railtie" if defined?(Rails::Railtie)
INTEGRATIONS.each do |integration|
begin
require "bugsnag/integrations/#{integration}"
rescue LoadError
end
end
end
# Load a specific integration
def load_integration(integration)
integration = :railtie if integration == :rails
if INTEGRATIONS.include?(integration) || integration == :railtie
require "bugsnag/integrations/#{integration}"
else
configuration.debug("Integration #{integration} is not currently supported")
end
end
##
# Leave a breadcrumb to be attached to subsequent reports
#
# @param name [String] the main breadcrumb name/message
# @param meta_data [Hash] String, Numeric, or Boolean meta data to attach
# @param type [String] the breadcrumb type, from Bugsnag::Breadcrumbs::VALID_BREADCRUMB_TYPES
# @param auto [Symbol] set to :auto if the breadcrumb is automatically created
def leave_breadcrumb(name, meta_data={}, type=Bugsnag::Breadcrumbs::MANUAL_BREADCRUMB_TYPE, auto=:manual)
breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new(name, type, meta_data, auto)
validator = Bugsnag::Breadcrumbs::Validator.new(configuration)
# Initial validation
validator.validate(breadcrumb)
# Skip if it's already invalid
return if breadcrumb.ignore?
# Run callbacks
configuration.before_breadcrumb_callbacks.each do |c|
c.arity > 0 ? c.call(breadcrumb) : c.call
break if breadcrumb.ignore?
end
# Return early if ignored
return if breadcrumb.ignore?
# Validate again in case of callback alteration
validator.validate(breadcrumb)
# Add to breadcrumbs buffer if still valid
configuration.breadcrumbs << breadcrumb unless breadcrumb.ignore?
end
##
# Returns the client's Cleaner object, or creates one if not yet created.
#
# @api private
#
# @return [Cleaner]
def cleaner
@cleaner = nil unless defined?(@cleaner)
@cleaner || LOCK.synchronize do
@cleaner ||= Bugsnag::Cleaner.new(configuration)
end
end
private
def should_deliver_notification?(exception, auto_notify)
reason = abort_reason(exception, auto_notify)
configuration.debug(reason) unless reason.nil?
reason.nil?
end
def abort_reason(exception, auto_notify)
if !configuration.auto_notify && auto_notify
"Not notifying because auto_notify is disabled"
elsif !configuration.valid_api_key?
"Not notifying due to an invalid api_key"
elsif !configuration.should_notify_release_stage?
"Not notifying due to notify_release_stages :#{configuration.notify_release_stages.inspect}"
elsif exception.respond_to?(:skip_bugsnag) && exception.skip_bugsnag
"Not notifying due to skip_bugsnag flag"
end
end
##
# Deliver the notification to Bugsnag
#
# @param report [Report]
# @return void
def deliver_notification(report)
configuration.info("Notifying #{configuration.notify_endpoint} of #{report.exceptions.last[:errorClass]}")
payload = report_to_json(report)
options = {:headers => report.headers}
Bugsnag::Delivery[configuration.delivery_method].deliver(
configuration.notify_endpoint,
payload,
configuration,
options
)
leave_breadcrumb(
report.summary[:error_class],
report.summary,
Bugsnag::Breadcrumbs::ERROR_BREADCRUMB_TYPE,
:auto
)
end
# Check if the API key is valid and warn (once) if it is not
def check_key_valid
@key_warning = false unless defined?(@key_warning)
if !configuration.valid_api_key? && !@key_warning
configuration.warn("No valid API key has been set, notifications will not be sent")
@key_warning = true
end
end
##
# Verifies the current endpoint setup
#
# If only a notify_endpoint has been set, session tracking will be disabled
# If only a session_endpoint has been set, and ArgumentError will be raised
def check_endpoint_setup
notify_set = configuration.notify_endpoint && configuration.notify_endpoint != Bugsnag::Configuration::DEFAULT_NOTIFY_ENDPOINT
session_set = configuration.session_endpoint && configuration.session_endpoint != Bugsnag::Configuration::DEFAULT_SESSION_ENDPOINT
if notify_set && !session_set
configuration.warn("The session endpoint has not been set, all further session capturing will be disabled")
configuration.disable_sessions
elsif !notify_set && session_set
raise ArgumentError, "The session endpoint cannot be modified without the notify endpoint"
end
end
##
# Convert the Report object to JSON
#
# We ensure the report is safe to send by removing recursion, fixing
# encoding errors and redacting metadata according to "meta_data_filters"
#
# @param report [Report]
# @return string
def report_to_json(report)
cleaned = cleaner.clean_object(report.as_json)
trimmed = Bugsnag::Helpers.trim_if_needed(cleaned)
::JSON.dump(trimmed)
end
end
end
# rubocop:enable Metrics/ModuleLength
Bugsnag.load_integrations unless ENV["BUGSNAG_DISABLE_AUTOCONFIGURE"]