-
Notifications
You must be signed in to change notification settings - Fork 173
/
bugsnag.rb
563 lines (490 loc) · 18.2 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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
require "rubygems"
require "thread"
require "bugsnag/version"
require "bugsnag/configuration"
require "bugsnag/meta_data"
require "bugsnag/report"
require "bugsnag/event"
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/classify_error"
require "bugsnag/middleware/delayed_job"
require "bugsnag/breadcrumb_type"
require "bugsnag/breadcrumbs/validator"
require "bugsnag/breadcrumbs/breadcrumb"
require "bugsnag/breadcrumbs/breadcrumbs"
require "bugsnag/utility/duplicator"
require "bugsnag/utility/metadata_delegate"
# 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.
#
# @yieldparam configuration [Configuration]
# @return [void]
def configure(validate_api_key=true)
yield(configuration) if block_given?
# Create the session tracker if sessions are enabled to avoid the overhead
# of creating it on the first request. We skip this if we're not validating
# the API key as we use this internally before the user's configure block
# has run, so we don't know if sessions are enabled yet.
session_tracker if validate_api_key && configuration.auto_capture_sessions
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
begin
yield(report) if block_given? && auto_notify
rescue StandardError => e
configuration.warn("Error in internal notify block: #{e}")
configuration.warn("Error in internal notify block stacktrace: #{e.backtrace.inspect}")
end
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.
begin
yield(report) if block_given? && !auto_notify
rescue StandardError => e
configuration.warn("Error in notify block: #{e}")
configuration.warn("Error in notify block stacktrace: #{e.backtrace.inspect}")
end
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
if report.unhandled_overridden?
# let the dashboard know that the unhandled flag was overridden
report.severity_reason[:unhandledOverridden] = true
end
deliver_notification(report)
end
end
##
# Registers an at_exit function to automatically catch errors on exit.
#
# @return [void]
def register_at_exit
return if at_exit_handler_installed?
@exit_handler_added = true
at_exit do
if $!
exception = unwrap_bundler_exception($!)
Bugsnag.notify(exception, 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.
#
# The {Bugsnag#configure} method will add this automatically, but it can be
# added manually using {Bugsnag#register_at_exit}.
#
# @return [Boolean]
def at_exit_handler_installed?
@exit_handler_added ||= false
end
##
# 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.
#
# @return [SessionTracker]
def session_tracker
@session_tracker = nil unless defined?(@session_tracker)
@session_tracker || LOCK.synchronize { @session_tracker ||= Bugsnag::SessionTracker.new}
end
##
# Starts a new session, which allows Bugsnag to track error rates across
# releases
#
# @return [void]
def start_session
session_tracker.start_session
end
##
# Stop any events being attributed to the current session until it is
# resumed or a new session is started
#
# @see resume_session
#
# @return [void]
def pause_session
session_tracker.pause_session
end
##
# Resume the current session if it was previously paused. If there is no
# current session, a new session will be started
#
# @see pause_session
#
# @return [Boolean] true if a paused session was resumed
def resume_session
session_tracker.resume_session
end
##
# Allow access to "before notify" callbacks as an array.
#
# These callbacks will be called whenever an error notification is being made.
#
# @deprecated Use {Bugsnag#add_on_error} instead
def before_notify_callbacks
Bugsnag.configuration.request_data[:before_callbacks] ||= []
end
##
# Attempts to load all integrations through auto-discovery.
#
# @return [void]
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.
#
# @param integration [Symbol] One of the integrations in {INTEGRATIONS}
# @return [void]
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, see {Bugsnag::BreadcrumbType}
# @param auto [Symbol] set to :auto if the breadcrumb is automatically created
# @return [void]
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 before_breadcrumb_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?
# Run on_breadcrumb callbacks
configuration.on_breadcrumb_callbacks.call(breadcrumb)
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
##
# Add the given callback to the list of on_error callbacks
#
# The on_error callbacks will be called when an error is captured or reported
# and are passed a {Bugsnag::Report} object
#
# Returning false from an on_error callback will cause the error to be ignored
# and will prevent any remaining callbacks from being called
#
# @param callback [Proc, Method, #call]
# @return [void]
def add_on_error(callback)
configuration.add_on_error(callback)
end
##
# Remove the given callback from the list of on_error callbacks
#
# Note that this must be the same Proc instance that was passed to
# {Bugsnag#add_on_error}, otherwise it will not be removed
#
# @param callback [Proc]
# @return [void]
def remove_on_error(callback)
configuration.remove_on_error(callback)
end
##
# Add the given callback to the list of on_breadcrumb callbacks
#
# The on_breadcrumb callbacks will be called when a breadcrumb is left and
# are passed the {Breadcrumbs::Breadcrumb Breadcrumb} object
#
# Returning false from an on_breadcrumb callback will cause the breadcrumb
# to be ignored and will prevent any remaining callbacks from being called
#
# @param callback [Proc, Method, #call]
# @return [void]
def add_on_breadcrumb(callback)
configuration.add_on_breadcrumb(callback)
end
##
# Remove the given callback from the list of on_breadcrumb callbacks
#
# Note that this must be the same instance that was passed to
# {add_on_breadcrumb}, otherwise it will not be removed
#
# @param callback [Proc, Method, #call]
# @return [void]
def remove_on_breadcrumb(callback)
configuration.remove_on_breadcrumb(callback)
end
##
# Returns the current list of breadcrumbs
#
# This is a per-thread circular buffer, containing at most 'max_breadcrumbs'
# breadcrumbs
#
# @return [Bugsnag::Utility::CircularBuffer]
def breadcrumbs
configuration.breadcrumbs
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
##
# Global metadata added to every event
#
# @return [Hash]
def metadata
configuration.metadata
end
##
# Add values to metadata
#
# @overload add_metadata(section, data)
# Merges data into the given section of metadata
# @param section [String, Symbol]
# @param data [Hash]
#
# @overload add_metadata(section, key, value)
# Sets key to value in the given section of metadata. If the value is nil
# the key will be deleted
# @param section [String, Symbol]
# @param key [String, Symbol]
# @param value
#
# @return [void]
def add_metadata(section, key_or_data, *args)
configuration.add_metadata(section, key_or_data, *args)
end
##
# Clear values from metadata
#
# @overload clear_metadata(section)
# Clears the given section of metadata
# @param section [String, Symbol]
#
# @overload clear_metadata(section, key)
# Clears the key in the given section of metadata
# @param section [String, Symbol]
# @param key [String, Symbol]
#
# @return [void]
def clear_metadata(section, *args)
configuration.clear_metadata(section, *args)
end
private
def should_deliver_notification?(exception, auto_notify)
return false unless configuration.enable_events
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]}")
options = { headers: report.headers }
delivery_method = Bugsnag::Delivery[configuration.delivery_method]
if delivery_method.respond_to?(:serialize_and_deliver)
delivery_method.serialize_and_deliver(
configuration.notify_endpoint,
proc { report_to_json(report) },
configuration,
options
)
else
delivery_method.deliver(
configuration.notify_endpoint,
report_to_json(report),
configuration,
options
)
end
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
##
# When running a script with 'bundle exec', uncaught exceptions will be
# converted to "friendly errors" which has the side effect of wrapping them
# in a SystemExit
#
# By default we ignore SystemExit, so need to unwrap the original exception
# in order to avoid ignoring real errors
#
# @param exception [Exception]
# @return [Exception]
def unwrap_bundler_exception(exception)
running_in_bundler = ENV.include?('BUNDLE_BIN_PATH')
# See if this exception came from Bundler's 'with_friendly_errors' method
return exception unless running_in_bundler
return exception unless exception.is_a?(SystemExit)
return exception unless exception.respond_to?(:cause)
return exception unless exception.backtrace.first.include?('/bundler/friendly_errors.rb')
return exception if exception.cause.nil?
unwrapped = exception.cause
# We may need to unwrap another level if the exception came from running
# an executable file directly (i.e. 'bundle exec <file>'). In this case
# there can be a SystemExit from 'with_friendly_errors' _and_ a SystemExit
# from 'kernel_load'
return unwrapped unless unwrapped.is_a?(SystemExit)
return unwrapped unless unwrapped.backtrace.first.include?('/bundler/cli/exec.rb')
return unwrapped if unwrapped.cause.nil?
unwrapped.cause
end
end
end
# rubocop:enable Metrics/ModuleLength
Bugsnag.load_integrations unless ENV["BUGSNAG_DISABLE_AUTOCONFIGURE"]