bugsnag 6.19.0 → 6.23.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.
@@ -9,11 +9,44 @@ require "bugsnag/integrations/rails/rails_breadcrumbs"
9
9
 
10
10
  module Bugsnag
11
11
  class Railtie < ::Rails::Railtie
12
-
13
12
  FRAMEWORK_ATTRIBUTES = {
14
13
  :framework => "Rails"
15
14
  }
16
15
 
16
+ ##
17
+ # Subscribes to an ActiveSupport event, leaving a breadcrumb when it triggers
18
+ #
19
+ # @api private
20
+ # @param event [Hash] details of the event to subscribe to
21
+ def event_subscription(event)
22
+ ActiveSupport::Notifications.subscribe(event[:id]) do |*, event_id, data|
23
+ filtered_data = data.slice(*event[:allowed_data])
24
+ filtered_data[:event_name] = event[:id]
25
+ filtered_data[:event_id] = event_id
26
+
27
+ if event[:id] == "sql.active_record"
28
+ if data.key?(:binds)
29
+ binds = data[:binds].each_with_object({}) { |bind, output| output[bind.name] = '?' if defined?(bind.name) }
30
+ filtered_data[:binds] = JSON.dump(binds) unless binds.empty?
31
+ end
32
+
33
+ # Rails < 6.1 included connection_id in the event data, but now
34
+ # includes the connection object instead
35
+ if data.key?(:connection) && !data.key?(:connection_id)
36
+ # the connection ID is the object_id of the connection object
37
+ filtered_data[:connection_id] = data[:connection].object_id
38
+ end
39
+ end
40
+
41
+ Bugsnag.leave_breadcrumb(
42
+ event[:message],
43
+ filtered_data,
44
+ event[:type],
45
+ :auto
46
+ )
47
+ end
48
+ end
49
+
17
50
  rake_tasks do
18
51
  require "bugsnag/integrations/rake"
19
52
  load "bugsnag/tasks/bugsnag.rake"
@@ -27,7 +60,7 @@ module Bugsnag
27
60
  config.logger = ::Rails.logger
28
61
  config.release_stage ||= ::Rails.env.to_s
29
62
  config.project_root = ::Rails.root.to_s
30
- config.middleware.insert_before Bugsnag::Middleware::Callbacks, Bugsnag::Middleware::Rails3Request
63
+ config.internal_middleware.use(Bugsnag::Middleware::Rails3Request)
31
64
  config.runtime_versions["rails"] = ::Rails::VERSION::STRING
32
65
  end
33
66
 
@@ -41,6 +74,14 @@ module Bugsnag
41
74
  include Bugsnag::Rails::ActiveRecordRescue
42
75
  end
43
76
 
77
+ ActiveSupport.on_load(:active_job) do
78
+ require "bugsnag/middleware/active_job"
79
+ Bugsnag.configuration.internal_middleware.use(Bugsnag::Middleware::ActiveJob)
80
+
81
+ require "bugsnag/integrations/rails/active_job"
82
+ include Bugsnag::Rails::ActiveJob
83
+ end
84
+
44
85
  Bugsnag::Rails::DEFAULT_RAILS_BREADCRUMBS.each { |event| event_subscription(event) }
45
86
 
46
87
  # Make sure we don't overwrite the value set by another integration because
@@ -80,28 +121,5 @@ module Bugsnag
80
121
  Bugsnag.configuration.warn("Unable to add Bugsnag::Rack middleware as the middleware stack is frozen")
81
122
  end
82
123
  end
83
-
84
- ##
85
- # Subscribes to an ActiveSupport event, leaving a breadcrumb when it triggers
86
- #
87
- # @api private
88
- # @param event [Hash] details of the event to subscribe to
89
- def event_subscription(event)
90
- ActiveSupport::Notifications.subscribe(event[:id]) do |*, event_id, data|
91
- filtered_data = data.slice(*event[:allowed_data])
92
- filtered_data[:event_name] = event[:id]
93
- filtered_data[:event_id] = event_id
94
- if event[:id] == "sql.active_record" && data.key?(:binds)
95
- binds = data[:binds].each_with_object({}) { |bind, output| output[bind.name] = '?' if defined?(bind.name) }
96
- filtered_data[:binds] = JSON.dump(binds) unless binds.empty?
97
- end
98
- Bugsnag.leave_breadcrumb(
99
- event[:message],
100
- filtered_data,
101
- event[:type],
102
- :auto
103
- )
104
- end
105
- end
106
124
  end
107
125
  end
@@ -44,9 +44,19 @@ module Bugsnag
44
44
  :attributes => FRAMEWORK_ATTRIBUTES
45
45
  }
46
46
 
47
- context = "#{payload['class']}@#{queue}"
48
- report.meta_data.merge!({:context => context, :payload => payload})
49
- report.context = context
47
+ metadata = payload
48
+ class_name = payload['class']
49
+
50
+ # when using Active Job the payload "class" will always be the Resque
51
+ # "JobWrapper", not the actual job class so we need to fix this here
52
+ if metadata['args'] && metadata['args'][0] && metadata['args'][0]['job_class']
53
+ class_name = metadata['args'][0]['job_class']
54
+ metadata['wrapped'] ||= class_name
55
+ end
56
+
57
+ context = "#{class_name}@#{queue}"
58
+ report.meta_data.merge!({ context: context, payload: metadata })
59
+ report.automatic_context = context
50
60
  end
51
61
  end
52
62
  end
@@ -0,0 +1,18 @@
1
+ module Bugsnag::Middleware
2
+ class ActiveJob
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(report)
8
+ data = report.request_data[:active_job]
9
+
10
+ if data
11
+ report.add_tab(:active_job, data)
12
+ report.automatic_context = "#{data[:job_name]}@#{data[:queue]}"
13
+ end
14
+
15
+ @bugsnag.call(report)
16
+ end
17
+ end
18
+ end
@@ -9,6 +9,7 @@ module Bugsnag::Middleware
9
9
  "ActionController::UnknownAction",
10
10
  "ActionController::UnknownFormat",
11
11
  "ActionController::UnknownHttpMethod",
12
+ "ActionDispatch::Http::MimeNegotiation::InvalidType",
12
13
  "ActiveRecord::RecordNotFound",
13
14
  "CGI::Session::CookieStore::TamperedWithCookie",
14
15
  "Mongoid::Errors::DocumentNotFound",
@@ -2,6 +2,11 @@ module Bugsnag::Middleware
2
2
  ##
3
3
  # Attaches delayed_job information to an error report
4
4
  class DelayedJob
5
+ # Active Job's queue adapter sets the "display_name" to this format. This
6
+ # breaks the event context as the ID and arguments are included, which will
7
+ # differ between executions of the same job
8
+ ACTIVE_JOB_DISPLAY_NAME = /^.* \[[0-9a-f-]+\] from DelayedJob\(.*\) with arguments: \[.*\]$/
9
+
5
10
  def initialize(bugsnag)
6
11
  @bugsnag = bugsnag
7
12
  end
@@ -23,8 +28,10 @@ module Bugsnag::Middleware
23
28
  if job.respond_to?(:payload_object)
24
29
  job_data[:active_job] = job.payload_object.job_data if job.payload_object.respond_to?(:job_data)
25
30
  payload_data = construct_job_payload(job.payload_object)
26
- report.context = payload_data[:display_name] if payload_data.include?(:display_name)
27
- report.context ||= payload_data[:class] if payload_data.include?(:class)
31
+
32
+ context = get_context(payload_data, job_data[:active_job])
33
+ report.automatic_context = context unless context.nil?
34
+
28
35
  job_data[:payload] = payload_data
29
36
  end
30
37
 
@@ -70,5 +77,17 @@ module Bugsnag::Middleware
70
77
  end
71
78
  data
72
79
  end
80
+
81
+ private
82
+
83
+ def get_context(payload_data, active_job_data)
84
+ if payload_data.include?(:display_name) && !ACTIVE_JOB_DISPLAY_NAME.match?(payload_data[:display_name])
85
+ payload_data[:display_name]
86
+ elsif active_job_data && active_job_data['job_class'] && active_job_data['queue_name']
87
+ "#{active_job_data['job_class']}@#{active_job_data['queue_name']}"
88
+ elsif payload_data.include?(:class)
89
+ payload_data[:class]
90
+ end
91
+ end
73
92
  end
74
93
  end
@@ -16,6 +16,8 @@ module Bugsnag::Middleware
16
16
 
17
17
  if exception.respond_to?(:bugsnag_context)
18
18
  context = exception.bugsnag_context
19
+ # note: this should set 'context' not 'automatic_context' as it's a
20
+ # user-supplied value
19
21
  report.context = context if context.is_a?(String)
20
22
  end
21
23
 
@@ -18,8 +18,8 @@ module Bugsnag::Middleware
18
18
  client_ip = request.ip.to_s rescue SPOOF
19
19
  session = env["rack.session"]
20
20
 
21
- # Set the context
22
- report.context = "#{request.request_method} #{request.path}"
21
+ # Set the automatic context
22
+ report.automatic_context = "#{request.request_method} #{request.path}"
23
23
 
24
24
  # Set a sensible default for user_id
25
25
  report.user["id"] = request.ip
@@ -15,8 +15,8 @@ module Bugsnag::Middleware
15
15
  client_ip = env["action_dispatch.remote_ip"].to_s rescue SPOOF
16
16
 
17
17
  if params
18
- # Set the context
19
- report.context = "#{params[:controller]}##{params[:action]}"
18
+ # Set the automatic context
19
+ report.automatic_context = "#{params[:controller]}##{params[:action]}"
20
20
 
21
21
  # Augment the request tab
22
22
  report.add_tab(:request, {
@@ -16,7 +16,7 @@ module Bugsnag::Middleware
16
16
  :arguments => task.arg_description
17
17
  })
18
18
 
19
- report.context ||= task.name
19
+ report.automatic_context ||= task.name
20
20
  end
21
21
 
22
22
  @bugsnag.call(report)
@@ -10,7 +10,7 @@ module Bugsnag::Middleware
10
10
  sidekiq = report.request_data[:sidekiq]
11
11
  if sidekiq
12
12
  report.add_tab(:sidekiq, sidekiq)
13
- report.context ||= "#{sidekiq[:msg]['wrapped'] || sidekiq[:msg]['class']}@#{sidekiq[:msg]['queue']}"
13
+ report.automatic_context ||= "#{sidekiq[:msg]['wrapped'] || sidekiq[:msg]['class']}@#{sidekiq[:msg]['queue']}"
14
14
  end
15
15
  @bugsnag.call(report)
16
16
  end
@@ -131,8 +131,8 @@ module Bugsnag
131
131
  #
132
132
  # @return [Array<Proc>]
133
133
  def middleware_procs
134
- # Split the middleware into separate lists of Procs and Classes
135
- procs, classes = @middlewares.partition {|middleware| middleware.is_a?(Proc) }
134
+ # Split the middleware into separate lists of callables (e.g. Proc, Lambda, Method) and Classes
135
+ callables, classes = @middlewares.partition {|middleware| middleware.respond_to?(:call) }
136
136
 
137
137
  # Wrap the classes in a proc that, when called, news up the middleware and
138
138
  # passes the next middleware in the queue
@@ -140,12 +140,12 @@ module Bugsnag
140
140
  proc {|next_middleware| middleware.new(next_middleware) }
141
141
  end
142
142
 
143
- # Wrap the list of procs in a proc that, when called, wraps them in an
143
+ # Wrap the list of callables in a proc that, when called, wraps them in an
144
144
  # 'OnErrorCallbacks' instance that also has a reference to the next middleware
145
- wrapped_procs = proc {|next_middleware| OnErrorCallbacks.new(next_middleware, procs) }
145
+ wrapped_callables = proc {|next_middleware| OnErrorCallbacks.new(next_middleware, callables) }
146
146
 
147
- # Return the combined middleware and wrapped procs
148
- middleware_instances.push(wrapped_procs)
147
+ # Return the combined middleware and wrapped callables
148
+ middleware_instances.push(wrapped_callables)
149
149
  end
150
150
  end
151
151
  end
@@ -1,8 +1,10 @@
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
7
9
  NOTIFIER_NAME = "Ruby Bugsnag Notifier"
8
10
  NOTIFIER_VERSION = Bugsnag::VERSION
@@ -45,16 +47,13 @@ module Bugsnag
45
47
  # @return [Configuration]
46
48
  attr_accessor :configuration
47
49
 
48
- # Additional context for this report
49
- # @return [String, nil]
50
- attr_accessor :context
51
-
52
50
  # The delivery method that will be used for this report
53
51
  # @see Configuration#delivery_method
54
52
  # @return [Symbol]
55
53
  attr_accessor :delivery_method
56
54
 
57
55
  # The list of exceptions in this report
56
+ # @deprecated Use {#errors} instead
58
57
  # @return [Array<Hash>]
59
58
  attr_accessor :exceptions
60
59
 
@@ -72,10 +71,12 @@ module Bugsnag
72
71
  attr_accessor :grouping_hash
73
72
 
74
73
  # Arbitrary metadata attached to this report
74
+ # @deprecated Use {#metadata} instead
75
75
  # @return [Hash]
76
76
  attr_accessor :meta_data
77
77
 
78
78
  # The raw Exception instances for this report
79
+ # @deprecated Use {#original_error} instead
79
80
  # @see #exceptions
80
81
  # @return [Array<Exception>]
81
82
  attr_accessor :raw_exceptions
@@ -102,21 +103,35 @@ module Bugsnag
102
103
  # @return [Hash]
103
104
  attr_accessor :user
104
105
 
106
+ # A list of errors in this report
107
+ # @return [Array<Error>]
108
+ attr_reader :errors
109
+
110
+ # The Exception instance this report was created for
111
+ # @return [Exception]
112
+ attr_reader :original_error
113
+
105
114
  ##
106
115
  # Initializes a new report from an exception.
107
116
  def initialize(exception, passed_configuration, auto_notify=false)
117
+ # store the creation time for use as device.time
118
+ @created_at = Time.now.utc.iso8601(3)
119
+
108
120
  @should_ignore = false
109
121
  @unhandled = auto_notify
110
122
 
111
123
  self.configuration = passed_configuration
112
124
 
125
+ @original_error = exception
113
126
  self.raw_exceptions = generate_raw_exceptions(exception)
114
127
  self.exceptions = generate_exception_list
128
+ @errors = generate_error_list
115
129
 
116
130
  self.api_key = configuration.api_key
117
131
  self.app_type = configuration.app_type
118
132
  self.app_version = configuration.app_version
119
133
  self.breadcrumbs = []
134
+ self.context = configuration.context if configuration.context_set?
120
135
  self.delivery_method = configuration.delivery_method
121
136
  self.hostname = configuration.hostname
122
137
  self.runtime_versions = configuration.runtime_versions.dup
@@ -125,8 +140,29 @@ module Bugsnag
125
140
  self.severity = auto_notify ? "error" : "warning"
126
141
  self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
127
142
  self.user = {}
143
+
144
+ @metadata_delegate = Utility::MetadataDelegate.new
128
145
  end
129
146
 
147
+ ##
148
+ # Additional context for this report
149
+ # @!attribute context
150
+ # @return [String, nil]
151
+ def context
152
+ return @context if defined?(@context)
153
+
154
+ @automatic_context
155
+ end
156
+
157
+ attr_writer :context
158
+
159
+ ##
160
+ # Context set automatically by Bugsnag uses this attribute, which prevents
161
+ # it from overwriting the user-supplied context
162
+ # @api private
163
+ # @return [String, nil]
164
+ attr_accessor :automatic_context
165
+
130
166
  ##
131
167
  # Add a new metadata tab to this notification.
132
168
  #
@@ -135,6 +171,8 @@ module Bugsnag
135
171
  # exists, this will be merged with the existing values. If a Hash is not
136
172
  # given, the value will be placed into the 'custom' tab
137
173
  # @return [void]
174
+ #
175
+ # @deprecated Use {#add_metadata} instead
138
176
  def add_tab(name, value)
139
177
  return if name.nil?
140
178
 
@@ -153,6 +191,8 @@ module Bugsnag
153
191
  #
154
192
  # @param name [String]
155
193
  # @return [void]
194
+ #
195
+ # @deprecated Use {#clear_metadata} instead
156
196
  def remove_tab(name)
157
197
  return if name.nil?
158
198
 
@@ -175,7 +215,8 @@ module Bugsnag
175
215
  context: context,
176
216
  device: {
177
217
  hostname: hostname,
178
- runtimeVersions: runtime_versions
218
+ runtimeVersions: runtime_versions,
219
+ time: @created_at
179
220
  },
180
221
  exceptions: exceptions,
181
222
  groupingHash: grouping_hash,
@@ -257,6 +298,82 @@ module Bugsnag
257
298
  end
258
299
  end
259
300
 
301
+ # A Hash containing arbitrary metadata
302
+ # @!attribute metadata
303
+ # @return [Hash]
304
+ def metadata
305
+ @meta_data
306
+ end
307
+
308
+ # @param metadata [Hash]
309
+ # @return [void]
310
+ def metadata=(metadata)
311
+ @meta_data = metadata
312
+ end
313
+
314
+ ##
315
+ # Data from the current HTTP request. May be nil if no data has been recorded
316
+ #
317
+ # @return [Hash, nil]
318
+ def request
319
+ @meta_data[:request]
320
+ end
321
+
322
+ ##
323
+ # Add values to metadata
324
+ #
325
+ # @overload add_metadata(section, data)
326
+ # Merges data into the given section of metadata
327
+ # @param section [String, Symbol]
328
+ # @param data [Hash]
329
+ #
330
+ # @overload add_metadata(section, key, value)
331
+ # Sets key to value in the given section of metadata. If the value is nil
332
+ # the key will be deleted
333
+ # @param section [String, Symbol]
334
+ # @param key [String, Symbol]
335
+ # @param value
336
+ #
337
+ # @return [void]
338
+ def add_metadata(section, key_or_data, *args)
339
+ @metadata_delegate.add_metadata(@meta_data, section, key_or_data, *args)
340
+ end
341
+
342
+ ##
343
+ # Clear values from metadata
344
+ #
345
+ # @overload clear_metadata(section)
346
+ # Clears the given section of metadata
347
+ # @param section [String, Symbol]
348
+ #
349
+ # @overload clear_metadata(section, key)
350
+ # Clears the key in the given section of metadata
351
+ # @param section [String, Symbol]
352
+ # @param key [String, Symbol]
353
+ #
354
+ # @return [void]
355
+ def clear_metadata(section, *args)
356
+ @metadata_delegate.clear_metadata(@meta_data, section, *args)
357
+ end
358
+
359
+ ##
360
+ # Set information about the current user
361
+ #
362
+ # Additional user fields can be added as metadata in a "user" section
363
+ #
364
+ # Setting a field to 'nil' will remove it from the user data
365
+ #
366
+ # @param id [String, nil]
367
+ # @param email [String, nil]
368
+ # @param name [String, nil]
369
+ # @return [void]
370
+ def set_user(id = nil, email = nil, name = nil)
371
+ new_user = { id: id, email: email, name: name }
372
+ new_user.reject! { |key, value| value.nil? }
373
+
374
+ @user = new_user
375
+ end
376
+
260
377
  private
261
378
 
262
379
  def generate_exception_list
@@ -269,6 +386,12 @@ module Bugsnag
269
386
  end
270
387
  end
271
388
 
389
+ def generate_error_list
390
+ exceptions.map do |exception|
391
+ Error.new(exception[:errorClass], exception[:message], exception[:stacktrace])
392
+ end
393
+ end
394
+
272
395
  def error_class(exception)
273
396
  # The "Class" check is for some strange exceptions like Timeout::Error
274
397
  # which throw the error class instead of an instance
@@ -311,4 +434,5 @@ module Bugsnag
311
434
  exceptions
312
435
  end
313
436
  end
437
+ # rubocop:enable Metrics/ClassLength
314
438
  end
@@ -35,7 +35,8 @@ module Bugsnag
35
35
  #
36
36
  # This allows Bugsnag to track error rates for a release.
37
37
  def start_session
38
- return unless Bugsnag.configuration.enable_sessions
38
+ return unless Bugsnag.configuration.enable_sessions && Bugsnag.configuration.should_notify_release_stage?
39
+
39
40
  start_delivery_thread
40
41
  start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00')
41
42
  new_session = {
@@ -83,7 +84,7 @@ module Bugsnag
83
84
  end
84
85
  end
85
86
  end
86
- @delivery_thread = Concurrent::TimerTask.execute(execution_interval: 30) do
87
+ @delivery_thread = Concurrent::TimerTask.execute(execution_interval: 10) do
87
88
  if @session_counts.size > 0
88
89
  send_sessions
89
90
  end
@@ -106,11 +107,6 @@ module Bugsnag
106
107
  return
107
108
  end
108
109
 
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
110
  body = {
115
111
  :notifier => {
116
112
  :name => Bugsnag::Report::NOTIFIER_NAME,
@@ -7,7 +7,7 @@ namespace :bugsnag do
7
7
  raise RuntimeError.new("Bugsnag test exception")
8
8
  rescue => e
9
9
  Bugsnag.notify(e) do |report|
10
- report.context = "rake#test_exception"
10
+ report.automatic_context = "rake#test_exception"
11
11
  end
12
12
  end
13
13
  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