diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d99662..df621388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +## TBD + +### Enhancements + +* Use `Exception#detailed_message` instead of `Exception#message` when available + | [#761](https://github.com/bugsnag/bugsnag-ruby/pull/761) + ## v6.26.0 (1 December 2022) ### Enhancements diff --git a/lib/bugsnag/report.rb b/lib/bugsnag/report.rb index ca635395..c2958d39 100644 --- a/lib/bugsnag/report.rb +++ b/lib/bugsnag/report.rb @@ -428,9 +428,11 @@ def update_handled_counts(is_unhandled, was_unhandled) def generate_exception_list raw_exceptions.map do |exception| + class_name = error_class(exception) + { - errorClass: error_class(exception), - message: exception.message, + errorClass: class_name, + message: error_message(exception, class_name), stacktrace: Stacktrace.process(exception.backtrace, configuration) } end @@ -448,6 +450,26 @@ def error_class(exception) (exception.is_a? Class) ? exception.name : exception.class.name end + def error_message(exception, class_name) + # Ruby 3.2 added Exception#detailed_message for Gems like "Did you mean" + # to annotate an exception's message + return exception.message unless exception.respond_to?(:detailed_message) + + # the "highlight" argument may add terminal escape codes to the output, + # which we don't want to include + # it _should_ always be present but it's possible to forget to add it or + # to have implemented this method before Ruby 3.2 + message = + begin + exception.detailed_message(highlight: false) + rescue ArgumentError + exception.detailed_message + end + + # remove the class name to be consistent with Exception#message + message.sub(" (#{class_name})", '') + end + def generate_raw_exceptions(exception) exceptions = [] diff --git a/spec/report_spec.rb b/spec/report_spec.rb index b226dcae..3756a741 100644 --- a/spec/report_spec.rb +++ b/spec/report_spec.rb @@ -28,6 +28,18 @@ def gloops end end +if RUBY_VERSION >= '2.0.0' + require_relative './support/exception_with_detailed_message' +else + require_relative './support/exception_with_detailed_message_ruby_1' +end + +class ExceptionWithDetailedMessageButNoHighlight < Exception + def detailed_message + "detail about '#{self}'" + end +end + shared_examples "Report or Event tests" do |class_to_test| context "metadata" do include_examples( @@ -99,6 +111,17 @@ def gloops }) end + it "uses Exception#detailed_message if available" do + exception = ExceptionWithDetailedMessage.new("some message") + report_or_event = class_to_test.new(exception, Bugsnag.configuration) + + expect(report_or_event.summary).to eq({ + error_class: "ExceptionWithDetailedMessage", + message: "some message with some extra detail", + severity: "warning" + }) + end + it "handles empty exceptions" do begin 1/0 @@ -288,6 +311,17 @@ def gloops ) }) end + + it "uses Exception#detailed_message if available" do + exception = ExceptionWithDetailedMessage.new("some message") + report_or_event = class_to_test.new(exception, Bugsnag.configuration) + + expect(report_or_event.errors.length).to eq(1) + + message = report_or_event.errors.first.error_message + + expect(message).to eq("some message with some extra detail") + end end it "has a reference to the original error" do @@ -1399,7 +1433,6 @@ def gloops end it "does not unwrap more than 5 exceptions" do - first_ex = ex = NestedException.new("Deep exception") 10.times do |idx| ex = ex.original_exception = NestedException.new("Deep exception #{idx}") @@ -1432,6 +1465,26 @@ def gloops } end + it "uses Exception#detailed_message if available" do + Bugsnag.notify(ExceptionWithDetailedMessage.new("some message")) + + expect(Bugsnag).to have_sent_notification{ |payload, headers| + exception = get_exception_from_payload(payload) + expect(exception["errorClass"]).to eq("ExceptionWithDetailedMessage") + expect(exception["message"]).to eq("some message with some extra detail") + } + end + + it "handles implementations of Exception#detailed_message with no 'highlight' parameter" do + Bugsnag.notify(ExceptionWithDetailedMessageButNoHighlight.new("some message")) + + expect(Bugsnag).to have_sent_notification{ |payload, headers| + exception = get_exception_from_payload(payload) + expect(exception["errorClass"]).to eq("ExceptionWithDetailedMessageButNoHighlight") + expect(exception["message"]).to eq("detail about 'some message'") + } + end + it "supports unix-style paths in backtraces" do ex = BugsnagTestException.new("It crashed") ex.set_backtrace([ diff --git a/spec/support/exception_with_detailed_message.rb b/spec/support/exception_with_detailed_message.rb new file mode 100644 index 00000000..60f07962 --- /dev/null +++ b/spec/support/exception_with_detailed_message.rb @@ -0,0 +1,10 @@ +class ExceptionWithDetailedMessage < Exception + def detailed_message(highlight: true) + # change the output to ensure we pass the right value for "highlight" + if highlight + "\e[1m!!! #{self} !!!\e[0m" + else + "#{self} with some extra detail" + end + end +end diff --git a/spec/support/exception_with_detailed_message_ruby_1.rb b/spec/support/exception_with_detailed_message_ruby_1.rb new file mode 100644 index 00000000..c610983f --- /dev/null +++ b/spec/support/exception_with_detailed_message_ruby_1.rb @@ -0,0 +1,11 @@ +# ExceptionWithDetailedMessage for Ruby 1.9 (no keyword argument) +class ExceptionWithDetailedMessage < Exception + def detailed_message(params) + # change the output to ensure we pass the right value for "highlight" + if params[:highlight] + "\e[1m!!! #{self} !!!\e[0m" + else + "#{self} with some extra detail" + end + end +end