bugsnag 6.19.0 → 6.26.0
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 +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
|