bugsnag 4.2.1 → 6.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.yardopts +12 -0
- data/CHANGELOG.md +814 -0
- data/README.md +21 -25
- data/VERSION +1 -1
- data/bugsnag.gemspec +19 -8
- data/lib/bugsnag/breadcrumb_type.rb +14 -0
- data/lib/bugsnag/breadcrumbs/breadcrumb.rb +109 -0
- data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +13 -0
- data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +48 -0
- data/lib/bugsnag/breadcrumbs/validator.rb +29 -0
- data/lib/bugsnag/cleaner.rb +170 -59
- data/lib/bugsnag/code_extractor.rb +137 -0
- data/lib/bugsnag/configuration.rb +670 -45
- data/lib/bugsnag/delivery/synchronous.rb +31 -14
- data/lib/bugsnag/delivery/thread_queue.rb +23 -6
- data/lib/bugsnag/delivery.rb +13 -0
- data/lib/bugsnag/endpoint_configuration.rb +11 -0
- data/lib/bugsnag/endpoint_validator.rb +80 -0
- data/lib/bugsnag/error.rb +25 -0
- data/lib/bugsnag/event.rb +5 -0
- data/lib/bugsnag/feature_flag.rb +74 -0
- data/lib/bugsnag/helpers.rb +121 -25
- data/lib/bugsnag/integrations/delayed_job.rb +51 -0
- data/lib/bugsnag/integrations/mailman.rb +43 -0
- data/lib/bugsnag/integrations/mongo.rb +133 -0
- data/lib/bugsnag/integrations/que.rb +53 -0
- data/lib/bugsnag/integrations/rack.rb +83 -0
- data/lib/bugsnag/integrations/rails/active_job.rb +100 -0
- data/lib/bugsnag/{rails → integrations/rails}/active_record_rescue.rb +10 -1
- data/lib/bugsnag/{rails → integrations/rails}/controller_methods.rb +1 -9
- data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +115 -0
- data/lib/bugsnag/integrations/railtie.rb +153 -0
- data/lib/bugsnag/integrations/rake.rb +74 -0
- data/lib/bugsnag/integrations/resque.rb +94 -0
- data/lib/bugsnag/integrations/shoryuken.rb +50 -0
- data/lib/bugsnag/integrations/sidekiq.rb +68 -0
- data/lib/bugsnag/meta_data.rb +1 -0
- data/lib/bugsnag/middleware/active_job.rb +18 -0
- data/lib/bugsnag/middleware/breadcrumbs.rb +21 -0
- data/lib/bugsnag/middleware/callbacks.rb +6 -8
- data/lib/bugsnag/middleware/classify_error.rb +50 -0
- data/lib/bugsnag/middleware/clearance_user.rb +33 -0
- data/lib/bugsnag/middleware/delayed_job.rb +93 -0
- data/lib/bugsnag/middleware/discard_error_class.rb +30 -0
- data/lib/bugsnag/middleware/exception_meta_data.rb +42 -0
- data/lib/bugsnag/middleware/ignore_error_class.rb +26 -0
- data/lib/bugsnag/middleware/mailman.rb +6 -4
- data/lib/bugsnag/middleware/rack_request.rb +126 -30
- data/lib/bugsnag/middleware/rails3_request.rb +15 -17
- data/lib/bugsnag/middleware/rake.rb +7 -5
- data/lib/bugsnag/middleware/session_data.rb +25 -0
- data/lib/bugsnag/middleware/sidekiq.rb +9 -4
- data/lib/bugsnag/middleware/suggestion_data.rb +34 -0
- data/lib/bugsnag/middleware/warden_user.rb +11 -6
- data/lib/bugsnag/middleware_stack.rb +62 -9
- data/lib/bugsnag/on_error_callbacks.rb +33 -0
- data/lib/bugsnag/report.rb +516 -0
- data/lib/bugsnag/session_tracker.rb +182 -0
- data/lib/bugsnag/stacktrace.rb +82 -0
- data/lib/bugsnag/tasks/bugsnag.rake +2 -70
- data/lib/bugsnag/utility/circular_buffer.rb +62 -0
- data/lib/bugsnag/utility/duplicator.rb +124 -0
- data/lib/bugsnag/utility/feature_data_store.rb +41 -0
- data/lib/bugsnag/utility/feature_flag_delegate.rb +89 -0
- data/lib/bugsnag/utility/metadata_delegate.rb +102 -0
- data/lib/bugsnag.rb +528 -80
- metadata +61 -123
- data/.document +0 -5
- data/.gitignore +0 -52
- data/.rspec +0 -3
- data/.travis.yml +0 -14
- data/CONTRIBUTING.md +0 -47
- data/Gemfile +0 -2
- data/Rakefile +0 -29
- data/lib/bugsnag/capistrano.rb +0 -7
- data/lib/bugsnag/capistrano2.rb +0 -32
- data/lib/bugsnag/delay/resque.rb +0 -21
- data/lib/bugsnag/delayed_job.rb +0 -57
- data/lib/bugsnag/deploy.rb +0 -34
- data/lib/bugsnag/mailman.rb +0 -28
- data/lib/bugsnag/middleware/rails2_request.rb +0 -52
- data/lib/bugsnag/notification.rb +0 -459
- data/lib/bugsnag/rack.rb +0 -53
- data/lib/bugsnag/rails/action_controller_rescue.rb +0 -62
- data/lib/bugsnag/rails.rb +0 -66
- data/lib/bugsnag/railtie.rb +0 -80
- data/lib/bugsnag/rake.rb +0 -25
- data/lib/bugsnag/resque.rb +0 -40
- data/lib/bugsnag/sidekiq.rb +0 -42
- data/lib/bugsnag/tasks/bugsnag.cap +0 -48
- data/rails/init.rb +0 -7
- data/spec/cleaner_spec.rb +0 -138
- data/spec/code_spec.rb +0 -86
- data/spec/fixtures/crashes/end_of_file.rb +0 -9
- data/spec/fixtures/crashes/short_file.rb +0 -1
- data/spec/fixtures/crashes/start_of_file.rb +0 -9
- data/spec/fixtures/middleware/internal_info_setter.rb +0 -11
- data/spec/fixtures/middleware/public_info_setter.rb +0 -11
- data/spec/fixtures/tasks/Rakefile +0 -15
- data/spec/helper_spec.rb +0 -163
- data/spec/integration_spec.rb +0 -132
- data/spec/middleware_spec.rb +0 -181
- data/spec/notification_spec.rb +0 -877
- data/spec/rack_spec.rb +0 -56
- data/spec/spec_helper.rb +0 -53
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative 'code_extractor'
|
2
|
+
|
3
|
+
module Bugsnag
|
4
|
+
module Stacktrace
|
5
|
+
# e.g. "org/jruby/RubyKernel.java:1264:in `catch'"
|
6
|
+
BACKTRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in [`']([^']+)')?$/
|
7
|
+
|
8
|
+
# e.g. "org.jruby.Ruby.runScript(Ruby.java:807)"
|
9
|
+
JAVA_BACKTRACE_REGEX = /^(.*)\((.*)(?::([0-9]+))?\)$/
|
10
|
+
|
11
|
+
##
|
12
|
+
# Process a backtrace and the configuration into a parsed stacktrace.
|
13
|
+
#
|
14
|
+
# @param backtrace [Array, nil] If nil, 'caller' will be used instead
|
15
|
+
# @param configuration [Configuration]
|
16
|
+
# @return [Array]
|
17
|
+
def self.process(backtrace, configuration)
|
18
|
+
code_extractor = CodeExtractor.new(configuration)
|
19
|
+
|
20
|
+
backtrace = caller if !backtrace || backtrace.empty?
|
21
|
+
|
22
|
+
processed_backtrace = backtrace.map do |trace|
|
23
|
+
# Parse the stacktrace line
|
24
|
+
if trace.match(BACKTRACE_LINE_REGEX)
|
25
|
+
file, line_str, method = [$1, $2, $3]
|
26
|
+
elsif trace.match(JAVA_BACKTRACE_REGEX)
|
27
|
+
method, file, line_str = [$1, $2, $3]
|
28
|
+
end
|
29
|
+
|
30
|
+
next if file.nil?
|
31
|
+
|
32
|
+
# Expand relative paths
|
33
|
+
file = File.realpath(file) rescue file
|
34
|
+
|
35
|
+
# Generate the stacktrace line hash
|
36
|
+
trace_hash = { lineNumber: line_str.to_i }
|
37
|
+
|
38
|
+
# Save a copy of the file path as we're about to modify it but need the
|
39
|
+
# raw version when extracting code (otherwise we can't open the file)
|
40
|
+
raw_file_path = file.dup
|
41
|
+
|
42
|
+
# Clean up the file path in the stacktrace
|
43
|
+
if defined?(configuration.project_root) && configuration.project_root.to_s != ''
|
44
|
+
trace_hash[:inProject] = true if file.start_with?(configuration.project_root.to_s)
|
45
|
+
file.sub!(/#{configuration.project_root}\//, "")
|
46
|
+
trace_hash.delete(:inProject) if vendor_path?(configuration, file)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Strip common gem path prefixes
|
50
|
+
if defined?(Gem)
|
51
|
+
Gem.path.each do |path|
|
52
|
+
file.sub!("#{path}/", "")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
trace_hash[:file] = file
|
57
|
+
|
58
|
+
# Add a method if we have it
|
59
|
+
trace_hash[:method] = method if method && (method =~ /^__bind/).nil?
|
60
|
+
|
61
|
+
# If we're going to send code then record the raw file path and the
|
62
|
+
# trace_hash, so we can extract from it later
|
63
|
+
code_extractor.add_file(raw_file_path, trace_hash) if configuration.send_code
|
64
|
+
|
65
|
+
trace_hash
|
66
|
+
end.compact
|
67
|
+
|
68
|
+
code_extractor.extract! if configuration.send_code
|
69
|
+
|
70
|
+
processed_backtrace
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
def self.vendor_path?(configuration, file_path)
|
75
|
+
return true if configuration.vendor_path && file_path.match(configuration.vendor_path)
|
76
|
+
|
77
|
+
configuration.vendor_paths.any? do |vendor_path|
|
78
|
+
file_path.start_with?("#{vendor_path.sub(/\/$/, '')}/")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -1,82 +1,14 @@
|
|
1
1
|
require "bugsnag"
|
2
2
|
|
3
3
|
namespace :bugsnag do
|
4
|
-
desc "Notify Bugsnag of a new deploy."
|
5
|
-
task :deploy do
|
6
|
-
api_key = ENV["BUGSNAG_API_KEY"]
|
7
|
-
release_stage = ENV["BUGSNAG_RELEASE_STAGE"]
|
8
|
-
app_version = ENV["BUGSNAG_APP_VERSION"]
|
9
|
-
revision = ENV["BUGSNAG_REVISION"]
|
10
|
-
repository = ENV["BUGSNAG_REPOSITORY"]
|
11
|
-
branch = ENV["BUGSNAG_BRANCH"]
|
12
|
-
|
13
|
-
Rake::Task["load"].invoke unless api_key
|
14
|
-
|
15
|
-
Bugsnag::Deploy.notify({
|
16
|
-
:api_key => api_key,
|
17
|
-
:release_stage => release_stage,
|
18
|
-
:app_version => app_version,
|
19
|
-
:revision => revision,
|
20
|
-
:repository => repository,
|
21
|
-
:branch => branch
|
22
|
-
})
|
23
|
-
end
|
24
|
-
|
25
4
|
desc "Send a test exception to Bugsnag."
|
26
5
|
task :test_exception => :load do
|
27
6
|
begin
|
28
7
|
raise RuntimeError.new("Bugsnag test exception")
|
29
8
|
rescue => e
|
30
|
-
Bugsnag.notify(e
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
desc "Show the bugsnag middleware stack"
|
35
|
-
task :middleware => :load do
|
36
|
-
Bugsnag.configuration.middleware.each {|m| puts m.to_s}
|
37
|
-
end
|
38
|
-
|
39
|
-
namespace :heroku do
|
40
|
-
desc "Add a heroku deploy hook to notify Bugsnag of deploys"
|
41
|
-
task :add_deploy_hook => :load do
|
42
|
-
# Wrapper to run command safely even in bundler
|
43
|
-
run_command = lambda { |command|
|
44
|
-
defined?(Bundler.with_clean_env) ? Bundler.with_clean_env { `#{command}` } : `#{command}`
|
45
|
-
}
|
46
|
-
|
47
|
-
# Fetch heroku config settings
|
48
|
-
config_command = "heroku config --shell"
|
49
|
-
config_command += " --app #{ENV["HEROKU_APP"]}" if ENV["HEROKU_APP"]
|
50
|
-
heroku_env = run_command.call(config_command).split(/[\n\r]/).each_with_object({}) do |c, obj|
|
51
|
-
k,v = c.split("=")
|
52
|
-
obj[k] = (v.nil? || v.strip.empty?) ? nil : v
|
53
|
-
end
|
54
|
-
|
55
|
-
# Check for Bugsnag API key (required)
|
56
|
-
api_key = heroku_env["BUGSNAG_API_KEY"] || Bugsnag.configuration.api_key || ENV["BUGSNAG_API_KEY"]
|
57
|
-
unless api_key
|
58
|
-
puts "Error: No API key found, have you run 'heroku config:set BUGSNAG_API_KEY=your-api-key'?"
|
59
|
-
next
|
9
|
+
Bugsnag.notify(e) do |report|
|
10
|
+
report.automatic_context = "rake#test_exception"
|
60
11
|
end
|
61
|
-
|
62
|
-
# Build the request, making use of deploy hook variables
|
63
|
-
# (https://devcenter.heroku.com/articles/deploy-hooks#customizing-messages)
|
64
|
-
params = {
|
65
|
-
:apiKey => api_key,
|
66
|
-
:branch => "master",
|
67
|
-
:revision => "{{head_long}}",
|
68
|
-
:releaseStage => heroku_env["RAILS_ENV"] || ENV["RAILS_ENV"] || "production"
|
69
|
-
}
|
70
|
-
repo = `git config --get remote.origin.url`.strip
|
71
|
-
params[:repository] = repo unless repo.empty?
|
72
|
-
|
73
|
-
# Add the hook
|
74
|
-
url = "https://notify.bugsnag.com/deploy?" + params.map {|k,v| "#{k}=#{v}"}.join("&")
|
75
|
-
command = "heroku addons:add deployhooks:http --url=\"#{url}\""
|
76
|
-
command += " --app #{ENV["HEROKU_APP"]}" if ENV["HEROKU_APP"]
|
77
|
-
|
78
|
-
puts "$ #{command}"
|
79
|
-
run_command.call(command)
|
80
12
|
end
|
81
13
|
end
|
82
14
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Bugsnag::Utility
|
2
|
+
##
|
3
|
+
# A container class with a maximum size, that removes oldest items as required.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
class CircularBuffer
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @return [Integer] the current maximum allowable number of items
|
10
|
+
attr_reader :max_items
|
11
|
+
|
12
|
+
##
|
13
|
+
# @param max_items [Integer] the initial maximum number of items
|
14
|
+
def initialize(max_items = 25)
|
15
|
+
@max_items = max_items
|
16
|
+
@buffer = []
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Adds an item to the circular buffer
|
21
|
+
#
|
22
|
+
# If this causes the buffer to exceed its maximum items, the oldest item will be removed
|
23
|
+
#
|
24
|
+
# @param item [Object] the item to add to the buffer
|
25
|
+
# @return [self] returns itself to allow method chaining
|
26
|
+
def <<(item)
|
27
|
+
@buffer << item
|
28
|
+
trim_buffer
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Iterates over the buffer
|
34
|
+
#
|
35
|
+
# @yield [Object] sequentially gives stored items to the block
|
36
|
+
def each(&block)
|
37
|
+
@buffer.each(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Sets the maximum allowable number of items
|
42
|
+
#
|
43
|
+
# If the current number of items exceeds the new maximum, oldest items will be removed
|
44
|
+
# until this is no longer the case
|
45
|
+
#
|
46
|
+
# @param new_max_items [Integer] the new allowed item maximum
|
47
|
+
def max_items=(new_max_items)
|
48
|
+
@max_items = new_max_items
|
49
|
+
trim_buffer
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
##
|
55
|
+
# Trims the buffer down to the current maximum allowable item number
|
56
|
+
def trim_buffer
|
57
|
+
trim_size = @buffer.size - @max_items
|
58
|
+
trim_size = 0 if trim_size < 0
|
59
|
+
@buffer.shift(trim_size)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Bugsnag::Utility
|
2
|
+
# @api private
|
3
|
+
class Duplicator
|
4
|
+
class << self
|
5
|
+
##
|
6
|
+
# Duplicate (deep clone) the given object
|
7
|
+
#
|
8
|
+
# @param object [Object]
|
9
|
+
# @param seen_objects [Hash<String, Object>]
|
10
|
+
# @return [Object]
|
11
|
+
def duplicate(object, seen_objects = {})
|
12
|
+
case object
|
13
|
+
# return immutable & non-duplicatable objects as-is
|
14
|
+
when Symbol, Numeric, Method, TrueClass, FalseClass, NilClass
|
15
|
+
object
|
16
|
+
when Array
|
17
|
+
duplicate_array(object, seen_objects)
|
18
|
+
when Hash
|
19
|
+
duplicate_hash(object, seen_objects)
|
20
|
+
when Range
|
21
|
+
duplicate_range(object, seen_objects)
|
22
|
+
when Struct
|
23
|
+
duplicate_struct(object, seen_objects)
|
24
|
+
else
|
25
|
+
duplicate_generic_object(object, seen_objects)
|
26
|
+
end
|
27
|
+
rescue StandardError
|
28
|
+
object
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def duplicate_array(array, seen_objects)
|
34
|
+
id = array.object_id
|
35
|
+
|
36
|
+
return seen_objects[id] if seen_objects.key?(id)
|
37
|
+
|
38
|
+
copy = array.dup
|
39
|
+
seen_objects[id] = copy
|
40
|
+
|
41
|
+
copy.map! do |value|
|
42
|
+
duplicate(value, seen_objects)
|
43
|
+
end
|
44
|
+
|
45
|
+
copy
|
46
|
+
end
|
47
|
+
|
48
|
+
def duplicate_hash(hash, seen_objects)
|
49
|
+
id = hash.object_id
|
50
|
+
|
51
|
+
return seen_objects[id] if seen_objects.key?(id)
|
52
|
+
|
53
|
+
copy = {}
|
54
|
+
seen_objects[id] = copy
|
55
|
+
|
56
|
+
hash.each do |key, value|
|
57
|
+
copy[duplicate(key, seen_objects)] = duplicate(value, seen_objects)
|
58
|
+
end
|
59
|
+
|
60
|
+
copy
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Ranges are immutable but the values they contain may not be
|
65
|
+
#
|
66
|
+
# For example, a range of "a".."z" can be mutated: range.first.upcase!
|
67
|
+
def duplicate_range(range, seen_objects)
|
68
|
+
id = range.object_id
|
69
|
+
|
70
|
+
return seen_objects[id] if seen_objects.key?(id)
|
71
|
+
|
72
|
+
begin
|
73
|
+
copy = range.class.new(
|
74
|
+
duplicate(range.first, seen_objects),
|
75
|
+
duplicate(range.last, seen_objects),
|
76
|
+
range.exclude_end?
|
77
|
+
)
|
78
|
+
rescue StandardError
|
79
|
+
copy = range.dup
|
80
|
+
end
|
81
|
+
|
82
|
+
seen_objects[id] = copy
|
83
|
+
end
|
84
|
+
|
85
|
+
def duplicate_struct(struct, seen_objects)
|
86
|
+
id = struct.object_id
|
87
|
+
|
88
|
+
return seen_objects[id] if seen_objects.key?(id)
|
89
|
+
|
90
|
+
copy = struct.dup
|
91
|
+
seen_objects[id] = copy
|
92
|
+
|
93
|
+
struct.each_pair do |attribute, value|
|
94
|
+
begin
|
95
|
+
copy.send("#{attribute}=", duplicate(value, seen_objects))
|
96
|
+
rescue StandardError # rubocop:todo Lint/SuppressedException
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
copy
|
101
|
+
end
|
102
|
+
|
103
|
+
def duplicate_generic_object(object, seen_objects)
|
104
|
+
id = object.object_id
|
105
|
+
|
106
|
+
return seen_objects[id] if seen_objects.key?(id)
|
107
|
+
|
108
|
+
copy = object.dup
|
109
|
+
seen_objects[id] = copy
|
110
|
+
|
111
|
+
begin
|
112
|
+
copy.instance_variables.each do |variable|
|
113
|
+
value = copy.instance_variable_get(variable)
|
114
|
+
|
115
|
+
copy.instance_variable_set(variable, duplicate(value, seen_objects))
|
116
|
+
end
|
117
|
+
rescue StandardError # rubocop:todo Lint/SuppressedException
|
118
|
+
end
|
119
|
+
|
120
|
+
copy
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Bugsnag::Utility
|
2
|
+
# @abstract Requires a #feature_flag_delegate method returning a
|
3
|
+
# {Bugsnag::Utility::FeatureFlagDelegate}
|
4
|
+
module FeatureDataStore
|
5
|
+
# Add a feature flag with the given name & variant
|
6
|
+
#
|
7
|
+
# @param name [String]
|
8
|
+
# @param variant [String, nil]
|
9
|
+
# @return [void]
|
10
|
+
def add_feature_flag(name, variant = nil)
|
11
|
+
feature_flag_delegate.add(name, variant)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Merge the given array of FeatureFlag instances into the stored feature
|
15
|
+
# flags
|
16
|
+
#
|
17
|
+
# New flags will be appended to the array. Flags with the same name will be
|
18
|
+
# overwritten, but their position in the array will not change
|
19
|
+
#
|
20
|
+
# @param feature_flags [Array<Bugsnag::FeatureFlag>]
|
21
|
+
# @return [void]
|
22
|
+
def add_feature_flags(feature_flags)
|
23
|
+
feature_flag_delegate.merge(feature_flags)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Remove the stored flag with the given name
|
27
|
+
#
|
28
|
+
# @param name [String]
|
29
|
+
# @return [void]
|
30
|
+
def clear_feature_flag(name)
|
31
|
+
feature_flag_delegate.remove(name)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Remove all the stored flags
|
35
|
+
#
|
36
|
+
# @return [void]
|
37
|
+
def clear_feature_flags
|
38
|
+
feature_flag_delegate.clear
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Bugsnag::Utility
|
2
|
+
# @api private
|
3
|
+
class FeatureFlagDelegate
|
4
|
+
def initialize
|
5
|
+
# feature flags are stored internally in a hash of "name" => <FeatureFlag>
|
6
|
+
# we don't use a Set because new feature flags should overwrite old ones
|
7
|
+
# that share a name, but FeatureFlag equality also uses the variant
|
8
|
+
@storage = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize_dup(original)
|
12
|
+
super
|
13
|
+
|
14
|
+
# copy the internal storage when 'dup' is called
|
15
|
+
@storage = @storage.dup
|
16
|
+
end
|
17
|
+
|
18
|
+
# Add a feature flag with the given name & variant
|
19
|
+
#
|
20
|
+
# @param name [String]
|
21
|
+
# @param variant [String, nil]
|
22
|
+
# @return [void]
|
23
|
+
def add(name, variant)
|
24
|
+
flag = Bugsnag::FeatureFlag.new(name, variant)
|
25
|
+
|
26
|
+
return unless flag.valid?
|
27
|
+
|
28
|
+
@storage[flag.name] = flag
|
29
|
+
end
|
30
|
+
|
31
|
+
# Merge the given array of FeatureFlag instances into the stored feature
|
32
|
+
# flags
|
33
|
+
#
|
34
|
+
# New flags will be appended to the array. Flags with the same name will be
|
35
|
+
# overwritten, but their position in the array will not change
|
36
|
+
#
|
37
|
+
# @param feature_flags [Array<Bugsnag::FeatureFlag>]
|
38
|
+
# @return [void]
|
39
|
+
def merge(feature_flags)
|
40
|
+
feature_flags.each do |flag|
|
41
|
+
next unless flag.is_a?(Bugsnag::FeatureFlag)
|
42
|
+
next unless flag.valid?
|
43
|
+
|
44
|
+
@storage[flag.name] = flag
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Remove the stored flag with the given name
|
49
|
+
#
|
50
|
+
# @param name [String]
|
51
|
+
# @return [void]
|
52
|
+
def remove(name)
|
53
|
+
@storage.delete(name)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Remove all the stored flags
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
def clear
|
60
|
+
@storage.clear
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get an array of FeatureFlag instances
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# [
|
67
|
+
# <#Bugsnag::FeatureFlag>,
|
68
|
+
# <#Bugsnag::FeatureFlag>,
|
69
|
+
# ]
|
70
|
+
#
|
71
|
+
# @return [Array<Bugsnag::FeatureFlag>]
|
72
|
+
def to_a
|
73
|
+
@storage.values
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get the feature flags in their JSON representation
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# [
|
80
|
+
# { "featureFlag" => "name", "variant" => "variant" },
|
81
|
+
# { "featureFlag" => "another name" },
|
82
|
+
# ]
|
83
|
+
#
|
84
|
+
# @return [Array<Hash{String => String}>]
|
85
|
+
def as_json
|
86
|
+
to_a.map(&:to_h)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Bugsnag::Utility
|
2
|
+
# @api private
|
3
|
+
class MetadataDelegate
|
4
|
+
# nil is a valid metadata value, so we need a sentinel object so we can tell
|
5
|
+
# if the value parameter has been provided
|
6
|
+
NOT_PROVIDED = Object.new
|
7
|
+
|
8
|
+
##
|
9
|
+
# Add values to metadata
|
10
|
+
#
|
11
|
+
# @overload add_metadata(metadata, section, data)
|
12
|
+
# Merges data into the given section of metadata
|
13
|
+
# @param metadata [Hash] The metadata hash to operate on
|
14
|
+
# @param section [String, Symbol]
|
15
|
+
# @param data [Hash]
|
16
|
+
#
|
17
|
+
# @overload add_metadata(metadata, section, key, value)
|
18
|
+
# Sets key to value in the given section of metadata. If the value is nil
|
19
|
+
# the key will be deleted
|
20
|
+
# @param metadata [Hash] The metadata hash to operate on
|
21
|
+
# @param section [String, Symbol]
|
22
|
+
# @param key [String, Symbol]
|
23
|
+
# @param value
|
24
|
+
#
|
25
|
+
# @return [void]
|
26
|
+
def add_metadata(metadata, section, key_or_data, value = NOT_PROVIDED)
|
27
|
+
case value
|
28
|
+
when NOT_PROVIDED
|
29
|
+
merge_metadata(metadata, section, key_or_data)
|
30
|
+
when nil
|
31
|
+
clear_metadata(metadata, section, key_or_data)
|
32
|
+
else
|
33
|
+
overwrite_metadata(metadata, section, key_or_data, value)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Clear values from metadata
|
39
|
+
#
|
40
|
+
# @overload clear_metadata(metadata, section)
|
41
|
+
# Clears the given section of metadata
|
42
|
+
# @param metadata [Hash] The metadata hash to operate on
|
43
|
+
# @param section [String, Symbol]
|
44
|
+
#
|
45
|
+
# @overload clear_metadata(metadata, section, key)
|
46
|
+
# Clears the key in the given section of metadata
|
47
|
+
# @param metadata [Hash] The metadata hash to operate on
|
48
|
+
# @param section [String, Symbol]
|
49
|
+
# @param key [String, Symbol]
|
50
|
+
#
|
51
|
+
# @return [void]
|
52
|
+
def clear_metadata(metadata, section, key = nil)
|
53
|
+
if key.nil?
|
54
|
+
metadata.delete(section)
|
55
|
+
elsif metadata[section]
|
56
|
+
metadata[section].delete(key)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
##
|
63
|
+
# Merge new metadata into the existing metadata
|
64
|
+
#
|
65
|
+
# Any keys with a 'nil' value in the new metadata will be deleted from the
|
66
|
+
# existing metadata
|
67
|
+
#
|
68
|
+
# @param existing_metadata [Hash]
|
69
|
+
# @param section [String, Symbol]
|
70
|
+
# @param new_metadata [Hash]
|
71
|
+
# @return [void]
|
72
|
+
def merge_metadata(existing_metadata, section, new_metadata)
|
73
|
+
return unless new_metadata.is_a?(Hash)
|
74
|
+
|
75
|
+
existing_metadata[section] ||= {}
|
76
|
+
data = existing_metadata[section]
|
77
|
+
|
78
|
+
new_metadata.each do |key, value|
|
79
|
+
if value.nil?
|
80
|
+
data.delete(key)
|
81
|
+
else
|
82
|
+
data[key] = value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Overwrite the value in metadata's section & key
|
89
|
+
#
|
90
|
+
# @param metadata [Hash]
|
91
|
+
# @param section [String, Symbol]
|
92
|
+
# @param key [String, Symbol]
|
93
|
+
# @param value
|
94
|
+
# @return [void]
|
95
|
+
def overwrite_metadata(metadata, section, key, value)
|
96
|
+
return unless key.is_a?(String) || key.is_a?(Symbol)
|
97
|
+
|
98
|
+
metadata[section] ||= {}
|
99
|
+
metadata[section][key] = value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|