bugsnag 6.22.1 → 6.24.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,11 @@
1
+ require "json"
2
+
1
3
  module Bugsnag::Middleware
2
4
  ##
3
5
  # Extracts and attaches rack data to an error report
4
6
  class RackRequest
5
7
  SPOOF = "[SPOOF]".freeze
8
+ COOKIE_HEADER = "Cookie".freeze
6
9
 
7
10
  def initialize(bugsnag)
8
11
  @bugsnag = bugsnag
@@ -18,8 +21,8 @@ module Bugsnag::Middleware
18
21
  client_ip = request.ip.to_s rescue SPOOF
19
22
  session = env["rack.session"]
20
23
 
21
- # Set the context
22
- report.context = "#{request.request_method} #{request.path}"
24
+ # Set the automatic context
25
+ report.automatic_context = "#{request.request_method} #{request.path}"
23
26
 
24
27
  # Set a sensible default for user_id
25
28
  report.user["id"] = request.ip
@@ -42,22 +45,6 @@ module Bugsnag::Middleware
42
45
  Bugsnag.configuration.warn "RackRequest - Rescued error while cleaning request.referer: #{stde}"
43
46
  end
44
47
 
45
- headers = {}
46
-
47
- env.each_pair do |key, value|
48
- if key.to_s.start_with?("HTTP_")
49
- header_key = key[5..-1]
50
- elsif ["CONTENT_TYPE", "CONTENT_LENGTH"].include?(key)
51
- header_key = key
52
- else
53
- next
54
- end
55
-
56
- headers[header_key.split("_").map {|s| s.capitalize}.join("-")] = value
57
- end
58
-
59
- headers["Referer"] = referer if headers["Referer"]
60
-
61
48
  # Add a request tab
62
49
  report.add_tab(:request, {
63
50
  :url => url,
@@ -65,9 +52,17 @@ module Bugsnag::Middleware
65
52
  :params => params.to_hash,
66
53
  :referer => referer,
67
54
  :clientIp => client_ip,
68
- :headers => headers
55
+ :headers => format_headers(env, referer)
69
56
  })
70
57
 
58
+ # add the HTTP version if present
59
+ if env["SERVER_PROTOCOL"]
60
+ report.add_metadata(:request, :httpVersion, env["SERVER_PROTOCOL"])
61
+ end
62
+
63
+ add_request_body(report, request, env)
64
+ add_cookies(report, request)
65
+
71
66
  # Add an environment tab
72
67
  if report.configuration.send_environment
73
68
  report.add_tab(:environment, env)
@@ -87,5 +82,75 @@ module Bugsnag::Middleware
87
82
 
88
83
  @bugsnag.call(report)
89
84
  end
85
+
86
+ private
87
+
88
+ def format_headers(env, referer)
89
+ headers = {}
90
+
91
+ env.each_pair do |key, value|
92
+ if key.to_s.start_with?("HTTP_")
93
+ header_key = key[5..-1]
94
+ elsif ["CONTENT_TYPE", "CONTENT_LENGTH"].include?(key)
95
+ header_key = key
96
+ else
97
+ next
98
+ end
99
+
100
+ headers[header_key.split("_").map {|s| s.capitalize}.join("-")] = value
101
+ end
102
+
103
+ headers["Referer"] = referer if headers["Referer"]
104
+
105
+ headers
106
+ end
107
+
108
+ def add_request_body(report, request, env)
109
+ body = parsed_request_body(request, env)
110
+
111
+ # this request may not have a body
112
+ return unless body.is_a?(Hash) && !body.empty?
113
+
114
+ report.add_metadata(:request, :body, body)
115
+ end
116
+
117
+ def parsed_request_body(request, env)
118
+ return request.POST rescue nil if request.form_data?
119
+
120
+ content_type = env["CONTENT_TYPE"]
121
+
122
+ return nil if content_type.nil?
123
+
124
+ if content_type.include?('/json') || content_type.include?('+json')
125
+ begin
126
+ body = request.body
127
+
128
+ return JSON.parse(body.read)
129
+ rescue StandardError
130
+ return nil
131
+ ensure
132
+ # the body must be rewound so other things can read it after we do
133
+ body.rewind
134
+ end
135
+ end
136
+
137
+ nil
138
+ end
139
+
140
+ def add_cookies(report, request)
141
+ return unless record_cookies?
142
+
143
+ cookies = request.cookies rescue nil
144
+
145
+ return unless cookies.is_a?(Hash) && !cookies.empty?
146
+
147
+ report.add_metadata(:request, :cookies, cookies)
148
+ end
149
+
150
+ def record_cookies?
151
+ # only record cookies in the request if none of the filters match "Cookie"
152
+ # the "Cookie" header will be filtered as normal
153
+ !Bugsnag.cleaner.filters_match?(COOKIE_HEADER)
154
+ end
90
155
  end
91
156
  end
@@ -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)
@@ -8,12 +8,14 @@ module Bugsnag::Middleware
8
8
 
9
9
  def call(report)
10
10
  session = Bugsnag::SessionTracker.get_current_session
11
- unless session.nil?
11
+
12
+ if session && !session[:paused?]
12
13
  if report.unhandled
13
14
  session[:events][:unhandled] += 1
14
15
  else
15
16
  session[:events][:handled] += 1
16
17
  end
18
+
17
19
  report.session = session
18
20
  end
19
21
 
@@ -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
@@ -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,31 +103,67 @@ 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
122
+ @initial_unhandled = @unhandled
110
123
 
111
124
  self.configuration = passed_configuration
112
125
 
126
+ @original_error = exception
113
127
  self.raw_exceptions = generate_raw_exceptions(exception)
114
128
  self.exceptions = generate_exception_list
129
+ @errors = generate_error_list
115
130
 
116
131
  self.api_key = configuration.api_key
117
132
  self.app_type = configuration.app_type
118
133
  self.app_version = configuration.app_version
119
134
  self.breadcrumbs = []
135
+ self.context = configuration.context if configuration.context_set?
120
136
  self.delivery_method = configuration.delivery_method
121
137
  self.hostname = configuration.hostname
122
138
  self.runtime_versions = configuration.runtime_versions.dup
123
- self.meta_data = {}
139
+ self.meta_data = Utility::Duplicator.duplicate(configuration.metadata)
124
140
  self.release_stage = configuration.release_stage
125
141
  self.severity = auto_notify ? "error" : "warning"
126
142
  self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
127
143
  self.user = {}
144
+
145
+ @metadata_delegate = Utility::MetadataDelegate.new
146
+ end
147
+
148
+ ##
149
+ # Additional context for this report
150
+ # @!attribute context
151
+ # @return [String, nil]
152
+ def context
153
+ return @context if defined?(@context)
154
+
155
+ @automatic_context
128
156
  end
129
157
 
158
+ attr_writer :context
159
+
160
+ ##
161
+ # Context set automatically by Bugsnag uses this attribute, which prevents
162
+ # it from overwriting the user-supplied context
163
+ # @api private
164
+ # @return [String, nil]
165
+ attr_accessor :automatic_context
166
+
130
167
  ##
131
168
  # Add a new metadata tab to this notification.
132
169
  #
@@ -135,6 +172,8 @@ module Bugsnag
135
172
  # exists, this will be merged with the existing values. If a Hash is not
136
173
  # given, the value will be placed into the 'custom' tab
137
174
  # @return [void]
175
+ #
176
+ # @deprecated Use {#add_metadata} instead
138
177
  def add_tab(name, value)
139
178
  return if name.nil?
140
179
 
@@ -153,6 +192,8 @@ module Bugsnag
153
192
  #
154
193
  # @param name [String]
155
194
  # @return [void]
195
+ #
196
+ # @deprecated Use {#clear_metadata} instead
156
197
  def remove_tab(name)
157
198
  return if name.nil?
158
199
 
@@ -175,7 +216,8 @@ module Bugsnag
175
216
  context: context,
176
217
  device: {
177
218
  hostname: hostname,
178
- runtimeVersions: runtime_versions
219
+ runtimeVersions: runtime_versions,
220
+ time: @created_at
179
221
  },
180
222
  exceptions: exceptions,
181
223
  groupingHash: grouping_hash,
@@ -257,8 +299,119 @@ module Bugsnag
257
299
  end
258
300
  end
259
301
 
302
+ # A Hash containing arbitrary metadata
303
+ # @!attribute metadata
304
+ # @return [Hash]
305
+ def metadata
306
+ @meta_data
307
+ end
308
+
309
+ # @param metadata [Hash]
310
+ # @return [void]
311
+ def metadata=(metadata)
312
+ @meta_data = metadata
313
+ end
314
+
315
+ ##
316
+ # Data from the current HTTP request. May be nil if no data has been recorded
317
+ #
318
+ # @return [Hash, nil]
319
+ def request
320
+ @meta_data[:request]
321
+ end
322
+
323
+ ##
324
+ # Add values to metadata
325
+ #
326
+ # @overload add_metadata(section, data)
327
+ # Merges data into the given section of metadata
328
+ # @param section [String, Symbol]
329
+ # @param data [Hash]
330
+ #
331
+ # @overload add_metadata(section, key, value)
332
+ # Sets key to value in the given section of metadata. If the value is nil
333
+ # the key will be deleted
334
+ # @param section [String, Symbol]
335
+ # @param key [String, Symbol]
336
+ # @param value
337
+ #
338
+ # @return [void]
339
+ def add_metadata(section, key_or_data, *args)
340
+ @metadata_delegate.add_metadata(@meta_data, section, key_or_data, *args)
341
+ end
342
+
343
+ ##
344
+ # Clear values from metadata
345
+ #
346
+ # @overload clear_metadata(section)
347
+ # Clears the given section of metadata
348
+ # @param section [String, Symbol]
349
+ #
350
+ # @overload clear_metadata(section, key)
351
+ # Clears the key in the given section of metadata
352
+ # @param section [String, Symbol]
353
+ # @param key [String, Symbol]
354
+ #
355
+ # @return [void]
356
+ def clear_metadata(section, *args)
357
+ @metadata_delegate.clear_metadata(@meta_data, section, *args)
358
+ end
359
+
360
+ ##
361
+ # Set information about the current user
362
+ #
363
+ # Additional user fields can be added as metadata in a "user" section
364
+ #
365
+ # Setting a field to 'nil' will remove it from the user data
366
+ #
367
+ # @param id [String, nil]
368
+ # @param email [String, nil]
369
+ # @param name [String, nil]
370
+ # @return [void]
371
+ def set_user(id = nil, email = nil, name = nil)
372
+ new_user = { id: id, email: email, name: name }
373
+ new_user.reject! { |key, value| value.nil? }
374
+
375
+ @user = new_user
376
+ end
377
+
378
+ def unhandled=(new_unhandled)
379
+ # fix the handled/unhandled counts in the current session
380
+ update_handled_counts(new_unhandled, @unhandled)
381
+
382
+ @unhandled = new_unhandled
383
+ end
384
+
385
+ ##
386
+ # Returns true if the unhandled flag has been changed from its initial value
387
+ #
388
+ # @api private
389
+ # @return [Boolean]
390
+ def unhandled_overridden?
391
+ @unhandled != @initial_unhandled
392
+ end
393
+
260
394
  private
261
395
 
396
+ def update_handled_counts(is_unhandled, was_unhandled)
397
+ # do nothing if there is no session to update
398
+ return if @session.nil?
399
+
400
+ # increment the counts for the current unhandled value
401
+ if is_unhandled
402
+ @session[:events][:unhandled] += 1
403
+ else
404
+ @session[:events][:handled] += 1
405
+ end
406
+
407
+ # decrement the counts for the previous unhandled value
408
+ if was_unhandled
409
+ @session[:events][:unhandled] -= 1
410
+ else
411
+ @session[:events][:handled] -= 1
412
+ end
413
+ end
414
+
262
415
  def generate_exception_list
263
416
  raw_exceptions.map do |exception|
264
417
  {
@@ -269,6 +422,12 @@ module Bugsnag
269
422
  end
270
423
  end
271
424
 
425
+ def generate_error_list
426
+ exceptions.map do |exception|
427
+ Error.new(exception[:errorClass], exception[:message], exception[:stacktrace])
428
+ end
429
+ end
430
+
272
431
  def error_class(exception)
273
432
  # The "Class" check is for some strange exceptions like Timeout::Error
274
433
  # which throw the error class instead of an instance
@@ -311,4 +470,5 @@ module Bugsnag
311
470
  exceptions
312
471
  end
313
472
  end
473
+ # rubocop:enable Metrics/ClassLength
314
474
  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
- :id => SecureRandom.uuid,
43
- :startedAt => start_time,
44
- :events => {
45
- :handled => 0,
46
- :unhandled => 0
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: 30) do
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,
@@ -43,7 +43,7 @@ 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 file.match(configuration.vendor_path)
46
+ trace_hash.delete(:inProject) if vendor_path?(configuration, file)
47
47
  end
48
48
 
49
49
  # Strip common gem path prefixes
@@ -67,5 +67,14 @@ module Bugsnag
67
67
 
68
68
  processed_backtrace
69
69
  end
70
+
71
+ # @api private
72
+ def self.vendor_path?(configuration, file_path)
73
+ return true if configuration.vendor_path && file_path.match(configuration.vendor_path)
74
+
75
+ configuration.vendor_paths.any? do |vendor_path|
76
+ file_path.start_with?("#{vendor_path.sub(/\/$/, '')}/")
77
+ end
78
+ end
70
79
  end
71
80
  end
@@ -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