bugsnag 5.5.0 → 6.0.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +33 -11
  3. data/CHANGELOG.md +23 -0
  4. data/Gemfile +20 -0
  5. data/Rakefile +19 -12
  6. data/UPGRADING.md +58 -0
  7. data/VERSION +1 -1
  8. data/bugsnag.gemspec +0 -9
  9. data/lib/bugsnag.rb +64 -86
  10. data/lib/bugsnag/configuration.rb +42 -26
  11. data/lib/bugsnag/delivery.rb +9 -0
  12. data/lib/bugsnag/delivery/synchronous.rb +3 -5
  13. data/lib/bugsnag/delivery/thread_queue.rb +4 -2
  14. data/lib/bugsnag/helpers.rb +5 -16
  15. data/lib/bugsnag/{delayed_job.rb → integrations/delayed_job.rb} +15 -7
  16. data/lib/bugsnag/{mailman.rb → integrations/mailman.rb} +13 -11
  17. data/lib/bugsnag/{que.rb → integrations/que.rb} +11 -11
  18. data/lib/bugsnag/{rack.rb → integrations/rack.rb} +22 -23
  19. data/lib/bugsnag/{rails → integrations/rails}/active_record_rescue.rb +9 -7
  20. data/lib/bugsnag/{rails → integrations/rails}/controller_methods.rb +0 -9
  21. data/lib/bugsnag/{railtie.rb → integrations/railtie.rb} +24 -21
  22. data/lib/bugsnag/{rake.rb → integrations/rake.rb} +12 -9
  23. data/lib/bugsnag/{resque.rb → integrations/resque.rb} +12 -9
  24. data/lib/bugsnag/integrations/shoryuken.rb +49 -0
  25. data/lib/bugsnag/{sidekiq.rb → integrations/sidekiq.rb} +13 -9
  26. data/lib/bugsnag/middleware/callbacks.rb +4 -8
  27. data/lib/bugsnag/middleware/classify_error.rb +7 -13
  28. data/lib/bugsnag/middleware/clearance_user.rb +8 -8
  29. data/lib/bugsnag/middleware/exception_meta_data.rb +34 -0
  30. data/lib/bugsnag/middleware/ignore_error_class.rb +21 -0
  31. data/lib/bugsnag/middleware/mailman.rb +4 -4
  32. data/lib/bugsnag/middleware/rack_request.rb +13 -13
  33. data/lib/bugsnag/middleware/rails3_request.rb +10 -10
  34. data/lib/bugsnag/middleware/rake.rb +5 -5
  35. data/lib/bugsnag/middleware/sidekiq.rb +5 -5
  36. data/lib/bugsnag/middleware/suggestion_data.rb +30 -0
  37. data/lib/bugsnag/middleware/warden_user.rb +6 -6
  38. data/lib/bugsnag/middleware_stack.rb +5 -5
  39. data/lib/bugsnag/report.rb +187 -0
  40. data/lib/bugsnag/stacktrace.rb +113 -0
  41. data/lib/bugsnag/tasks/bugsnag.rake +2 -70
  42. data/spec/cleaner_spec.rb +6 -0
  43. data/spec/configuration_spec.rb +1 -1
  44. data/spec/fixtures/middleware/internal_info_setter.rb +3 -3
  45. data/spec/fixtures/middleware/public_info_setter.rb +3 -3
  46. data/spec/fixtures/tasks/Rakefile +2 -3
  47. data/spec/integration_spec.rb +5 -20
  48. data/spec/{delayed_job_spec.rb → integrations/delayed_job_spec.rb} +0 -0
  49. data/spec/integrations/sidekiq_spec.rb +34 -0
  50. data/spec/middleware_spec.rb +108 -35
  51. data/spec/rack_spec.rb +1 -1
  52. data/spec/{notification_spec.rb → report_spec.rb} +226 -209
  53. data/spec/spec_helper.rb +18 -0
  54. data/spec/{code_spec.rb → stacktrace_spec.rb} +1 -1
  55. metadata +23 -139
  56. data/.document +0 -5
  57. data/lib/bugsnag/capistrano.rb +0 -7
  58. data/lib/bugsnag/capistrano2.rb +0 -32
  59. data/lib/bugsnag/delay/resque.rb +0 -21
  60. data/lib/bugsnag/deploy.rb +0 -35
  61. data/lib/bugsnag/middleware/rails2_request.rb +0 -52
  62. data/lib/bugsnag/notification.rb +0 -506
  63. data/lib/bugsnag/rails.rb +0 -70
  64. data/lib/bugsnag/rails/action_controller_rescue.rb +0 -74
  65. data/lib/bugsnag/shoryuken.rb +0 -41
  66. data/lib/bugsnag/tasks/bugsnag.cap +0 -48
  67. data/rails/init.rb +0 -7
@@ -4,20 +4,20 @@ module Bugsnag::Middleware
4
4
  @bugsnag = bugsnag
5
5
  end
6
6
 
7
- def call(notification)
8
- task = notification.request_data[:bugsnag_running_task]
7
+ def call(report)
8
+ task = report.request_data[:bugsnag_running_task]
9
9
 
10
10
  if task
11
- notification.add_tab(:rake_task, {
11
+ report.add_tab(:rake_task, {
12
12
  :name => task.name,
13
13
  :description => task.full_comment,
14
14
  :arguments => task.arg_description
15
15
  })
16
16
 
17
- notification.context ||= task.name
17
+ report.context ||= task.name
18
18
  end
19
19
 
20
- @bugsnag.call(notification)
20
+ @bugsnag.call(report)
21
21
  end
22
22
  end
23
23
  end
@@ -4,13 +4,13 @@ module Bugsnag::Middleware
4
4
  @bugsnag = bugsnag
5
5
  end
6
6
 
7
- def call(notification)
8
- sidekiq = notification.request_data[:sidekiq]
7
+ def call(report)
8
+ sidekiq = report.request_data[:sidekiq]
9
9
  if sidekiq
10
- notification.add_tab(:sidekiq, sidekiq)
11
- notification.context ||= "#{sidekiq[:msg]['wrapped'] || sidekiq[:msg]['class']}@#{sidekiq[:msg]['queue']}"
10
+ report.add_tab(:sidekiq, sidekiq)
11
+ report.context ||= "#{sidekiq[:msg]['wrapped'] || sidekiq[:msg]['class']}@#{sidekiq[:msg]['queue']}"
12
12
  end
13
- @bugsnag.call(notification)
13
+ @bugsnag.call(report)
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,30 @@
1
+ module Bugsnag::Middleware
2
+ class SuggestionData
3
+
4
+ CAPTURE_REGEX = /Did you mean\?([\s\S]+)$/
5
+ DELIMITER = "\n"
6
+
7
+ def initialize(bugsnag)
8
+ @bugsnag = bugsnag
9
+ end
10
+
11
+ def call(report)
12
+ matches = []
13
+ report.raw_exceptions.each do |exception|
14
+ match = CAPTURE_REGEX.match(exception.message)
15
+ next unless match
16
+
17
+ suggestions = match.captures[0].split(DELIMITER)
18
+ matches.concat suggestions.map{ |suggestion| suggestion.strip }
19
+ end
20
+
21
+ if matches.size == 1
22
+ report.add_tab(:error, {:suggestion => matches.first})
23
+ elsif matches.size > 1
24
+ report.add_tab(:error, {:suggestions => matches})
25
+ end
26
+
27
+ @bugsnag.call(report)
28
+ end
29
+ end
30
+ end
@@ -7,9 +7,9 @@ module Bugsnag::Middleware
7
7
  @bugsnag = bugsnag
8
8
  end
9
9
 
10
- def call(notification)
11
- if notification.request_data[:rack_env] && notification.request_data[:rack_env]["warden"]
12
- env = notification.request_data[:rack_env]
10
+ def call(report)
11
+ if report.request_data[:rack_env] && report.request_data[:rack_env]["warden"]
12
+ env = report.request_data[:rack_env]
13
13
  session = env["rack.session"] || {}
14
14
 
15
15
  # Find all warden user scopes
@@ -29,11 +29,11 @@ module Bugsnag::Middleware
29
29
  end
30
30
 
31
31
  # We merge the first warden scope down, so that it is the main "user" for the request
32
- notification.user = user unless user.empty?
32
+ report.user = user unless user.empty?
33
33
  end
34
34
  end
35
35
 
36
- @bugsnag.call(notification)
36
+ @bugsnag.call(report)
37
37
  end
38
38
  end
39
- end
39
+ end
@@ -63,7 +63,7 @@ module Bugsnag
63
63
  end
64
64
 
65
65
  # Runs the middleware stack and calls
66
- def run(notification)
66
+ def run(report)
67
67
  # The final lambda is the termination of the middleware stack. It calls deliver on the notification
68
68
  lambda_has_run = false
69
69
  notify_lambda = lambda do |notif|
@@ -73,19 +73,19 @@ module Bugsnag
73
73
 
74
74
  begin
75
75
  # We reverse them, so we can call "call" on the first middleware
76
- middleware_procs.reverse.inject(notify_lambda) { |n,e| e.call(n) }.call(notification)
76
+ middleware_procs.reverse.inject(notify_lambda) { |n,e| e.call(n) }.call(report)
77
77
  rescue StandardError => e
78
78
  # KLUDGE: Since we don't re-raise middleware exceptions, this breaks rspec
79
79
  raise if e.class.to_s == "RSpec::Expectations::ExpectationNotMetError"
80
80
 
81
81
  # We dont notify, as we dont want to loop forever in the case of really broken middleware, we will
82
82
  # still send this notify
83
- Bugsnag.warn "Bugsnag middleware error: #{e}"
84
- Bugsnag.log "Middleware error stacktrace: #{e.backtrace.inspect}"
83
+ Bugsnag.configuration.warn "Bugsnag middleware error: #{e}"
84
+ Bugsnag.configuration.warn "Middleware error stacktrace: #{e.backtrace.inspect}"
85
85
  end
86
86
 
87
87
  # Ensure that the deliver has been performed, and no middleware has botched it
88
- notify_lambda.call(notification) unless lambda_has_run
88
+ notify_lambda.call(report) unless lambda_has_run
89
89
  end
90
90
 
91
91
  private
@@ -0,0 +1,187 @@
1
+ require "json"
2
+ require "pathname"
3
+ require "bugsnag/stacktrace"
4
+
5
+ module Bugsnag
6
+ class Report
7
+ NOTIFIER_NAME = "Ruby Bugsnag Notifier"
8
+ NOTIFIER_VERSION = Bugsnag::VERSION
9
+ NOTIFIER_URL = "http://www.bugsnag.com"
10
+
11
+ UNHANDLED_EXCEPTION = "unhandledException"
12
+ UNHANDLED_EXCEPTION_MIDDLEWARE = "unhandledExceptionMiddleware"
13
+ ERROR_CLASS = "errorClass"
14
+ HANDLED_EXCEPTION = "handledException"
15
+ USER_SPECIFIED_SEVERITY = "userSpecifiedSeverity"
16
+ USER_CALLBACK_SET_SEVERITY = "userCallbackSetSeverity"
17
+
18
+ MAX_EXCEPTIONS_TO_UNWRAP = 5
19
+
20
+ CURRENT_PAYLOAD_VERSION = "2"
21
+
22
+ attr_accessor :api_key
23
+ attr_accessor :app_type
24
+ attr_accessor :app_version
25
+ attr_accessor :configuration
26
+ attr_accessor :context
27
+ attr_accessor :delivery_method
28
+ attr_accessor :exceptions
29
+ attr_accessor :hostname
30
+ attr_accessor :grouping_hash
31
+ attr_accessor :meta_data
32
+ attr_accessor :raw_exceptions
33
+ attr_accessor :release_stage
34
+ attr_accessor :severity
35
+ attr_accessor :severity_reason
36
+ attr_accessor :user
37
+
38
+ def initialize(exception, passed_configuration, auto_notify=false)
39
+ @should_ignore = false
40
+ @unhandled = auto_notify
41
+
42
+ self.configuration = passed_configuration
43
+
44
+ self.raw_exceptions = generate_raw_exceptions(exception)
45
+ self.exceptions = generate_exception_list
46
+
47
+ self.api_key = configuration.api_key
48
+ self.app_type = configuration.app_type
49
+ self.app_version = configuration.app_version
50
+ self.delivery_method = configuration.delivery_method
51
+ self.hostname = configuration.hostname
52
+ self.meta_data = {}
53
+ self.release_stage = configuration.release_stage
54
+ self.severity = auto_notify ? "error" : "warning"
55
+ self.severity_reason = auto_notify ? {:type => UNHANDLED_EXCEPTION} : {:type => HANDLED_EXCEPTION}
56
+ self.user = {}
57
+ end
58
+
59
+ # Add a new tab to this notification
60
+ def add_tab(name, value)
61
+ return if name.nil?
62
+
63
+ if value.is_a? Hash
64
+ meta_data[name] ||= {}
65
+ meta_data[name].merge! value
66
+ else
67
+ meta_data["custom"] = {} unless meta_data["custom"]
68
+
69
+ meta_data["custom"][name.to_s] = value
70
+ end
71
+ end
72
+
73
+ # Remove a tab from this notification
74
+ def remove_tab(name)
75
+ return if name.nil?
76
+
77
+ meta_data.delete(name)
78
+ end
79
+
80
+ # Build an exception payload
81
+ def as_json
82
+ # Build the payload's exception event
83
+ payload_event = {
84
+ app: {
85
+ version: app_version,
86
+ releaseStage: release_stage,
87
+ type: app_type
88
+ },
89
+ context: context,
90
+ device: {
91
+ hostname: hostname
92
+ },
93
+ exceptions: exceptions,
94
+ groupingHash: grouping_hash,
95
+ payloadVersion: CURRENT_PAYLOAD_VERSION,
96
+ severity: severity,
97
+ severityReason: severity_reason,
98
+ unhandled: @unhandled,
99
+ user: user
100
+ }
101
+
102
+ # cleanup character encodings
103
+ payload_event = Bugsnag::Cleaner.clean_object_encoding(payload_event)
104
+
105
+ # filter out sensitive values in (and cleanup encodings) metaData
106
+ payload_event[:metaData] = Bugsnag::Cleaner.new(configuration.meta_data_filters).clean_object(meta_data)
107
+ payload_event.reject! {|k,v| v.nil? }
108
+
109
+ # return the payload hash
110
+ {
111
+ :apiKey => api_key,
112
+ :notifier => {
113
+ :name => NOTIFIER_NAME,
114
+ :version => NOTIFIER_VERSION,
115
+ :url => NOTIFIER_URL
116
+ },
117
+ :events => [payload_event]
118
+ }
119
+ end
120
+
121
+ def ignore?
122
+ @should_ignore
123
+ end
124
+
125
+ def request_data
126
+ configuration.request_data
127
+ end
128
+
129
+ def ignore!
130
+ @should_ignore = true
131
+ end
132
+
133
+ private
134
+
135
+ def generate_exception_list
136
+ raw_exceptions.map do |exception|
137
+ {
138
+ errorClass: error_class(exception),
139
+ message: exception.message,
140
+ stacktrace: Stacktrace.new(exception.backtrace, configuration).to_a
141
+ }
142
+ end
143
+ end
144
+
145
+ def error_class(exception)
146
+ # The "Class" check is for some strange exceptions like Timeout::Error
147
+ # which throw the error class instead of an instance
148
+ (exception.is_a? Class) ? exception.name : exception.class.name
149
+ end
150
+
151
+ def generate_raw_exceptions(exception)
152
+ exceptions = []
153
+
154
+ ex = exception
155
+ while ex != nil && !exceptions.include?(ex) && exceptions.length < MAX_EXCEPTIONS_TO_UNWRAP
156
+
157
+ unless ex.is_a? Exception
158
+ if ex.respond_to?(:to_exception)
159
+ ex = ex.to_exception
160
+ elsif ex.respond_to?(:exception)
161
+ ex = ex.exception
162
+ end
163
+ end
164
+
165
+ unless ex.is_a?(Exception) || (defined?(Java::JavaLang::Throwable) && ex.is_a?(Java::JavaLang::Throwable))
166
+ configuration.warn("Converting non-Exception to RuntimeError: #{ex.inspect}")
167
+ ex = RuntimeError.new(ex.to_s)
168
+ ex.set_backtrace caller
169
+ end
170
+
171
+ exceptions << ex
172
+
173
+ if ex.respond_to?(:cause) && ex.cause
174
+ ex = ex.cause
175
+ elsif ex.respond_to?(:continued_exception) && ex.continued_exception
176
+ ex = ex.continued_exception
177
+ elsif ex.respond_to?(:original_exception) && ex.original_exception
178
+ ex = ex.original_exception
179
+ else
180
+ ex = nil
181
+ end
182
+ end
183
+
184
+ exceptions
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,113 @@
1
+ module Bugsnag
2
+ class Stacktrace
3
+
4
+ # e.g. "org/jruby/RubyKernel.java:1264:in `catch'"
5
+ BACKTRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$/
6
+
7
+ # e.g. "org.jruby.Ruby.runScript(Ruby.java:807)"
8
+ JAVA_BACKTRACE_REGEX = /^(.*)\((.*)(?::([0-9]+))?\)$/
9
+
10
+ def initialize(backtrace, configuration)
11
+ @configuration = configuration
12
+
13
+ backtrace = caller if !backtrace || backtrace.empty?
14
+ @processed_backtrace = backtrace.map do |trace|
15
+ if trace.match(BACKTRACE_LINE_REGEX)
16
+ file, line_str, method = [$1, $2, $3]
17
+ elsif trace.match(JAVA_BACKTRACE_REGEX)
18
+ method, file, line_str = [$1, $2, $3]
19
+ end
20
+
21
+ # Parse the stacktrace line
22
+
23
+ # Skip stacktrace lines inside lib/bugsnag
24
+ next(nil) if file.nil? || file =~ %r{lib/bugsnag(/|\.rb)}
25
+
26
+ # Expand relative paths
27
+ p = Pathname.new(file)
28
+ if p.relative?
29
+ file = p.realpath.to_s rescue file
30
+ end
31
+
32
+ # Generate the stacktrace line hash
33
+ trace_hash = {}
34
+ trace_hash[:inProject] = true if in_project?(file)
35
+ trace_hash[:lineNumber] = line_str.to_i
36
+
37
+ if configuration.send_code
38
+ trace_hash[:code] = code(file, trace_hash[:lineNumber])
39
+ end
40
+
41
+ # Clean up the file path in the stacktrace
42
+ if defined?(@configuration.project_root) && @configuration.project_root.to_s != ''
43
+ file.sub!(/#{@configuration.project_root}\//, "")
44
+ end
45
+
46
+ # Strip common gem path prefixes
47
+ if defined?(Gem)
48
+ file = Gem.path.inject(file) {|line, path| line.sub(/#{path}\//, "") }
49
+ end
50
+
51
+ trace_hash[:file] = file
52
+
53
+ # Add a method if we have it
54
+ trace_hash[:method] = method if method && (method =~ /^__bind/).nil?
55
+
56
+ if trace_hash[:file] && !trace_hash[:file].empty?
57
+ trace_hash
58
+ else
59
+ nil
60
+ end
61
+ end.compact
62
+ end
63
+
64
+ def to_a
65
+ @processed_backtrace
66
+ end
67
+
68
+ private
69
+
70
+ def in_project?(line)
71
+ @configuration.project_root && line.start_with?(@configuration.project_root.to_s)
72
+ end
73
+
74
+ def code(file, line_number, num_lines = 7)
75
+ code_hash = {}
76
+
77
+ from_line = [line_number - num_lines, 1].max
78
+
79
+ # don't try and open '(irb)' or '-e'
80
+ return unless File.exist?(file)
81
+
82
+ # Populate code hash with line numbers and code lines
83
+ File.open(file) do |f|
84
+ current_line_number = 0
85
+ f.each_line do |line|
86
+ current_line_number += 1
87
+
88
+ next if current_line_number < from_line
89
+
90
+ code_hash[current_line_number] = line[0...200].rstrip
91
+
92
+ break if code_hash.length >= ( num_lines * 1.5 ).ceil
93
+ end
94
+ end
95
+
96
+ while code_hash.length > num_lines
97
+ last_line = code_hash.keys.max
98
+ first_line = code_hash.keys.min
99
+
100
+ if (last_line - line_number) > (line_number - first_line)
101
+ code_hash.delete(last_line)
102
+ else
103
+ code_hash.delete(first_line)
104
+ end
105
+ end
106
+
107
+ code_hash
108
+ rescue
109
+ @configuration.warn("Error fetching code: #{$!.inspect}")
110
+ nil
111
+ end
112
+ end
113
+ end
@@ -1,82 +1,14 @@
1
1
  require "bugsnag"
2
2
 
3
3
  namespace :bugsnag do
4
- desc "Notify Bugsnag of a new deploy."
5
- task :deploy do
6
- api_key = ENV["BUGSNAG_API_KEY"]
7
- release_stage = ENV["BUGSNAG_RELEASE_STAGE"]
8
- app_version = ENV["BUGSNAG_APP_VERSION"]
9
- revision = ENV["BUGSNAG_REVISION"]
10
- repository = ENV["BUGSNAG_REPOSITORY"]
11
- branch = ENV["BUGSNAG_BRANCH"]
12
-
13
- Rake::Task["load"].invoke unless api_key
14
-
15
- Bugsnag::Deploy.notify({
16
- :api_key => api_key,
17
- :release_stage => release_stage,
18
- :app_version => app_version,
19
- :revision => revision,
20
- :repository => repository,
21
- :branch => branch
22
- })
23
- end
24
-
25
4
  desc "Send a test exception to Bugsnag."
26
5
  task :test_exception => :load do
27
6
  begin
28
7
  raise RuntimeError.new("Bugsnag test exception")
29
8
  rescue => e
30
- Bugsnag.notify(e, {:context => "rake#test_exception"})
31
- end
32
- end
33
-
34
- desc "Show the bugsnag middleware stack"
35
- task :middleware => :load do
36
- Bugsnag.configuration.middleware.each {|m| puts m.to_s}
37
- end
38
-
39
- namespace :heroku do
40
- desc "Add a heroku deploy hook to notify Bugsnag of deploys"
41
- task :add_deploy_hook => :load do
42
- # Wrapper to run command safely even in bundler
43
- run_command = lambda { |command|
44
- defined?(Bundler.with_clean_env) ? Bundler.with_clean_env { `#{command}` } : `#{command}`
45
- }
46
-
47
- # Fetch heroku config settings
48
- config_command = "heroku config --shell"
49
- config_command += " --app #{ENV["HEROKU_APP"]}" if ENV["HEROKU_APP"]
50
- heroku_env = run_command.call(config_command).split(/[\n\r]/).each_with_object({}) do |c, obj|
51
- k,v = c.split("=")
52
- obj[k] = (v.nil? || v.strip.empty?) ? nil : v
53
- end
54
-
55
- # Check for Bugsnag API key (required)
56
- api_key = heroku_env["BUGSNAG_API_KEY"] || Bugsnag.configuration.api_key || ENV["BUGSNAG_API_KEY"]
57
- unless api_key
58
- puts "Error: No API key found, have you run 'heroku config:set BUGSNAG_API_KEY=your-api-key'?"
59
- next
9
+ Bugsnag.notify(e) do |report|
10
+ report.context = "rake#test_exception"
60
11
  end
61
-
62
- # Build the request, making use of deploy hook variables
63
- # (https://devcenter.heroku.com/articles/deploy-hooks#customizing-messages)
64
- params = {
65
- :apiKey => api_key,
66
- :branch => "master",
67
- :revision => "{{head_long}}",
68
- :releaseStage => heroku_env["RAILS_ENV"] || ENV["RAILS_ENV"] || "production"
69
- }
70
- repo = `git config --get remote.origin.url`.strip
71
- params[:repository] = repo unless repo.empty?
72
-
73
- # Add the hook
74
- url = "https://notify.bugsnag.com/deploy?" + params.map {|k,v| "#{k}=#{v}"}.join("&")
75
- command = "heroku addons:add deployhooks:http --url=\"#{url}\""
76
- command += " --app #{ENV["HEROKU_APP"]}" if ENV["HEROKU_APP"]
77
-
78
- puts "$ #{command}"
79
- run_command.call(command)
80
12
  end
81
13
  end
82
14
  end