bugsnag 6.19.0 → 6.26.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +176 -0
- data/VERSION +1 -1
- data/bugsnag.gemspec +18 -1
- data/lib/bugsnag/breadcrumb_type.rb +14 -0
- data/lib/bugsnag/breadcrumbs/breadcrumb.rb +34 -1
- data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +1 -0
- data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +50 -0
- data/lib/bugsnag/cleaner.rb +31 -18
- data/lib/bugsnag/configuration.rb +243 -25
- data/lib/bugsnag/delivery/synchronous.rb +2 -2
- data/lib/bugsnag/delivery/thread_queue.rb +2 -2
- 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 +7 -0
- data/lib/bugsnag/feature_flag.rb +74 -0
- data/lib/bugsnag/integrations/mongo.rb +5 -3
- data/lib/bugsnag/integrations/rack.rb +3 -3
- data/lib/bugsnag/integrations/rails/active_job.rb +102 -0
- data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +2 -0
- data/lib/bugsnag/integrations/railtie.rb +70 -27
- data/lib/bugsnag/integrations/resque.rb +17 -3
- data/lib/bugsnag/integrations/sidekiq.rb +1 -0
- data/lib/bugsnag/middleware/active_job.rb +18 -0
- data/lib/bugsnag/middleware/classify_error.rb +1 -0
- data/lib/bugsnag/middleware/delayed_job.rb +21 -2
- data/lib/bugsnag/middleware/exception_meta_data.rb +2 -0
- data/lib/bugsnag/middleware/rack_request.rb +84 -19
- data/lib/bugsnag/middleware/rails3_request.rb +2 -2
- data/lib/bugsnag/middleware/rake.rb +1 -1
- data/lib/bugsnag/middleware/session_data.rb +3 -1
- data/lib/bugsnag/middleware/sidekiq.rb +1 -1
- data/lib/bugsnag/middleware/suggestion_data.rb +9 -7
- data/lib/bugsnag/middleware_stack.rb +6 -6
- data/lib/bugsnag/report.rb +204 -8
- data/lib/bugsnag/session_tracker.rb +52 -12
- data/lib/bugsnag/stacktrace.rb +13 -2
- data/lib/bugsnag/tasks/bugsnag.rake +1 -1
- 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 +156 -8
- metadata +24 -7
data/lib/bugsnag/report.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
require "json"
|
2
2
|
require "pathname"
|
3
|
+
require "bugsnag/error"
|
3
4
|
require "bugsnag/stacktrace"
|
4
5
|
|
5
6
|
module Bugsnag
|
7
|
+
# rubocop:todo Metrics/ClassLength
|
6
8
|
class Report
|
9
|
+
include Utility::FeatureDataStore
|
10
|
+
|
7
11
|
NOTIFIER_NAME = "Ruby Bugsnag Notifier"
|
8
12
|
NOTIFIER_VERSION = Bugsnag::VERSION
|
9
13
|
NOTIFIER_URL = "https://www.bugsnag.com"
|
@@ -45,16 +49,13 @@ module Bugsnag
|
|
45
49
|
# @return [Configuration]
|
46
50
|
attr_accessor :configuration
|
47
51
|
|
48
|
-
# Additional context for this report
|
49
|
-
# @return [String, nil]
|
50
|
-
attr_accessor :context
|
51
|
-
|
52
52
|
# The delivery method that will be used for this report
|
53
53
|
# @see Configuration#delivery_method
|
54
54
|
# @return [Symbol]
|
55
55
|
attr_accessor :delivery_method
|
56
56
|
|
57
57
|
# The list of exceptions in this report
|
58
|
+
# @deprecated Use {#errors} instead
|
58
59
|
# @return [Array<Hash>]
|
59
60
|
attr_accessor :exceptions
|
60
61
|
|
@@ -72,10 +73,12 @@ module Bugsnag
|
|
72
73
|
attr_accessor :grouping_hash
|
73
74
|
|
74
75
|
# Arbitrary metadata attached to this report
|
76
|
+
# @deprecated Use {#metadata} instead
|
75
77
|
# @return [Hash]
|
76
78
|
attr_accessor :meta_data
|
77
79
|
|
78
80
|
# The raw Exception instances for this report
|
81
|
+
# @deprecated Use {#original_error} instead
|
79
82
|
# @see #exceptions
|
80
83
|
# @return [Array<Exception>]
|
81
84
|
attr_accessor :raw_exceptions
|
@@ -102,31 +105,68 @@ module Bugsnag
|
|
102
105
|
# @return [Hash]
|
103
106
|
attr_accessor :user
|
104
107
|
|
108
|
+
# A list of errors in this report
|
109
|
+
# @return [Array<Error>]
|
110
|
+
attr_reader :errors
|
111
|
+
|
112
|
+
# The Exception instance this report was created for
|
113
|
+
# @return [Exception]
|
114
|
+
attr_reader :original_error
|
115
|
+
|
105
116
|
##
|
106
117
|
# Initializes a new report from an exception.
|
107
118
|
def initialize(exception, passed_configuration, auto_notify=false)
|
119
|
+
# store the creation time for use as device.time
|
120
|
+
@created_at = Time.now.utc.iso8601(3)
|
121
|
+
|
108
122
|
@should_ignore = false
|
109
123
|
@unhandled = auto_notify
|
124
|
+
@initial_unhandled = @unhandled
|
110
125
|
|
111
126
|
self.configuration = passed_configuration
|
112
127
|
|
128
|
+
@original_error = exception
|
113
129
|
self.raw_exceptions = generate_raw_exceptions(exception)
|
114
130
|
self.exceptions = generate_exception_list
|
131
|
+
@errors = generate_error_list
|
115
132
|
|
116
133
|
self.api_key = configuration.api_key
|
117
134
|
self.app_type = configuration.app_type
|
118
135
|
self.app_version = configuration.app_version
|
119
136
|
self.breadcrumbs = []
|
137
|
+
self.context = configuration.context if configuration.context_set?
|
120
138
|
self.delivery_method = configuration.delivery_method
|
121
139
|
self.hostname = configuration.hostname
|
122
140
|
self.runtime_versions = configuration.runtime_versions.dup
|
123
|
-
self.meta_data =
|
141
|
+
self.meta_data = Utility::Duplicator.duplicate(configuration.metadata)
|
124
142
|
self.release_stage = configuration.release_stage
|
125
143
|
self.severity = auto_notify ? "error" : "warning"
|
126
144
|
self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
|
127
145
|
self.user = {}
|
146
|
+
|
147
|
+
@metadata_delegate = Utility::MetadataDelegate.new
|
148
|
+
@feature_flag_delegate = Bugsnag.feature_flag_delegate.dup
|
128
149
|
end
|
129
150
|
|
151
|
+
##
|
152
|
+
# Additional context for this report
|
153
|
+
# @!attribute context
|
154
|
+
# @return [String, nil]
|
155
|
+
def context
|
156
|
+
return @context if defined?(@context)
|
157
|
+
|
158
|
+
@automatic_context
|
159
|
+
end
|
160
|
+
|
161
|
+
attr_writer :context
|
162
|
+
|
163
|
+
##
|
164
|
+
# Context set automatically by Bugsnag uses this attribute, which prevents
|
165
|
+
# it from overwriting the user-supplied context
|
166
|
+
# @api private
|
167
|
+
# @return [String, nil]
|
168
|
+
attr_accessor :automatic_context
|
169
|
+
|
130
170
|
##
|
131
171
|
# Add a new metadata tab to this notification.
|
132
172
|
#
|
@@ -135,6 +175,8 @@ module Bugsnag
|
|
135
175
|
# exists, this will be merged with the existing values. If a Hash is not
|
136
176
|
# given, the value will be placed into the 'custom' tab
|
137
177
|
# @return [void]
|
178
|
+
#
|
179
|
+
# @deprecated Use {#add_metadata} instead
|
138
180
|
def add_tab(name, value)
|
139
181
|
return if name.nil?
|
140
182
|
|
@@ -153,6 +195,8 @@ module Bugsnag
|
|
153
195
|
#
|
154
196
|
# @param name [String]
|
155
197
|
# @return [void]
|
198
|
+
#
|
199
|
+
# @deprecated Use {#clear_metadata} instead
|
156
200
|
def remove_tab(name)
|
157
201
|
return if name.nil?
|
158
202
|
|
@@ -175,9 +219,11 @@ module Bugsnag
|
|
175
219
|
context: context,
|
176
220
|
device: {
|
177
221
|
hostname: hostname,
|
178
|
-
runtimeVersions: runtime_versions
|
222
|
+
runtimeVersions: runtime_versions,
|
223
|
+
time: @created_at
|
179
224
|
},
|
180
225
|
exceptions: exceptions,
|
226
|
+
featureFlags: @feature_flag_delegate.as_json,
|
181
227
|
groupingHash: grouping_hash,
|
182
228
|
metaData: meta_data,
|
183
229
|
session: session,
|
@@ -197,6 +243,7 @@ module Bugsnag
|
|
197
243
|
:version => NOTIFIER_VERSION,
|
198
244
|
:url => NOTIFIER_URL
|
199
245
|
},
|
246
|
+
:payloadVersion => CURRENT_PAYLOAD_VERSION,
|
200
247
|
:events => [payload_event]
|
201
248
|
}
|
202
249
|
end
|
@@ -257,24 +304,172 @@ module Bugsnag
|
|
257
304
|
end
|
258
305
|
end
|
259
306
|
|
307
|
+
# A Hash containing arbitrary metadata
|
308
|
+
# @!attribute metadata
|
309
|
+
# @return [Hash]
|
310
|
+
def metadata
|
311
|
+
@meta_data
|
312
|
+
end
|
313
|
+
|
314
|
+
# @param metadata [Hash]
|
315
|
+
# @return [void]
|
316
|
+
def metadata=(metadata)
|
317
|
+
@meta_data = metadata
|
318
|
+
end
|
319
|
+
|
320
|
+
##
|
321
|
+
# Data from the current HTTP request. May be nil if no data has been recorded
|
322
|
+
#
|
323
|
+
# @return [Hash, nil]
|
324
|
+
def request
|
325
|
+
@meta_data[:request]
|
326
|
+
end
|
327
|
+
|
328
|
+
##
|
329
|
+
# Add values to metadata
|
330
|
+
#
|
331
|
+
# @overload add_metadata(section, data)
|
332
|
+
# Merges data into the given section of metadata
|
333
|
+
# @param section [String, Symbol]
|
334
|
+
# @param data [Hash]
|
335
|
+
#
|
336
|
+
# @overload add_metadata(section, key, value)
|
337
|
+
# Sets key to value in the given section of metadata. If the value is nil
|
338
|
+
# the key will be deleted
|
339
|
+
# @param section [String, Symbol]
|
340
|
+
# @param key [String, Symbol]
|
341
|
+
# @param value
|
342
|
+
#
|
343
|
+
# @return [void]
|
344
|
+
def add_metadata(section, key_or_data, *args)
|
345
|
+
@metadata_delegate.add_metadata(@meta_data, section, key_or_data, *args)
|
346
|
+
end
|
347
|
+
|
348
|
+
##
|
349
|
+
# Clear values from metadata
|
350
|
+
#
|
351
|
+
# @overload clear_metadata(section)
|
352
|
+
# Clears the given section of metadata
|
353
|
+
# @param section [String, Symbol]
|
354
|
+
#
|
355
|
+
# @overload clear_metadata(section, key)
|
356
|
+
# Clears the key in the given section of metadata
|
357
|
+
# @param section [String, Symbol]
|
358
|
+
# @param key [String, Symbol]
|
359
|
+
#
|
360
|
+
# @return [void]
|
361
|
+
def clear_metadata(section, *args)
|
362
|
+
@metadata_delegate.clear_metadata(@meta_data, section, *args)
|
363
|
+
end
|
364
|
+
|
365
|
+
# Get the array of stored feature flags
|
366
|
+
#
|
367
|
+
# @return [Array<Bugsnag::FeatureFlag>]
|
368
|
+
def feature_flags
|
369
|
+
@feature_flag_delegate.to_a
|
370
|
+
end
|
371
|
+
|
372
|
+
##
|
373
|
+
# Set information about the current user
|
374
|
+
#
|
375
|
+
# Additional user fields can be added as metadata in a "user" section
|
376
|
+
#
|
377
|
+
# Setting a field to 'nil' will remove it from the user data
|
378
|
+
#
|
379
|
+
# @param id [String, nil]
|
380
|
+
# @param email [String, nil]
|
381
|
+
# @param name [String, nil]
|
382
|
+
# @return [void]
|
383
|
+
def set_user(id = nil, email = nil, name = nil)
|
384
|
+
new_user = { id: id, email: email, name: name }
|
385
|
+
new_user.reject! { |key, value| value.nil? }
|
386
|
+
|
387
|
+
@user = new_user
|
388
|
+
end
|
389
|
+
|
390
|
+
def unhandled=(new_unhandled)
|
391
|
+
# fix the handled/unhandled counts in the current session
|
392
|
+
update_handled_counts(new_unhandled, @unhandled)
|
393
|
+
|
394
|
+
@unhandled = new_unhandled
|
395
|
+
end
|
396
|
+
|
397
|
+
##
|
398
|
+
# Returns true if the unhandled flag has been changed from its initial value
|
399
|
+
#
|
400
|
+
# @api private
|
401
|
+
# @return [Boolean]
|
402
|
+
def unhandled_overridden?
|
403
|
+
@unhandled != @initial_unhandled
|
404
|
+
end
|
405
|
+
|
260
406
|
private
|
261
407
|
|
408
|
+
attr_reader :feature_flag_delegate
|
409
|
+
|
410
|
+
def update_handled_counts(is_unhandled, was_unhandled)
|
411
|
+
# do nothing if there is no session to update
|
412
|
+
return if @session.nil?
|
413
|
+
|
414
|
+
# increment the counts for the current unhandled value
|
415
|
+
if is_unhandled
|
416
|
+
@session[:events][:unhandled] += 1
|
417
|
+
else
|
418
|
+
@session[:events][:handled] += 1
|
419
|
+
end
|
420
|
+
|
421
|
+
# decrement the counts for the previous unhandled value
|
422
|
+
if was_unhandled
|
423
|
+
@session[:events][:unhandled] -= 1
|
424
|
+
else
|
425
|
+
@session[:events][:handled] -= 1
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
262
429
|
def generate_exception_list
|
263
430
|
raw_exceptions.map do |exception|
|
431
|
+
class_name = error_class(exception)
|
432
|
+
|
264
433
|
{
|
265
|
-
errorClass:
|
266
|
-
message: exception
|
434
|
+
errorClass: class_name,
|
435
|
+
message: error_message(exception, class_name),
|
267
436
|
stacktrace: Stacktrace.process(exception.backtrace, configuration)
|
268
437
|
}
|
269
438
|
end
|
270
439
|
end
|
271
440
|
|
441
|
+
def generate_error_list
|
442
|
+
exceptions.map do |exception|
|
443
|
+
Error.new(exception[:errorClass], exception[:message], exception[:stacktrace])
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
272
447
|
def error_class(exception)
|
273
448
|
# The "Class" check is for some strange exceptions like Timeout::Error
|
274
449
|
# which throw the error class instead of an instance
|
275
450
|
(exception.is_a? Class) ? exception.name : exception.class.name
|
276
451
|
end
|
277
452
|
|
453
|
+
def error_message(exception, class_name)
|
454
|
+
# Ruby 3.2 added Exception#detailed_message for Gems like "Did you mean"
|
455
|
+
# to annotate an exception's message
|
456
|
+
return exception.message unless exception.respond_to?(:detailed_message)
|
457
|
+
|
458
|
+
# the "highlight" argument may add terminal escape codes to the output,
|
459
|
+
# which we don't want to include
|
460
|
+
# it _should_ always be present but it's possible to forget to add it or
|
461
|
+
# to have implemented this method before Ruby 3.2
|
462
|
+
message =
|
463
|
+
begin
|
464
|
+
exception.detailed_message(highlight: false)
|
465
|
+
rescue ArgumentError
|
466
|
+
exception.detailed_message
|
467
|
+
end
|
468
|
+
|
469
|
+
# remove the class name to be consistent with Exception#message
|
470
|
+
message.sub(" (#{class_name})", '')
|
471
|
+
end
|
472
|
+
|
278
473
|
def generate_raw_exceptions(exception)
|
279
474
|
exceptions = []
|
280
475
|
|
@@ -311,4 +506,5 @@ module Bugsnag
|
|
311
506
|
exceptions
|
312
507
|
end
|
313
508
|
end
|
509
|
+
# rubocop:enable Metrics/ClassLength
|
314
510
|
end
|
@@ -34,16 +34,20 @@ module Bugsnag
|
|
34
34
|
# Starts a new session, storing it on the current thread.
|
35
35
|
#
|
36
36
|
# This allows Bugsnag to track error rates for a release.
|
37
|
+
#
|
38
|
+
# @return [void]
|
37
39
|
def start_session
|
38
|
-
return unless Bugsnag.configuration.enable_sessions
|
40
|
+
return unless Bugsnag.configuration.enable_sessions && Bugsnag.configuration.should_notify_release_stage?
|
41
|
+
|
39
42
|
start_delivery_thread
|
40
43
|
start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00')
|
41
44
|
new_session = {
|
42
|
-
:
|
43
|
-
:
|
44
|
-
|
45
|
-
|
46
|
-
:
|
45
|
+
id: SecureRandom.uuid,
|
46
|
+
startedAt: start_time,
|
47
|
+
paused?: false,
|
48
|
+
events: {
|
49
|
+
handled: 0,
|
50
|
+
unhandled: 0
|
47
51
|
}
|
48
52
|
}
|
49
53
|
SessionTracker.set_current_session(new_session)
|
@@ -52,6 +56,47 @@ module Bugsnag
|
|
52
56
|
|
53
57
|
alias_method :create_session, :start_session
|
54
58
|
|
59
|
+
##
|
60
|
+
# Stop any events being attributed to the current session until it is
|
61
|
+
# resumed or a new session is started
|
62
|
+
#
|
63
|
+
# @see resume_session
|
64
|
+
#
|
65
|
+
# @return [void]
|
66
|
+
def pause_session
|
67
|
+
current_session = SessionTracker.get_current_session
|
68
|
+
|
69
|
+
return unless current_session
|
70
|
+
|
71
|
+
current_session[:paused?] = true
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Resume the current session if it was previously paused. If there is no
|
76
|
+
# current session, a new session will be started
|
77
|
+
#
|
78
|
+
# @see pause_session
|
79
|
+
#
|
80
|
+
# @return [Boolean] true if a paused session was resumed
|
81
|
+
def resume_session
|
82
|
+
current_session = SessionTracker.get_current_session
|
83
|
+
|
84
|
+
if current_session
|
85
|
+
# if the session is paused then resume it, otherwise we don't need to
|
86
|
+
# do anything
|
87
|
+
if current_session[:paused?]
|
88
|
+
current_session[:paused?] = false
|
89
|
+
|
90
|
+
return true
|
91
|
+
end
|
92
|
+
else
|
93
|
+
# if there's no current session, start a new one
|
94
|
+
start_session
|
95
|
+
end
|
96
|
+
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
55
100
|
##
|
56
101
|
# Delivers the current session_counts lists to the session endpoint.
|
57
102
|
def send_sessions
|
@@ -83,7 +128,7 @@ module Bugsnag
|
|
83
128
|
end
|
84
129
|
end
|
85
130
|
end
|
86
|
-
@delivery_thread = Concurrent::TimerTask.execute(execution_interval:
|
131
|
+
@delivery_thread = Concurrent::TimerTask.execute(execution_interval: 10) do
|
87
132
|
if @session_counts.size > 0
|
88
133
|
send_sessions
|
89
134
|
end
|
@@ -106,11 +151,6 @@ module Bugsnag
|
|
106
151
|
return
|
107
152
|
end
|
108
153
|
|
109
|
-
if !Bugsnag.configuration.should_notify_release_stage?
|
110
|
-
Bugsnag.configuration.debug("Not delivering sessions due to notify_release_stages :#{Bugsnag.configuration.notify_release_stages.inspect}")
|
111
|
-
return
|
112
|
-
end
|
113
|
-
|
114
154
|
body = {
|
115
155
|
:notifier => {
|
116
156
|
:name => Bugsnag::Report::NOTIFIER_NAME,
|
data/lib/bugsnag/stacktrace.rb
CHANGED
@@ -43,12 +43,14 @@ module Bugsnag
|
|
43
43
|
if defined?(configuration.project_root) && configuration.project_root.to_s != ''
|
44
44
|
trace_hash[:inProject] = true if file.start_with?(configuration.project_root.to_s)
|
45
45
|
file.sub!(/#{configuration.project_root}\//, "")
|
46
|
-
trace_hash.delete(:inProject) if
|
46
|
+
trace_hash.delete(:inProject) if vendor_path?(configuration, file)
|
47
47
|
end
|
48
48
|
|
49
49
|
# Strip common gem path prefixes
|
50
50
|
if defined?(Gem)
|
51
|
-
|
51
|
+
Gem.path.each do |path|
|
52
|
+
file.sub!("#{path}/", "")
|
53
|
+
end
|
52
54
|
end
|
53
55
|
|
54
56
|
trace_hash[:file] = file
|
@@ -67,5 +69,14 @@ module Bugsnag
|
|
67
69
|
|
68
70
|
processed_backtrace
|
69
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
|
70
81
|
end
|
71
82
|
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
|