Skip to content

Commit

Permalink
Track whether an error report was captured automatically (#369)
Browse files Browse the repository at this point in the history
* Added support for handled-unhandled
* Added severity-reasons to middleware
* Moved ignored classes into middleware to set info
  • Loading branch information
Cawllec authored and kattrali committed Oct 2, 2017
1 parent fb6db4f commit b3cfe6f
Show file tree
Hide file tree
Showing 19 changed files with 315 additions and 37 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ rvm:
- jruby-19mode

before_install:
- gem install bundler -v 1.12
- gem update --system
- bundle --version
- gem --version
18 changes: 17 additions & 1 deletion lib/bugsnag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require "bugsnag/middleware/mailman"
require "bugsnag/middleware/rake"
require "bugsnag/middleware/callbacks"
require "bugsnag/middleware/classify_error"

module Bugsnag
LOG_PREFIX = "** [Bugsnag] "
Expand All @@ -43,6 +44,9 @@ def configure(config_hash=nil)
# Use resque for asynchronous notification if required
require "bugsnag/delay/resque" if configuration.delay_with_resque && defined?(Resque)

# Add info error classifier to internal middleware
configuration.internal_middleware.use(Bugsnag::Middleware::ClassifyError)

# Warn if an api_key hasn't been set
@key_warning = false unless defined?(@key_warning)

Expand All @@ -64,8 +68,19 @@ def configure(config_hash=nil)
def notify(exception, overrides=nil, request_data=nil, &block)
notification = Notification.new(exception, configuration, overrides, request_data)

initial_severity = notification.severity
initial_reason = notification.severity_reason

yield(notification) if block_given?

if notification.severity != initial_severity
notification.severity_reason = {
:type => Bugsnag::Notification::USER_CALLBACK_SET_SEVERITY
}
else
notification.severity_reason = initial_reason
end

unless notification.ignore?
notification.deliver
notification
Expand All @@ -80,7 +95,8 @@ def notify(exception, overrides=nil, request_data=nil, &block)
# error class
def auto_notify(exception, overrides=nil, request_data=nil, &block)
overrides ||= {}
overrides.merge!({:severity => "error"})
overrides[:severity] = "error" unless overrides.has_key? :severity
overrides[:unhandled] = true unless overrides.has_key? :unhandled
notify_or_ignore(exception, overrides, request_data, &block) if configuration.auto_notify
end

Expand Down
16 changes: 1 addition & 15 deletions lib/bugsnag/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,6 @@ class Configuration
"rack.request.form_vars"
].freeze

DEFAULT_IGNORE_CLASSES = [
"AbstractController::ActionNotFound",
"ActionController::InvalidAuthenticityToken",
"ActionController::ParameterMissing",
"ActionController::UnknownAction",
"ActionController::UnknownFormat",
"ActionController::UnknownHttpMethod",
"ActiveRecord::RecordNotFound",
"CGI::Session::CookieStore::TamperedWithCookie",
"Mongoid::Errors::DocumentNotFound",
"SignalException",
"SystemExit",
].freeze

DEFAULT_IGNORE_USER_AGENTS = [].freeze

DEFAULT_DELIVERY_METHOD = :thread_queue
Expand All @@ -72,7 +58,7 @@ def initialize
self.send_environment = false
self.send_code = true
self.params_filters = Set.new(DEFAULT_PARAMS_FILTERS)
self.ignore_classes = Set.new(DEFAULT_IGNORE_CLASSES)
self.ignore_classes = Set.new()
self.ignore_user_agents = Set.new(DEFAULT_IGNORE_USER_AGENTS)
self.endpoint = DEFAULT_ENDPOINT
self.hostname = default_hostname
Expand Down
6 changes: 6 additions & 0 deletions lib/bugsnag/delayed_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ def error(job, error)
:job => {
:class => job.class.name,
:id => job.id,
},
:severity_reason => {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "DelayedJob"
}
}
}
if job.respond_to?(:queue) && (queue = job.queue)
Expand Down
9 changes: 8 additions & 1 deletion lib/bugsnag/mailman.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ def call(mail)
yield
rescue Exception => ex
raise ex if [Interrupt, SystemExit, SignalException].include? ex.class
Bugsnag.auto_notify(ex)
Bugsnag.auto_notify(ex, {
:severity_reason => {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "Mailman"
}
}
})
raise
ensure
Bugsnag.clear_request_data
Expand Down
53 changes: 53 additions & 0 deletions lib/bugsnag/middleware/classify_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module Bugsnag::Middleware
class ClassifyError
INFO_CLASSES = [
"AbstractController::ActionNotFound",
"ActionController::InvalidAuthenticityToken",
"ActionController::ParameterMissing",
"ActionController::UnknownAction",
"ActionController::UnknownFormat",
"ActionController::UnknownHttpMethod",
"ActiveRecord::RecordNotFound",
"CGI::Session::CookieStore::TamperedWithCookie",
"Mongoid::Errors::DocumentNotFound",
"SignalException",
"SystemExit"
]

def initialize(bugsnag)
@bugsnag = bugsnag
end

def call(notification)
notification.exceptions.each do |ex|

outer_break = false

ancestor_chain = ex.class.ancestors.select {
|ancestor| ancestor.is_a?(Class)
}.map {
|ancestor| ancestor.to_s
}

INFO_CLASSES.each do |info_class|
if ancestor_chain.include?(info_class)
notification.severity_reason = {
:type => Bugsnag::Notification::ERROR_CLASS,
:attributes => {
:errorClass => info_class
}
}
notification.severity = 'info'
outer_break = true
break
end
end

break if outer_break
end

@bugsnag.call(notification)
end
end
end

46 changes: 45 additions & 1 deletion lib/bugsnag/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ class Notification
NOTIFIER_VERSION = Bugsnag::VERSION
NOTIFIER_URL = "http://www.bugsnag.com"

HANDLED_EXCEPTION = "handledException"
UNHANDLED_EXCEPTION = "unhandledException"
UNHANDLED_EXCEPTION_MIDDLEWARE = "unhandledExceptionMiddleware"
ERROR_CLASS = "errorClass"
USER_SPECIFIED_SEVERITY = "userSpecifiedSeverity"
USER_CALLBACK_SET_SEVERITY = "userCallbackSetSeverity"

API_KEY_REGEX = /[0-9a-f]{32}/i

# e.g. "org/jruby/RubyKernel.java:1264:in `catch'"
Expand All @@ -31,6 +38,8 @@ class Notification

attr_accessor :context
attr_reader :user
attr_reader :severity
attr_accessor :severity_reason
attr_accessor :configuration
attr_accessor :meta_data

Expand All @@ -50,10 +59,36 @@ def initialize(exception, configuration, overrides = nil, request_data = nil)
@user = {}
@should_ignore = false
@severity = nil
@unhandled = false
@severity_reason = nil
@grouping_hash = nil
@delivery_method = nil

self.severity = @overrides[:severity]
if @overrides.key? :unhandled
@unhandled = @overrides[:unhandled]
@overrides.delete :unhandled
end

valid_severity = @overrides.key?(:severity) && SUPPORTED_SEVERITIES.include?(@overrides[:severity])
has_reason = @overrides.key? :severity_reason

if valid_severity && has_reason
@severity = @overrides[:severity]
@severity_reason = @overrides[:severity_reason]
elsif valid_severity
@severity = @overrides[:severity]
@severity_reason = {
:type => USER_SPECIFIED_SEVERITY
}
elsif has_reason
@severity_reason = @overrides[:severity_reason]
else
@severity_reason = {
:type => HANDLED_EXCEPTION
}
end

@overrides.delete :severity_reason
@overrides.delete :severity

if @overrides.key? :grouping_hash
Expand Down Expand Up @@ -214,12 +249,19 @@ def deliver
# make meta_data available to public middleware
@meta_data = generate_meta_data(@exceptions, @overrides)

initial_severity = self.severity

# Run the middleware here (including Bugsnag::Middleware::Callbacks)
# at the end of the middleware stack, execute the actual notification delivery
@configuration.middleware.run(self) do
# This supports self.ignore! for before_notify_callbacks.
return if @should_ignore

# Check to see if the severity has been changed
if initial_severity != self.severity

end

# Build the endpoint url
endpoint = (@configuration.use_ssl ? "https://" : "http://") + @configuration.endpoint
Bugsnag.log("Notifying #{endpoint} of #{@exceptions.last.class}")
Expand All @@ -243,6 +285,8 @@ def build_exception_payload
:payloadVersion => payload_version,
:exceptions => exception_list,
:severity => self.severity,
:unhandled => @unhandled,
:severityReason => @severity_reason,
:groupingHash => self.grouping_hash,
}

Expand Down
9 changes: 8 additions & 1 deletion lib/bugsnag/que.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
begin
job = job.dup # Make sure the original job object is not mutated.

Bugsnag.auto_notify(error) do |notification|
Bugsnag.auto_notify(error, {
:severity_reason => {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "Que"
}
}
}) do |notification|
job[:error_count] += 1

# If the job was scheduled using ActiveJob then unwrap the job details for clarity:
Expand Down
16 changes: 14 additions & 2 deletions lib/bugsnag/rack.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
module Bugsnag
class Rack

SEVERITY_REASON = {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "Rack"
}
}

def initialize(app)
@app = app

Expand Down Expand Up @@ -34,15 +42,19 @@ def call(env)
response = @app.call(env)
rescue Exception => raised
# Notify bugsnag of rack exceptions
Bugsnag.auto_notify(raised)
Bugsnag.auto_notify(raised, {
:severity_reason => SEVERITY_REASON
})

# Re-raise the exception
raise
end

# Notify bugsnag of rack exceptions
if env["rack.exception"]
Bugsnag.auto_notify(env["rack.exception"])
Bugsnag.auto_notify(env["rack.exception"], {
:severity_reason => SEVERITY_REASON
})
end

response
Expand Down
16 changes: 14 additions & 2 deletions lib/bugsnag/rails/action_controller_rescue.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Rails 2.x only
module Bugsnag::Rails
module ActionControllerRescue

SEVERITY_REASON = {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "Rails"
}
}

def self.included(base)
base.extend(ClassMethods)

Expand All @@ -22,13 +30,17 @@ def set_bugsnag_request_data
end

def rescue_action_in_public_with_bugsnag(exception)
Bugsnag.auto_notify(exception)
Bugsnag.auto_notify(exception, {
:severity_reason => SEVERITY_REASON
})

rescue_action_in_public_without_bugsnag(exception)
end

def rescue_action_locally_with_bugsnag(exception)
Bugsnag.auto_notify(exception)
Bugsnag.auto_notify(exception, {
:severity_reason => SEVERITY_REASON
})

rescue_action_locally_without_bugsnag(exception)
end
Expand Down
9 changes: 8 additions & 1 deletion lib/bugsnag/rails/active_record_rescue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ def run_callbacks(kind, *args, &block)
super
rescue StandardError => exception
# This exception will NOT be escalated, so notify it here.
Bugsnag.auto_notify(exception)
Bugsnag.auto_notify(exception, {
:severity_reason => {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "Rails"
}
}
})
raise
end
else
Expand Down
9 changes: 8 additions & 1 deletion lib/bugsnag/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ class Railtie < Rails::Railtie
runner do
at_exit do
if $!
Bugsnag.auto_notify($!)
Bugsnag.auto_notify($!, {
:severity_reason => {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "Rails"
}
}
})
end
end
end
Expand Down
9 changes: 8 additions & 1 deletion lib/bugsnag/rake.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ def execute_with_bugsnag(args=nil)
execute_without_bugsnag(args)

rescue Exception => ex
Bugsnag.auto_notify(ex)
Bugsnag.auto_notify(ex, {
:severity_reason => {
:type => Bugsnag::Notification::UNHANDLED_EXCEPTION_MIDDLEWARE,
:attributes => {
:framework => "Rake"
}
}
})
raise
ensure
Bugsnag.set_request_data :bugsnag_running_task, old_task
Expand Down
Loading

0 comments on commit b3cfe6f

Please sign in to comment.