bugsnag 5.5.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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