Skip to content

Commit

Permalink
Add singleton class extension support to Bugsnag::Middleware::Excepti…
Browse files Browse the repository at this point in the history
…onMetaData

Using `Bugsnag::MetaData` to attach metadata or other Bugsnag-specific data directly to an exception class works great for the common case:

```
class MySpecialError < StandardError
  include Bugsnag::MetaData
end

error = MySpecialError.new("Oh no!")
error.bugsnag_meta_data = {foo: :bar}
```

However, it doesn't support dynamically extending an exception class at runtime.

```
begin
  do_a_thing
rescue => e
  e.extend(Bugsnag::MetaData)       # extend singleton class
  e.bugsnag_meta_data = {foo: :bar} # add context
  raise e                           # re-raise, let other code send to Bugsnag
end
```

This used to work in Bugsnag 5.1.0 [1] but broke when a type check was added in 6.0.0 [2].

This commit changes the implementation to use `respond_to?` over a type check. This offers a more flexible interface. For example, an exception need not include `Bugsnag::MetaData` directly, they need only to define the `bugsnag_meta_data` method.

This change also applies to `bugsnag_user_id`, `bugsnag_context`, and `bugsnag_grouping_hash`.

[1] https://github.com/bugsnag/bugsnag-ruby/blob/v5.1.0/lib/bugsnag/notification.rb#L311-L317
[2] https://github.com/bugsnag/bugsnag-ruby/blob/94cea3195d0fb3b3ed1c79ab38d9d4b1b9a732a3/lib/bugsnag/middleware/exception_meta_data.rb#L10
  • Loading branch information
jnraine committed Feb 5, 2018
1 parent 44c6ab1 commit f62cf92
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 14 deletions.
26 changes: 12 additions & 14 deletions lib/bugsnag/middleware/exception_meta_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,21 @@ def initialize(bugsnag)
def call(report)
# Apply the user's information attached to the exceptions
report.raw_exceptions.each do |exception|
if exception.class.include?(Bugsnag::MetaData)
if exception.bugsnag_user_id.is_a?(String)
report.user = {id: exception.bugsnag_user_id}
end
if exception.respond_to?(:bugsnag_user_id) && exception.bugsnag_user_id.is_a?(String)
report.user = {id: exception.bugsnag_user_id}
end

if exception.bugsnag_context.is_a?(String)
report.context = exception.bugsnag_context
end
if exception.respond_to?(:bugsnag_context) && exception.bugsnag_context.is_a?(String)
report.context = exception.bugsnag_context
end

if exception.bugsnag_grouping_hash.is_a?(String)
report.grouping_hash = exception.bugsnag_grouping_hash
end
if exception.respond_to?(:bugsnag_grouping_hash) && exception.bugsnag_grouping_hash.is_a?(String)
report.grouping_hash = exception.bugsnag_grouping_hash
end

if exception.respond_to?(:bugsnag_meta_data) && exception.bugsnag_meta_data
exception.bugsnag_meta_data.each do |key, value|
report.add_tab key, value
end
if exception.respond_to?(:bugsnag_meta_data) && exception.bugsnag_meta_data
exception.bugsnag_meta_data.each do |key, value|
report.add_tab key, value
end
end
end
Expand Down
90 changes: 90 additions & 0 deletions spec/middleware/exception_meta_data_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# encoding: utf-8
require 'spec_helper'

describe Bugsnag::Middleware::ExceptionMetaData do
let(:report_class) do
Class.new do
attr_accessor :raw_exceptions, :tabs, :user, :context, :grouping_hash

def initialize(errors)
self.raw_exceptions = Array(errors)
end

def add_tab(key, value)
self.tabs ||= {}
tabs[key] = value
end
end
end

let(:middleware) { Bugsnag::Middleware::ExceptionMetaData.new(lambda {|_|}) }
let(:bugsnag_error_class) { Class.new(StandardError) { include Bugsnag::MetaData } }

it "adds metadata when exception singleton class extended with Bugsnag::MetaData" do
error = RuntimeError.new
error.extend(Bugsnag::MetaData)
error.bugsnag_meta_data = {"foo" => "bar"}

report = report_class.new(error)

middleware.call(report)

expect(report.tabs).to eq({"foo" => "bar"})
end

it "adds metadata when exception class includes Bugsnag::MetaData" do
error = bugsnag_error_class.new
error.bugsnag_meta_data = {"foo" => "bar"}

report = report_class.new(error)

middleware.call(report)

expect(report.tabs).to eq({"foo" => "bar"})
end

it "sets user ID when a string" do
error = bugsnag_error_class.new
error.bugsnag_user_id = "1234"

report = report_class.new(error)

middleware.call(report)

expect(report.user).to eq({id: "1234"})
end

it "sets context when a string" do
error = bugsnag_error_class.new
error.bugsnag_context = "Foo#bar"

report = report_class.new(error)

middleware.call(report)

expect(report.context).to eq("Foo#bar")
end

it "sets grouping_hash when a string" do
error = bugsnag_error_class.new
error.bugsnag_grouping_hash = "abcdef"

report = report_class.new(error)

middleware.call(report)

expect(report.grouping_hash).to eq("abcdef")
end

it "does nothing when no bugsnag attributes are set" do
error = bugsnag_error_class.new
report = report_class.new(error)

middleware.call(report)

expect(report.user).to eq(nil)
expect(report.tabs).to eq(nil)
expect(report.grouping_hash).to eq(nil)
expect(report.context).to eq(nil)
end
end

0 comments on commit f62cf92

Please sign in to comment.