diff --git a/CHANGELOG.md b/CHANGELOG.md index 3560db3e..4dcf959e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Changelog | [#689](https://github.com/bugsnag/bugsnag-ruby/pull/689) * Add `time` (an ISO8601 string in UTC) to `device` metadata | [#690](https://github.com/bugsnag/bugsnag-ruby/pull/690) +* Add `errors` to `Report`/`Event` containing an array of `Error` objects. The `Error` object contains `error_class`, `error_message` and `type` (always "ruby") + | [#691](https://github.com/bugsnag/bugsnag-ruby/pull/691) ### Fixes @@ -37,6 +39,7 @@ Changelog * The `Breadcrumb#name` attribute has been deprecated in favour of `Breadcrumb#message` * The breadcrumb type constants in the `Bugsnag::Breadcrumbs` module has been deprecated in favour of the constants available in the `Bugsnag::BreadcrumbType` module For example, `Bugsnag::Breadcrumbs::ERROR_BREADCRUMB_TYPE` is now available as `Bugsnag::BreadcrumbType::ERROR` +* `Report#exceptions` has been deprecated in favour of the new `errors` property ## v6.22.1 (11 August 2021) diff --git a/lib/bugsnag/error.rb b/lib/bugsnag/error.rb new file mode 100644 index 00000000..581b4ade --- /dev/null +++ b/lib/bugsnag/error.rb @@ -0,0 +1,9 @@ +module Bugsnag + # @!attribute error_class + # @return [String] the error's class name + # @!attribute error_message + # @return [String] the error's message + # @!attribute type + # @return [String] the type of error (always "ruby") + Error = Struct.new(:error_class, :error_message, :type) +end diff --git a/lib/bugsnag/report.rb b/lib/bugsnag/report.rb index b63c472f..9c8be25e 100644 --- a/lib/bugsnag/report.rb +++ b/lib/bugsnag/report.rb @@ -1,8 +1,10 @@ require "json" require "pathname" +require "bugsnag/error" require "bugsnag/stacktrace" module Bugsnag + # rubocop:todo Metrics/ClassLength class Report NOTIFIER_NAME = "Ruby Bugsnag Notifier" NOTIFIER_VERSION = Bugsnag::VERSION @@ -19,6 +21,9 @@ class Report CURRENT_PAYLOAD_VERSION = "4.0" + # @api private + ERROR_TYPE = "ruby".freeze + # Whether this report is for a handled or unhandled error # @return [Boolean] attr_reader :unhandled @@ -51,6 +56,7 @@ class Report attr_accessor :delivery_method # The list of exceptions in this report + # @deprecated Use {#errors} instead # @return [Array] attr_accessor :exceptions @@ -99,6 +105,10 @@ class Report # @return [Hash] attr_accessor :user + # A list of errors in this report + # @return [Array] + attr_reader :errors + ## # Initializes a new report from an exception. def initialize(exception, passed_configuration, auto_notify=false) @@ -112,6 +122,7 @@ def initialize(exception, passed_configuration, auto_notify=false) self.raw_exceptions = generate_raw_exceptions(exception) self.exceptions = generate_exception_list + @errors = generate_error_list self.api_key = configuration.api_key self.app_type = configuration.app_type @@ -303,6 +314,12 @@ def generate_exception_list end end + def generate_error_list + exceptions.map do |exception| + Error.new(exception[:errorClass], exception[:message], ERROR_TYPE) + end + end + def error_class(exception) # The "Class" check is for some strange exceptions like Timeout::Error # which throw the error class instead of an instance @@ -345,4 +362,5 @@ def generate_raw_exceptions(exception) exceptions end end + # rubocop:enable Metrics/ClassLength end diff --git a/spec/report_spec.rb b/spec/report_spec.rb index 9375924b..0db481aa 100644 --- a/spec/report_spec.rb +++ b/spec/report_spec.rb @@ -125,6 +125,73 @@ def gloops end end end + + describe "#errors" do + it "has required hash keys" do + exception = RuntimeError.new("example error") + report = class_to_test.new(exception, Bugsnag.configuration) + + expect(report.errors).to eq( + [Bugsnag::Error.new("RuntimeError", "example error", "ruby")] + ) + end + + it "includes errors that caused the top-most exception" do + begin + begin + raise "one" + rescue + Ruby21Exception.raise!("two") + end + rescue => exception + end + + report = class_to_test.new(exception, Bugsnag.configuration) + + expect(report.errors).to eq( + [ + Bugsnag::Error.new("Ruby21Exception", "two", "ruby"), + Bugsnag::Error.new("RuntimeError", "one", "ruby") + ] + ) + end + + it "cannot be assigned to" do + exception = RuntimeError.new("example error") + report = class_to_test.new(exception, Bugsnag.configuration) + + expect(report).not_to respond_to(:errors=) + end + + it "can be mutated" do + exception = RuntimeError.new("example error") + report = class_to_test.new(exception, Bugsnag.configuration) + + report.errors.push("haha") + report.errors.push("haha 2") + report.errors.pop + + expect(report.errors).to eq( + [ + Bugsnag::Error.new("RuntimeError", "example error", "ruby"), + "haha" + ] + ) + end + + it "contains mutable data" do + exception = RuntimeError.new("example error") + report = class_to_test.new(exception, Bugsnag.configuration) + + report.errors.first.error_class = "haha" + report.errors.first.error_message = "ahah" + report.errors.first.type = "aahh" + + expect(report.errors).to eq( + [Bugsnag::Error.new("haha", "ahah", "aahh")] + ) + end + end end # rubocop:disable Metrics/BlockLength