bugsnag 6.21.0 → 6.25.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/CHANGELOG.md +137 -0
  4. data/VERSION +1 -1
  5. data/bugsnag.gemspec +18 -1
  6. data/lib/bugsnag/breadcrumb_type.rb +14 -0
  7. data/lib/bugsnag/breadcrumbs/breadcrumb.rb +34 -1
  8. data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +1 -0
  9. data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +50 -0
  10. data/lib/bugsnag/cleaner.rb +31 -18
  11. data/lib/bugsnag/configuration.rb +240 -22
  12. data/lib/bugsnag/delivery/synchronous.rb +2 -2
  13. data/lib/bugsnag/delivery/thread_queue.rb +2 -2
  14. data/lib/bugsnag/endpoint_configuration.rb +11 -0
  15. data/lib/bugsnag/endpoint_validator.rb +80 -0
  16. data/lib/bugsnag/error.rb +25 -0
  17. data/lib/bugsnag/event.rb +7 -0
  18. data/lib/bugsnag/feature_flag.rb +74 -0
  19. data/lib/bugsnag/integrations/rack.rb +3 -3
  20. data/lib/bugsnag/integrations/rails/active_job.rb +102 -0
  21. data/lib/bugsnag/integrations/railtie.rb +36 -3
  22. data/lib/bugsnag/integrations/resque.rb +17 -3
  23. data/lib/bugsnag/middleware/active_job.rb +18 -0
  24. data/lib/bugsnag/middleware/delayed_job.rb +21 -2
  25. data/lib/bugsnag/middleware/exception_meta_data.rb +2 -0
  26. data/lib/bugsnag/middleware/rack_request.rb +84 -19
  27. data/lib/bugsnag/middleware/rails3_request.rb +2 -2
  28. data/lib/bugsnag/middleware/rake.rb +1 -1
  29. data/lib/bugsnag/middleware/session_data.rb +3 -1
  30. data/lib/bugsnag/middleware/sidekiq.rb +1 -1
  31. data/lib/bugsnag/middleware/suggestion_data.rb +9 -7
  32. data/lib/bugsnag/report.rb +204 -8
  33. data/lib/bugsnag/session_tracker.rb +52 -12
  34. data/lib/bugsnag/stacktrace.rb +13 -2
  35. data/lib/bugsnag/tasks/bugsnag.rake +1 -1
  36. data/lib/bugsnag/utility/duplicator.rb +124 -0
  37. data/lib/bugsnag/utility/feature_data_store.rb +41 -0
  38. data/lib/bugsnag/utility/feature_flag_delegate.rb +89 -0
  39. data/lib/bugsnag/utility/metadata_delegate.rb +102 -0
  40. data/lib/bugsnag.rb +143 -5
  41. metadata +24 -7
@@ -0,0 +1,102 @@
1
+ require 'set'
2
+
3
+ module Bugsnag::Rails
4
+ module ActiveJob
5
+ SEVERITY = 'error'
6
+ SEVERITY_REASON = {
7
+ type: Bugsnag::Report::UNHANDLED_EXCEPTION_MIDDLEWARE,
8
+ attributes: { framework: 'Active Job' }
9
+ }
10
+
11
+ EXISTING_INTEGRATIONS = Set[
12
+ 'ActiveJob::QueueAdapters::DelayedJobAdapter',
13
+ 'ActiveJob::QueueAdapters::QueAdapter',
14
+ 'ActiveJob::QueueAdapters::ResqueAdapter',
15
+ 'ActiveJob::QueueAdapters::ShoryukenAdapter',
16
+ 'ActiveJob::QueueAdapters::SidekiqAdapter'
17
+ ]
18
+
19
+ INLINE_ADAPTER = 'ActiveJob::QueueAdapters::InlineAdapter'
20
+
21
+ # these methods were added after the first Active Job release so
22
+ # may not be present, depending on the Rails version
23
+ MAYBE_MISSING_METHODS = [
24
+ :provider_job_id,
25
+ :priority,
26
+ :executions,
27
+ :enqueued_at,
28
+ :timezone
29
+ ]
30
+
31
+ def self.included(base)
32
+ base.class_eval do
33
+ around_perform do |job, block|
34
+ adapter = _bugsnag_get_adapter_name(job)
35
+
36
+ # if we have an integration for this queue adapter already then we should
37
+ # leave this job alone or we'll end up with duplicate metadata
38
+ next block.call if EXISTING_INTEGRATIONS.include?(adapter)
39
+
40
+ Bugsnag.configuration.detected_app_type = 'active job'
41
+
42
+ begin
43
+ Bugsnag.configuration.set_request_data(:active_job, _bugsnag_extract_metadata(job))
44
+
45
+ block.call
46
+ rescue Exception => e
47
+ Bugsnag.notify(e, true) do |report|
48
+ report.severity = SEVERITY
49
+ report.severity_reason = SEVERITY_REASON
50
+ end
51
+
52
+ # when using the "inline" adapter the job is run immediately, which
53
+ # will result in our Rack integration catching the re-raised error
54
+ # and reporting it a second time if it's run in a web request
55
+ if adapter == INLINE_ADAPTER
56
+ e.instance_eval do
57
+ def skip_bugsnag
58
+ true
59
+ end
60
+ end
61
+ end
62
+
63
+ raise
64
+ ensure
65
+ Bugsnag.configuration.clear_request_data
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def _bugsnag_get_adapter_name(job)
74
+ adapter = job.class.queue_adapter
75
+
76
+ # in Rails 4 queue adapters were references to a class. In Rails 5+
77
+ # they are an instance of that class instead
78
+ return adapter.name if adapter.is_a?(Class)
79
+
80
+ adapter.class.name
81
+ end
82
+
83
+ def _bugsnag_extract_metadata(job)
84
+ metadata = {
85
+ job_id: job.job_id,
86
+ job_name: job.class.name,
87
+ queue: job.queue_name,
88
+ arguments: job.arguments,
89
+ locale: job.locale
90
+ }
91
+
92
+ MAYBE_MISSING_METHODS.each do |method_name|
93
+ next unless job.respond_to?(method_name)
94
+
95
+ metadata[method_name] = job.send(method_name)
96
+ end
97
+
98
+ metadata.compact!
99
+ metadata
100
+ end
101
+ end
102
+ end
@@ -47,6 +47,29 @@ module Bugsnag
47
47
  end
48
48
  end
49
49
 
50
+ ##
51
+ # Do we need to rescue (& notify) in Active Record callbacks?
52
+ #
53
+ # On Rails versions < 4.2, Rails did not raise errors in AR callbacks
54
+ # On Rails version 4.2, a config option was added to control this
55
+ # On Rails version 5.0, the config option was removed and errors in callbacks
56
+ # always bubble up
57
+ #
58
+ # @api private
59
+ def self.rescue_in_active_record_callbacks?
60
+ # Rails 5+ will re-raise errors in callbacks, so we don't need to rescue them
61
+ return false if ::Rails::VERSION::MAJOR > 4
62
+
63
+ # before 4.2, errors were always swallowed, so we need to rescue them
64
+ return true if ::Rails::VERSION::MAJOR < 4
65
+
66
+ # a config option was added in 4.2 to control this, but won't exist in 4.0 & 4.1
67
+ return true unless ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks)
68
+
69
+ # if the config option is false, we need to rescue and notify
70
+ ActiveRecord::Base.raise_in_transactional_callbacks == false
71
+ end
72
+
50
73
  rake_tasks do
51
74
  require "bugsnag/integrations/rake"
52
75
  load "bugsnag/tasks/bugsnag.rake"
@@ -60,7 +83,7 @@ module Bugsnag
60
83
  config.logger = ::Rails.logger
61
84
  config.release_stage ||= ::Rails.env.to_s
62
85
  config.project_root = ::Rails.root.to_s
63
- config.middleware.insert_before Bugsnag::Middleware::Callbacks, Bugsnag::Middleware::Rails3Request
86
+ config.internal_middleware.use(Bugsnag::Middleware::Rails3Request)
64
87
  config.runtime_versions["rails"] = ::Rails::VERSION::STRING
65
88
  end
66
89
 
@@ -70,8 +93,18 @@ module Bugsnag
70
93
  end
71
94
 
72
95
  ActiveSupport.on_load(:active_record) do
73
- require "bugsnag/integrations/rails/active_record_rescue"
74
- include Bugsnag::Rails::ActiveRecordRescue
96
+ if Bugsnag::Railtie.rescue_in_active_record_callbacks?
97
+ require "bugsnag/integrations/rails/active_record_rescue"
98
+ include Bugsnag::Rails::ActiveRecordRescue
99
+ end
100
+ end
101
+
102
+ ActiveSupport.on_load(:active_job) do
103
+ require "bugsnag/middleware/active_job"
104
+ Bugsnag.configuration.internal_middleware.use(Bugsnag::Middleware::ActiveJob)
105
+
106
+ require "bugsnag/integrations/rails/active_job"
107
+ include Bugsnag::Rails::ActiveJob
75
108
  end
76
109
 
77
110
  Bugsnag::Rails::DEFAULT_RAILS_BREADCRUMBS.each { |event| event_subscription(event) }
@@ -44,9 +44,23 @@ 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 = metadata['class']
49
+
50
+ # when using Active Job the payload "class" will always be the Resque
51
+ # "JobWrapper", so we need to unwrap the actual class name
52
+ if class_name == "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper"
53
+ unwrapped_class_name = metadata['args'][0]['job_class'] rescue nil
54
+
55
+ if unwrapped_class_name
56
+ class_name = unwrapped_class_name
57
+ metadata['wrapped'] ||= unwrapped_class_name
58
+ end
59
+ end
60
+
61
+ context = "#{class_name}@#{queue}"
62
+ report.meta_data.merge!({ context: context, payload: metadata })
63
+ report.automatic_context = context
50
64
  end
51
65
  end
52
66
  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
@@ -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
 
@@ -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
@@ -10,23 +10,25 @@ module Bugsnag::Middleware
10
10
  @bugsnag = bugsnag
11
11
  end
12
12
 
13
- def call(report)
13
+ def call(event)
14
14
  matches = []
15
- report.raw_exceptions.each do |exception|
16
- match = CAPTURE_REGEX.match(exception.message)
15
+
16
+ event.errors.each do |error|
17
+ match = CAPTURE_REGEX.match(error.error_message)
18
+
17
19
  next unless match
18
20
 
19
21
  suggestions = match.captures[0].split(DELIMITER)
20
- matches.concat suggestions.map{ |suggestion| suggestion.strip }
22
+ matches.concat(suggestions.map(&:strip))
21
23
  end
22
24
 
23
25
  if matches.size == 1
24
- report.add_tab(:error, {:suggestion => matches.first})
26
+ event.add_metadata(:error, { suggestion: matches.first })
25
27
  elsif matches.size > 1
26
- report.add_tab(:error, {:suggestions => matches})
28
+ event.add_metadata(:error, { suggestions: matches })
27
29
  end
28
30
 
29
- @bugsnag.call(report)
31
+ @bugsnag.call(event)
30
32
  end
31
33
  end
32
34
  end