Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to store metadata globally #699

Merged
merged 4 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Changelog

* Allow overriding an event's unhandled flag
| [#698](https://github.com/bugsnag/bugsnag-ruby/pull/698)
* Add the ability to store metadata globally
| [#699](https://github.com/bugsnag/bugsnag-ruby/pull/699)

## v6.23.0 (21 September 2021)

Expand Down
46 changes: 46 additions & 0 deletions lib/bugsnag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
require "bugsnag/breadcrumbs/breadcrumb"
require "bugsnag/breadcrumbs/breadcrumbs"

require "bugsnag/utility/duplicator"
require "bugsnag/utility/metadata_delegate"

# rubocop:todo Metrics/ModuleLength
Expand Down Expand Up @@ -357,6 +358,51 @@ def cleaner
end
end

##
# Global metadata added to every event
#
# @return [Hash]
def metadata
configuration.metadata
end

##
# Add values to metadata
#
# @overload add_metadata(section, data)
# Merges data into the given section of metadata
# @param section [String, Symbol]
# @param data [Hash]
#
# @overload add_metadata(section, key, value)
# Sets key to value in the given section of metadata. If the value is nil
# the key will be deleted
# @param section [String, Symbol]
# @param key [String, Symbol]
# @param value
#
# @return [void]
def add_metadata(section, key_or_data, *args)
configuration.add_metadata(section, key_or_data, *args)
end

##
# Clear values from metadata
#
# @overload clear_metadata(section)
# Clears the given section of metadata
# @param section [String, Symbol]
#
# @overload clear_metadata(section, key)
# Clears the key in the given section of metadata
# @param section [String, Symbol]
# @param key [String, Symbol]
#
# @return [void]
def clear_metadata(section, *args)
configuration.clear_metadata(section, *args)
end

private

def should_deliver_notification?(exception, auto_notify)
Expand Down
48 changes: 48 additions & 0 deletions lib/bugsnag/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ class Configuration
# @return [String, nil]
attr_accessor :context

# Global metadata added to every event
# @return [Hash]
attr_reader :metadata

# @api private
# @return [Array<String>]
attr_reader :scopes_to_filter
Expand Down Expand Up @@ -218,6 +222,9 @@ def initialize
@session_endpoint = DEFAULT_SESSION_ENDPOINT
@enable_sessions = true

@metadata = {}
@metadata_delegate = Utility::MetadataDelegate.new

# SystemExit and SignalException are common Exception types seen with
# successful exits and are not automatically reported to Bugsnag
# TODO move these defaults into `discard_classes` when `ignore_classes`
Expand Down Expand Up @@ -556,6 +563,47 @@ def remove_on_breadcrumb(callback)
@on_breadcrumb_callbacks.remove(callback)
end

##
# Add values to metadata
#
# @overload add_metadata(section, data)
# Merges data into the given section of metadata
# @param section [String, Symbol]
# @param data [Hash]
#
# @overload add_metadata(section, key, value)
# Sets key to value in the given section of metadata. If the value is nil
# the key will be deleted
# @param section [String, Symbol]
# @param key [String, Symbol]
# @param value
#
# @return [void]
def add_metadata(section, key_or_data, *args)
@mutex.synchronize do
@metadata_delegate.add_metadata(@metadata, section, key_or_data, *args)
end
end

##
# Clear values from metadata
#
# @overload clear_metadata(section)
# Clears the given section of metadata
# @param section [String, Symbol]
#
# @overload clear_metadata(section, key)
# Clears the key in the given section of metadata
# @param section [String, Symbol]
# @param key [String, Symbol]
#
# @return [void]
def clear_metadata(section, *args)
@mutex.synchronize do
@metadata_delegate.clear_metadata(@metadata, section, *args)
end
end

##
# Has the context been explicitly set?
#
Expand Down
2 changes: 1 addition & 1 deletion lib/bugsnag/report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def initialize(exception, passed_configuration, auto_notify=false)
self.delivery_method = configuration.delivery_method
self.hostname = configuration.hostname
self.runtime_versions = configuration.runtime_versions.dup
self.meta_data = {}
self.meta_data = Utility::Duplicator.duplicate(configuration.metadata)
self.release_stage = configuration.release_stage
self.severity = auto_notify ? "error" : "warning"
self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
Expand Down
124 changes: 124 additions & 0 deletions lib/bugsnag/utility/duplicator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
module Bugsnag::Utility
# @api private
class Duplicator
class << self
##
# Duplicate (deep clone) the given object
#
# @param object [Object]
# @param seen_objects [Hash<String, Object>]
# @return [Object]
def duplicate(object, seen_objects = {})
case object
# return immutable & non-duplicatable objects as-is
when Symbol, Numeric, Method, TrueClass, FalseClass, NilClass
object
when Array
duplicate_array(object, seen_objects)
when Hash
duplicate_hash(object, seen_objects)
when Range
duplicate_range(object, seen_objects)
when Struct
duplicate_struct(object, seen_objects)
else
duplicate_generic_object(object, seen_objects)
end
rescue StandardError
object
end

private

def duplicate_array(array, seen_objects)
id = array.object_id

return seen_objects[id] if seen_objects.key?(id)

copy = array.dup
seen_objects[id] = copy

copy.map! do |value|
duplicate(value, seen_objects)
end

copy
end

def duplicate_hash(hash, seen_objects)
id = hash.object_id

return seen_objects[id] if seen_objects.key?(id)

copy = {}
seen_objects[id] = copy

hash.each do |key, value|
copy[duplicate(key, seen_objects)] = duplicate(value, seen_objects)
end

copy
end

##
# Ranges are immutable but the values they contain may not be
#
# For example, a range of "a".."z" can be mutated: range.first.upcase!
def duplicate_range(range, seen_objects)
id = range.object_id

return seen_objects[id] if seen_objects.key?(id)

begin
copy = range.class.new(
duplicate(range.first, seen_objects),
duplicate(range.last, seen_objects),
range.exclude_end?
)
rescue StandardError
copy = range.dup
end

seen_objects[id] = copy
end

def duplicate_struct(struct, seen_objects)
id = struct.object_id

return seen_objects[id] if seen_objects.key?(id)

copy = struct.dup
seen_objects[id] = copy

struct.each_pair do |attribute, value|
begin
copy.send("#{attribute}=", duplicate(value, seen_objects))
rescue StandardError # rubocop:todo Lint/SuppressedException
end
end

copy
end

def duplicate_generic_object(object, seen_objects)
id = object.object_id

return seen_objects[id] if seen_objects.key?(id)

copy = object.dup
seen_objects[id] = copy

begin
copy.instance_variables.each do |variable|
value = copy.instance_variable_get(variable)

copy.instance_variable_set(variable, duplicate(value, seen_objects))
end
rescue StandardError # rubocop:todo Lint/SuppressedException
end

copy
end
end
end
end
80 changes: 80 additions & 0 deletions spec/bugsnag_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# encoding: utf-8
require 'spec_helper'
require 'support/shared_examples_for_metadata'

describe Bugsnag do

Expand Down Expand Up @@ -920,4 +921,83 @@ module Kernel
})
end
end

describe "global metadata" do
include_examples(
"metadata delegate",
lambda do |metadata, *args|
Bugsnag.configuration.instance_variable_set(:@metadata, metadata)

Bugsnag.add_metadata(*args)
end,
lambda do |metadata, *args|
Bugsnag.configuration.instance_variable_set(:@metadata, metadata)

Bugsnag.clear_metadata(*args)
end
)

describe "#metadata" do
it "is initially empty" do
expect(subject.metadata).to be_empty
end

it "cannot be reassigned" do
expect(subject).not_to respond_to(:metadata=)
end

it "reflects changes made by add_/clear_metadata" do
subject.add_metadata(:abc, { a: 1, b: 2, c: 3 })
subject.add_metadata(:xyz, :x, 1)

expect(subject.metadata).to eq({ abc: { a: 1, b: 2, c: 3 }, xyz: { x: 1 } })

subject.clear_metadata(:abc)

expect(subject.metadata).to eq({ xyz: { x: 1 } })
end
end

it "is added to the payload" do
Bugsnag.add_metadata(:abc, { a: 1, b: 2, c: 3 })
Bugsnag.add_metadata(:xyz, { x: 1, y: 2, z: 3 })
Bugsnag.add_metadata(:example, { array: [1, 2, 3], string: "hello" })

Bugsnag.notify(RuntimeError.new("example")) do |report|
report.add_metadata(:abc, :d, 4)
report.metadata[:example][:array].push(4, 5, 6)
report.metadata[:example][:string].upcase!

report.clear_metadata(:abc, :b)
report.clear_metadata(:xyz, :z)
Bugsnag.clear_metadata(:abc)

expect(report.metadata).to eq({
abc: { a: 1, c: 3, d: 4 },
xyz: { x: 1, y: 2 },
example: { array: [1, 2, 3, 4, 5, 6], string: "HELLO" },
})

expect(Bugsnag.metadata).to eq({
xyz: { x: 1, y: 2, z: 3 },
example: { array: [1, 2, 3], string: "hello" },
})
end

expect(Bugsnag).to(have_sent_notification { |payload, headers|
event = get_event_from_payload(payload)

expect(event["metaData"]).to eq({
"abc" => { "a" => 1, "c" => 3, "d" => 4 },
"xyz" => { "x" => 1, "y" => 2 },
"example" => { "array" => [1, 2, 3, 4, 5, 6], "string" => "HELLO" },
})

expect(Bugsnag.metadata).to eq({
xyz: { x: 1, y: 2, z: 3 },
example: { array: [1, 2, 3], string: "hello" },
})
})
end
end
end
Loading