diff --git a/lib/bugsnag.rb b/lib/bugsnag.rb index 06273b318..2fbcd2d9f 100644 --- a/lib/bugsnag.rb +++ b/lib/bugsnag.rb @@ -34,7 +34,8 @@ def configure # Explicitly notify of an exception def notify(exception, auto_notify=false, &block) - if auto_notify && !configuration.auto_notify + + if !configuration.auto_notify && auto_notify configuration.debug("Not notifying because auto_notify is disabled") return end @@ -49,7 +50,7 @@ def notify(exception, auto_notify=false, &block) return end - report = Report.new(exception, configuration) + 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 @@ -65,6 +66,10 @@ def notify(exception, auto_notify=false, &block) 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? @@ -80,6 +85,15 @@ def notify(exception, auto_notify=false, &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 => Bugsnag::Report::USER_CALLBACK_SET_SEVERITY + } + else + report.severity_reason = initial_reason + end + # Deliver configuration.info("Notifying #{configuration.endpoint} of #{report.exceptions.last[:errorClass]}") payload_string = ::JSON.dump(Bugsnag::Helpers.trim_if_needed(report.as_json)) diff --git a/lib/bugsnag/configuration.rb b/lib/bugsnag/configuration.rb index 38b23e4cc..cb922a356 100644 --- a/lib/bugsnag/configuration.rb +++ b/lib/bugsnag/configuration.rb @@ -5,6 +5,7 @@ require "bugsnag/middleware/callbacks" require "bugsnag/middleware/exception_meta_data" require "bugsnag/middleware/ignore_error_class" +require "bugsnag/middleware/classify_error" module Bugsnag class Configuration @@ -73,6 +74,7 @@ def initialize self.internal_middleware = Bugsnag::MiddlewareStack.new self.internal_middleware.use Bugsnag::Middleware::ExceptionMetaData self.internal_middleware.use Bugsnag::Middleware::IgnoreErrorClass + self.internal_middleware.use Bugsnag::Middleware::ClassifyError self.middleware = Bugsnag::MiddlewareStack.new self.middleware.use Bugsnag::Middleware::Callbacks diff --git a/lib/bugsnag/integrations/delayed_job.rb b/lib/bugsnag/integrations/delayed_job.rb index 235ca2098..123badce8 100644 --- a/lib/bugsnag/integrations/delayed_job.rb +++ b/lib/bugsnag/integrations/delayed_job.rb @@ -9,6 +9,11 @@ module Delayed module Plugins class Bugsnag < Plugin + + FRAMEWORK_ATTRIBUTES = { + :framework => "DelayedJob" + } + module Notify def error(job, error) overrides = { @@ -36,6 +41,10 @@ def error(job, error) ::Bugsnag.notify(error, true) do |report| report.severity = "error" + report.severity_reason = { + :type => ::Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } report.meta_data.merge! overrides end diff --git a/lib/bugsnag/integrations/mailman.rb b/lib/bugsnag/integrations/mailman.rb index c0dc7cd5f..78eeecfd9 100644 --- a/lib/bugsnag/integrations/mailman.rb +++ b/lib/bugsnag/integrations/mailman.rb @@ -2,6 +2,11 @@ module Bugsnag class Mailman + + FRAMEWORK_ATTRIBUTES = { + :framework => "Mailman" + } + def initialize Bugsnag.configuration.internal_middleware.use(Bugsnag::Middleware::Mailman) Bugsnag.configuration.app_type = "mailman" @@ -15,6 +20,10 @@ def call(mail) raise ex if [Interrupt, SystemExit, SignalException].include? ex.class Bugsnag.notify(ex, true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } end raise ensure diff --git a/lib/bugsnag/integrations/rack.rb b/lib/bugsnag/integrations/rack.rb index 121628146..b23d78a53 100644 --- a/lib/bugsnag/integrations/rack.rb +++ b/lib/bugsnag/integrations/rack.rb @@ -1,5 +1,10 @@ module Bugsnag class Rack + + FRAMEWORK_ATTRIBUTES = { + :framework => "Rack" + } + def initialize(app) @app = app @@ -35,6 +40,10 @@ def call(env) # Notify bugsnag of rack exceptions Bugsnag.notify(raised, true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => Bugsnag::Rack::FRAMEWORK_ATTRIBUTES + } end # Re-raise the exception @@ -45,6 +54,10 @@ def call(env) if env["rack.exception"] Bugsnag.notify(env["rack.exception"], true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } end end diff --git a/lib/bugsnag/integrations/rails/active_record_rescue.rb b/lib/bugsnag/integrations/rails/active_record_rescue.rb index aee619ad7..65843e872 100644 --- a/lib/bugsnag/integrations/rails/active_record_rescue.rb +++ b/lib/bugsnag/integrations/rails/active_record_rescue.rb @@ -1,6 +1,9 @@ module Bugsnag::Rails module ActiveRecordRescue KINDS = [:commit, :rollback].freeze + FRAMEWORK_ATTRIBUTES = { + :framework => "Rails" + } def run_callbacks(kind, *args, &block) if KINDS.include?(kind) @@ -10,6 +13,10 @@ def run_callbacks(kind, *args, &block) # This exception will NOT be escalated, so notify it here. Bugsnag.notify(exception, true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } end raise end diff --git a/lib/bugsnag/integrations/railtie.rb b/lib/bugsnag/integrations/railtie.rb index f2047f394..126a4495b 100644 --- a/lib/bugsnag/integrations/railtie.rb +++ b/lib/bugsnag/integrations/railtie.rb @@ -7,6 +7,11 @@ module Bugsnag class Railtie < Rails::Railtie + + FRAMEWORK_ATTRIBUTES = { + :framework => "Rails" + } + rake_tasks do require "bugsnag/integrations/rake" load "bugsnag/tasks/bugsnag.rake" @@ -19,6 +24,10 @@ class Railtie < Rails::Railtie if $! Bugsnag.notify($!, true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } end end end diff --git a/lib/bugsnag/integrations/rake.rb b/lib/bugsnag/integrations/rake.rb index a33e5eaea..4756f2c94 100644 --- a/lib/bugsnag/integrations/rake.rb +++ b/lib/bugsnag/integrations/rake.rb @@ -4,6 +4,10 @@ class Rake::Task + FRAMEWORK_ATTRIBUTES = { + :framework => "Rake" + } + def execute_with_bugsnag(args=nil) Bugsnag.configuration.app_type = "rake" old_task = Bugsnag.configuration.request_data[:bugsnag_running_task] @@ -14,6 +18,10 @@ def execute_with_bugsnag(args=nil) rescue Exception => ex Bugsnag.notify(ex, true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } end raise ensure diff --git a/lib/bugsnag/integrations/resque.rb b/lib/bugsnag/integrations/resque.rb index 9abba7814..409db0e38 100644 --- a/lib/bugsnag/integrations/resque.rb +++ b/lib/bugsnag/integrations/resque.rb @@ -3,6 +3,11 @@ module Bugsnag class Resque < ::Resque::Failure::Base + + FRAMEWORK_ATTRIBUTES = { + :framework => "Resque" + } + def self.configure(&block) add_failure_backend Bugsnag.configure(&block) @@ -28,6 +33,10 @@ def self.add_failure_backend def save Bugsnag.notify(exception, true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } report.meta_data.merge!({:context => "#{payload['class']}@#{queue}", :payload => payload, :delivery_method => :synchronous}) end end diff --git a/lib/bugsnag/integrations/sidekiq.rb b/lib/bugsnag/integrations/sidekiq.rb index f403afcb1..8239e4b2f 100644 --- a/lib/bugsnag/integrations/sidekiq.rb +++ b/lib/bugsnag/integrations/sidekiq.rb @@ -2,6 +2,11 @@ module Bugsnag class Sidekiq + + FRAMEWORK_ATTRIBUTES = { + :framework => "Sidekiq" + } + def initialize Bugsnag.configuration.internal_middleware.use(Bugsnag::Middleware::Sidekiq) Bugsnag.configuration.app_type = "sidekiq" @@ -18,6 +23,10 @@ def call(worker, msg, queue) raise ex if [Interrupt, SystemExit, SignalException].include? ex.class Bugsnag.notify(ex, true) do |report| report.severity = "error" + report.severity_reason = { + :type => Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE, + :attributes => FRAMEWORK_ATTRIBUTES + } end raise ensure diff --git a/lib/bugsnag/middleware/classify_error.rb b/lib/bugsnag/middleware/classify_error.rb new file mode 100644 index 000000000..0867a6432 --- /dev/null +++ b/lib/bugsnag/middleware/classify_error.rb @@ -0,0 +1,48 @@ +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(report) + report.raw_exceptions.each do |ex| + + 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) + report.severity_reason = { + :type => Bugsnag::Report::ERROR_CLASS, + :attributes => { + :errorClass => info_class + } + } + report.severity = 'info' + break + end + end + end + + @bugsnag.call(report) + end + end +end + \ No newline at end of file diff --git a/lib/bugsnag/report.rb b/lib/bugsnag/report.rb index 5f3c289eb..3ed2d78e0 100644 --- a/lib/bugsnag/report.rb +++ b/lib/bugsnag/report.rb @@ -8,6 +8,13 @@ class Report NOTIFIER_VERSION = Bugsnag::VERSION NOTIFIER_URL = "http://www.bugsnag.com" + UNHANDLED_EXCEPTION = "unhandledException" + UNHANDLED_EXCEPTION_MIDDLEWARE = "unhandledExceptionMiddleware" + ERROR_CLASS = "errorClass" + HANDLED_EXCEPTION = "handledException" + USER_SPECIFIED_SEVERITY = "userSpecifiedSeverity" + USER_CALLBACK_SET_SEVERITY = "userCallbackSetSeverity" + MAX_EXCEPTIONS_TO_UNWRAP = 5 CURRENT_PAYLOAD_VERSION = "2" @@ -25,10 +32,12 @@ class Report attr_accessor :raw_exceptions attr_accessor :release_stage attr_accessor :severity + attr_accessor :severity_reason attr_accessor :user - def initialize(exception, passed_configuration) + def initialize(exception, passed_configuration, auto_notify=false) @should_ignore = false + @unhandled = auto_notify self.configuration = passed_configuration @@ -42,7 +51,8 @@ def initialize(exception, passed_configuration) self.hostname = configuration.hostname self.meta_data = {} self.release_stage = configuration.release_stage - self.severity = "warning" + self.severity = auto_notify ? "error" : "warning" + self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION} self.user = {} end @@ -84,6 +94,8 @@ def as_json groupingHash: grouping_hash, payloadVersion: CURRENT_PAYLOAD_VERSION, severity: severity, + severityReason: severity_reason, + unhandled: @unhandled, user: user } diff --git a/lib/bugsnag/tasks/bugsnag.rake b/lib/bugsnag/tasks/bugsnag.rake index 25618bef2..be214b3da 100644 --- a/lib/bugsnag/tasks/bugsnag.rake +++ b/lib/bugsnag/tasks/bugsnag.rake @@ -6,7 +6,9 @@ namespace :bugsnag do begin raise RuntimeError.new("Bugsnag test exception") rescue => e - Bugsnag.notify(e, {:context => "rake#test_exception"}) + Bugsnag.notify(e) do |report| + report.context = "rake#test_exception" + end end end end diff --git a/spec/middleware_spec.rb b/spec/middleware_spec.rb index 24b9f8f91..267669e4f 100644 --- a/spec/middleware_spec.rb +++ b/spec/middleware_spec.rb @@ -178,4 +178,43 @@ def call(report) } end + it "doesn't allow handledState properties to be changed in middleware" do + HandledStateChanger = Class.new do + def initialize(bugsnag) + @bugsnag = bugsnag + end + + def call(report) + report.severity_reason = { + :test => "test" + } + @bugsnag.call(report) + end + end + + Bugsnag.configure do |c| + c.middleware.use HandledStateChanger + end + + Bugsnag.notify(BugsnagTestException.new("It crashed"), true) do |report| + report.severity_reason = { + :type => "middleware_handler", + :attributes => { + :name => "middleware_test" + } + } + end + + expect(Bugsnag).to have_sent_notification{ |payload| + event = get_event_from_payload(payload) + expect(event["unhandled"]).to be true + expect(event["severityReason"]).to eq({ + "type" => "middleware_handler", + "attributes" => { + "name" => "middleware_test" + } + }) + } + end + end diff --git a/spec/report_spec.rb b/spec/report_spec.rb index 7a0947bc8..dbdae3b52 100644 --- a/spec/report_spec.rb +++ b/spec/report_spec.rb @@ -387,18 +387,6 @@ def gloops } end - it "autonotifies errors" do - Bugsnag.notify(BugsnagTestException.new("It crashed"), true) do |report| - report.severity = "error" - end - - expect(Bugsnag).to have_sent_notification{ |payload| - event = get_event_from_payload(payload) - expect(event["severity"]).to eq("error") - } - end - - it "accepts a context in overrides" do Bugsnag.notify(BugsnagTestException.new("It crashed")) do |report| report.context = 'test_context' @@ -421,7 +409,7 @@ def gloops } end - it "does not send a notification if auto_notify is false" do + it "does not send an automatic notification if auto_notify is false" do Bugsnag.configure do |config| config.auto_notify = false end @@ -888,6 +876,57 @@ def gloops } end + it 'should use defaults when notify is called' do + Bugsnag.notify(BugsnagTestException.new("It crashed")) + + expect(Bugsnag).to have_sent_notification{ |payload| + event = payload["events"][0] + expect(event["unhandled"]).to be false + expect(event["severityReason"]).to eq({"type" => "handledException"}) + } + end + + it 'should attach severity reason through a block when auto_notify is true' do + Bugsnag.notify(BugsnagTestException.new("It crashed"), true) do |report| + report.severity_reason = { + :type => "middleware_handler", + :attributes => { + :name => "middleware_test" + } + } + end + + expect(Bugsnag).to have_sent_notification{ |payload| + event = payload["events"][0] + expect(event["severityReason"]).to eq( + { + "type" => "middleware_handler", + "attributes" => { + "name" => "middleware_test" + } + } + ) + expect(event["unhandled"]).to be true + } + end + + it 'should not attach severity reason from callback when auto_notify is false' do + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |report| + report.severity_reason = { + :type => "middleware_handler", + :attributes => { + :name => "middleware_test" + } + } + end + + expect(Bugsnag).to have_sent_notification{ |payload| + event = payload["events"][0] + expect(event["unhandled"]).to be false + expect(event["severityReason"]).to eq({"type" => "handledException"}) + } + end + if defined?(JRUBY_VERSION) it "should work with java.lang.Throwables" do