airbrake 3.1.2 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG +56 -0
  2. data/Gemfile +0 -9
  3. data/README.md +71 -3
  4. data/SUPPORTED_RAILS_VERSIONS +15 -0
  5. data/TESTING.md +8 -0
  6. data/airbrake.gemspec +10 -5
  7. data/features/rack.feature +4 -0
  8. data/features/rails.feature +66 -5
  9. data/features/step_definitions/rails_application_steps.rb +52 -7
  10. data/features/support/airbrake_shim.rb.template +5 -0
  11. data/generators/airbrake/lib/rake_commands.rb +1 -1
  12. data/generators/airbrake/templates/airbrake_tasks.rake +1 -1
  13. data/lib/airbrake.rb +14 -10
  14. data/lib/airbrake/backtrace.rb +8 -0
  15. data/lib/airbrake/capistrano.rb +0 -1
  16. data/lib/airbrake/configuration.rb +31 -4
  17. data/lib/airbrake/notice.rb +51 -9
  18. data/lib/airbrake/rack.rb +0 -1
  19. data/lib/airbrake/rails/controller_methods.rb +18 -7
  20. data/lib/airbrake/rails/javascript_notifier.rb +0 -1
  21. data/lib/airbrake/rails/middleware/exceptions_catcher.rb +10 -6
  22. data/lib/airbrake/rails3_tasks.rb +1 -2
  23. data/lib/airbrake/rake_handler.rb +3 -4
  24. data/lib/airbrake/sender.rb +35 -23
  25. data/lib/airbrake/shared_tasks.rb +2 -1
  26. data/lib/airbrake/version.rb +1 -1
  27. data/test/{airbrake_2_2.xsd → airbrake_2_3.xsd} +11 -1
  28. data/test/airbrake_tasks_test.rb +3 -3
  29. data/test/backtrace_test.rb +2 -2
  30. data/test/capistrano_test.rb +6 -6
  31. data/test/catcher_test.rb +2 -2
  32. data/test/configuration_test.rb +17 -2
  33. data/test/helper.rb +14 -14
  34. data/test/javascript_notifier_test.rb +1 -2
  35. data/test/logger_test.rb +8 -2
  36. data/test/notice_test.rb +98 -76
  37. data/test/notifier_test.rb +34 -4
  38. data/test/rack_test.rb +1 -1
  39. data/test/rails_initializer_test.rb +1 -1
  40. data/test/recursion_test.rb +1 -1
  41. data/test/sender_test.rb +22 -21
  42. data/test/user_informer_test.rb +1 -1
  43. metadata +176 -31
@@ -1,11 +1,16 @@
1
1
  require 'sham_rack'
2
2
 
3
+ Airbrake.configuration.logger = Logger.new STDOUT if defined?(Airbrake)
4
+
3
5
  ShamRack.at("api.airbrake.io") do |env|
4
6
  response = <<-end_xml
5
7
  <notice>
6
8
  <id>b6817316-9c45-ed26-45eb-780dbb86aadb</id>
7
9
  <url>http://airbrake.io/locate/b6817316-9c45-ed26-45eb-780dbb86aadb</url>
8
10
  </notice>
11
+
12
+ Request:
13
+ #{env["rack.input"].read}
9
14
  end_xml
10
15
  ["200 OK", { "Content-type" => "text/xml" }, [response]]
11
16
  end
@@ -2,7 +2,7 @@ Rails::Generator::Commands::Create.class_eval do
2
2
  def rake(cmd, opts = {})
3
3
  logger.rake "rake #{cmd}"
4
4
  unless system("rake #{cmd}")
5
- logger.rake "#{cmd} failed. Rolling back"
5
+ logger.rake "#{cmd} failed. Rolling back"
6
6
  command(:destroy).invoke!
7
7
  end
8
8
  end
@@ -1,7 +1,7 @@
1
1
  # Don't load anything when running the gems:* tasks.
2
2
  # Otherwise, airbrake will be considered a framework gem.
3
3
  # https://thoughtbot.lighthouseapp.com/projects/14221/tickets/629
4
- unless ARGV.any? {|a| a =~ /^gems/}
4
+ unless ARGV.any? {|a| a =~ /^gems/}
5
5
 
6
6
  Dir[File.join(Rails.root, 'vendor', 'gems', 'airbrake-*')].each do |vendored_notifier|
7
7
  $: << File.join(vendored_notifier, 'lib')
@@ -1,13 +1,8 @@
1
+ require "girl_friday"
1
2
  require 'net/http'
2
3
  require 'net/https'
3
4
  require 'rubygems'
4
- begin
5
- require 'active_support'
6
- require 'active_support/core_ext'
7
- rescue LoadError
8
- require 'activesupport'
9
- require 'activesupport/core_ext'
10
- end
5
+ require 'active_support/core_ext/object/blank'
11
6
  require 'airbrake/version'
12
7
  require 'airbrake/configuration'
13
8
  require 'airbrake/notice'
@@ -19,7 +14,7 @@ require 'airbrake/user_informer'
19
14
  require 'airbrake/railtie' if defined?(Rails::Railtie)
20
15
 
21
16
  module Airbrake
22
- API_VERSION = "2.2"
17
+ API_VERSION = "2.3"
23
18
  LOG_PREFIX = "** [Airbrake] "
24
19
 
25
20
  HEADERS = {
@@ -51,6 +46,11 @@ module Airbrake
51
46
  write_verbose_log("Response from Airbrake: \n#{response}")
52
47
  end
53
48
 
49
+ # Prints out the details about the notice that wasn't sent to server
50
+ def report_notice(notice)
51
+ write_verbose_log("Notice details: \n#{notice}")
52
+ end
53
+
54
54
  # Returns the Ruby version, Rails version, and current Rails environment
55
55
  def environment_info
56
56
  info = "[Ruby: #{RUBY_VERSION}]"
@@ -60,7 +60,7 @@ module Airbrake
60
60
 
61
61
  # Writes out the given message to the #logger
62
62
  def write_verbose_log(message)
63
- logger.info LOG_PREFIX + message if logger
63
+ logger.debug LOG_PREFIX + message if logger
64
64
  end
65
65
 
66
66
  # Look for the Rails logger currently defined
@@ -131,7 +131,11 @@ module Airbrake
131
131
 
132
132
  def send_notice(notice)
133
133
  if configuration.public?
134
- sender.send_to_airbrake(notice.to_xml)
134
+ if configuration.async?
135
+ configuration.async.call(notice)
136
+ else
137
+ sender.send_to_airbrake(notice)
138
+ end
135
139
  end
136
140
  end
137
141
 
@@ -77,6 +77,14 @@ module Airbrake
77
77
  "<Backtrace: " + lines.collect { |line| line.inspect }.join(", ") + ">"
78
78
  end
79
79
 
80
+ def to_s
81
+ content = []
82
+ lines.each do |line|
83
+ content << line
84
+ end
85
+ content.join("\n")
86
+ end
87
+
80
88
  def ==(other)
81
89
  if other.respond_to?(:lines)
82
90
  lines == other.lines
@@ -41,4 +41,3 @@ end
41
41
  if Capistrano::Configuration.instance
42
42
  Airbrake::Capistrano.load_into(Capistrano::Configuration.instance)
43
43
  end
44
-
@@ -12,7 +12,7 @@ module Airbrake
12
12
 
13
13
  # The API key for your project, found on the project edit form.
14
14
  attr_accessor :api_key
15
-
15
+
16
16
  # If you're using the Javascript notifier and would want to separate
17
17
  # Javascript notifications into another Airbrake project, specify
18
18
  # its APi key here.
@@ -28,7 +28,7 @@ module Airbrake
28
28
 
29
29
  # +true+ for https connections, +false+ for http connections.
30
30
  attr_accessor :secure
31
-
31
+
32
32
  # +true+ to use whatever CAs OpenSSL has installed on your system. +false+ to use the ca-bundle.crt file included in Airbrake itself (reccomended and default)
33
33
  attr_accessor :use_system_ssl_cert_chain
34
34
 
@@ -100,11 +100,12 @@ module Airbrake
100
100
  # (boolean or nil; set to nil to catch exceptions when rake isn't running from a terminal; default is nil)
101
101
  attr_accessor :rescue_rake_exceptions
102
102
 
103
+
103
104
  DEFAULT_PARAMS_FILTERS = %w(password password_confirmation).freeze
104
105
 
105
106
  DEFAULT_BACKTRACE_FILTERS = [
106
107
  lambda { |line|
107
- if defined?(Airbrake.configuration.project_root) && Airbrake.configuration.project_root.to_s != ''
108
+ if defined?(Airbrake.configuration.project_root) && Airbrake.configuration.project_root.to_s != ''
108
109
  line.sub(/#{Airbrake.configuration.project_root}/, "[PROJECT_ROOT]")
109
110
  else
110
111
  line
@@ -239,6 +240,26 @@ module Airbrake
239
240
  end
240
241
  end
241
242
 
243
+ # Should Airbrake send notifications asynchronously
244
+ # (boolean, nil or callable; default is nil).
245
+ # Can be used as callable-setter when block provided.
246
+ def async(&block)
247
+ if block_given?
248
+ @async = block
249
+ end
250
+ @async
251
+ end
252
+ alias_method :async?, :async
253
+
254
+ def async=(value)
255
+ # use default GirlFriday-async for 'true' value
256
+ @async = if value == true
257
+ default_async_processor
258
+ else
259
+ value
260
+ end
261
+ end
262
+
242
263
  def js_api_key
243
264
  @js_api_key || self.api_key
244
265
  end
@@ -276,6 +297,12 @@ module Airbrake
276
297
  end
277
298
  end
278
299
 
300
+ # Async notice delivery defaults to girl friday
301
+ def default_async_processor
302
+ queue = GirlFriday::WorkQueue.new(nil, :size => 3) do |notice|
303
+ Airbrake.sender.send_to_airbrake(notice)
304
+ end
305
+ lambda {|notice| queue << notice}
306
+ end
279
307
  end
280
-
281
308
  end
@@ -4,6 +4,21 @@ require 'socket'
4
4
  module Airbrake
5
5
  class Notice
6
6
 
7
+ class << self
8
+ def attr_reader_with_tracking(*names)
9
+ attr_readers.concat(names)
10
+ attr_reader_without_tracking(*names)
11
+ end
12
+
13
+ alias_method :attr_reader_without_tracking, :attr_reader
14
+ alias_method :attr_reader, :attr_reader_with_tracking
15
+
16
+
17
+ def attr_readers
18
+ @attr_readers ||= []
19
+ end
20
+ end
21
+
7
22
  # The exception that caused this notice, if any
8
23
  attr_reader :exception
9
24
 
@@ -69,6 +84,23 @@ module Airbrake
69
84
  # The host name where this error occurred (if any)
70
85
  attr_reader :hostname
71
86
 
87
+ # Details about the user who experienced the error
88
+ attr_reader :user
89
+
90
+ private
91
+
92
+ # Private writers for all the attributes
93
+ attr_writer :exception, :api_key, :backtrace, :error_class, :error_message,
94
+ :backtrace_filters, :parameters, :params_filters,
95
+ :environment_filters, :session_data, :project_root, :url, :ignore,
96
+ :ignore_by_filters, :notifier_name, :notifier_url, :notifier_version,
97
+ :component, :action, :cgi_data, :environment_name, :hostname, :user
98
+
99
+ # Arguments given in the initializer
100
+ attr_accessor :args
101
+
102
+ public
103
+
72
104
  def initialize(args)
73
105
  self.args = args
74
106
  self.exception = args[:exception]
@@ -96,10 +128,11 @@ module Airbrake
96
128
  self.backtrace = Backtrace.parse(exception_attribute(:backtrace, caller), :filters => self.backtrace_filters)
97
129
  self.error_class = exception_attribute(:error_class) {|exception| exception.class.name }
98
130
  self.error_message = exception_attribute(:error_message, 'Notification') do |exception|
99
- "#{exception.class.name}: #{exception.message}"
131
+ "#{exception.class.name}: #{args[:error_message] || exception.message}"
100
132
  end
101
133
 
102
134
  self.hostname = local_hostname
135
+ self.user = args[:user]
103
136
 
104
137
  also_use_rack_params_filters
105
138
  find_session_data
@@ -161,6 +194,14 @@ module Airbrake
161
194
  env.tag!("environment-name", environment_name)
162
195
  env.tag!("hostname", hostname)
163
196
  end
197
+ unless user.blank?
198
+ notice.tag!("current-user") do |u|
199
+ u.tag!("id",user[:id])
200
+ u.tag!("name",user[:name])
201
+ u.tag!("email",user[:email])
202
+ u.tag!("username",user[:username])
203
+ end
204
+ end
164
205
  end
165
206
  xml.to_s
166
207
  end
@@ -188,14 +229,6 @@ module Airbrake
188
229
 
189
230
  private
190
231
 
191
- attr_writer :exception, :api_key, :backtrace, :error_class, :error_message,
192
- :backtrace_filters, :parameters, :params_filters,
193
- :environment_filters, :session_data, :project_root, :url, :ignore,
194
- :ignore_by_filters, :notifier_name, :notifier_url, :notifier_version,
195
- :component, :action, :cgi_data, :environment_name, :hostname
196
-
197
- # Arguments given in the initializer
198
- attr_accessor :args
199
232
 
200
233
  # Gets a property named +attribute+ of an exception, either from an actual
201
234
  # exception or a hash.
@@ -317,6 +350,8 @@ module Airbrake
317
350
 
318
351
  def rack_env(method)
319
352
  rack_request.send(method) if rack_request
353
+ rescue
354
+ {:message => "failed to call #{method} on Rack::Request -- #{$!.message}"}
320
355
  end
321
356
 
322
357
  def rack_request
@@ -344,5 +379,12 @@ module Airbrake
344
379
  Socket.gethostname
345
380
  end
346
381
 
382
+ def to_s
383
+ content = []
384
+ self.class.attr_readers.each do |attr|
385
+ content << " #{attr}: #{send(attr)}"
386
+ end
387
+ content.join("\n")
388
+ end
347
389
  end
348
390
  end
@@ -22,7 +22,6 @@ module Airbrake
22
22
  class Rack
23
23
  def initialize(app)
24
24
  @app = app
25
- Airbrake.configuration.logger ||= Logger.new STDOUT
26
25
  end
27
26
 
28
27
  def ignored_user_agent?(env)
@@ -3,12 +3,15 @@ module Airbrake
3
3
  module ControllerMethods
4
4
 
5
5
  def airbrake_request_data
6
- { :parameters => airbrake_filter_if_filtering(params.to_hash),
6
+ {
7
+ :parameters => airbrake_filter_if_filtering(params.to_hash),
7
8
  :session_data => airbrake_filter_if_filtering(airbrake_session_data),
8
9
  :controller => params[:controller],
9
10
  :action => params[:action],
10
11
  :url => airbrake_request_url,
11
- :cgi_data => airbrake_filter_if_filtering(request.env) }
12
+ :cgi_data => airbrake_filter_if_filtering(request.env),
13
+ :user => airbrake_current_user
14
+ }
12
15
  end
13
16
 
14
17
  private
@@ -20,7 +23,7 @@ module Airbrake
20
23
  Airbrake.notify(hash_or_exception, airbrake_request_data)
21
24
  end
22
25
  end
23
-
26
+
24
27
  def airbrake_local_request?
25
28
  if defined?(::Rails.application.config)
26
29
  ::Rails.application.config.consider_all_requests_local || (request.local? && (!request.env["HTTP_X_FORWARDED_FOR"]))
@@ -39,15 +42,15 @@ module Airbrake
39
42
  def airbrake_filter_if_filtering(hash)
40
43
  return hash if ! hash.is_a?(Hash)
41
44
 
42
-
45
+
43
46
  if respond_to?(:filter_parameters) # Rails 2
44
- filter_parameters(hash)
47
+ filter_parameters(hash)
45
48
  elsif defined?(ActionDispatch::Http::ParameterFilter) # Rails 3
46
49
  ActionDispatch::Http::ParameterFilter.new(::Rails.application.config.filter_parameters).filter(hash)
47
50
  else
48
51
  hash
49
52
  end rescue hash
50
-
53
+
51
54
  end
52
55
 
53
56
  def airbrake_session_data
@@ -68,7 +71,15 @@ module Airbrake
68
71
  url << request.fullpath
69
72
  url
70
73
  end
74
+
75
+ def airbrake_current_user
76
+ user = current_user || current_member
77
+ user.attributes.select do |k, v|
78
+ /^(id|name|username|email)$/ === k unless v.blank?
79
+ end
80
+ rescue NoMethodError, NameError
81
+ {}
82
+ end
71
83
  end
72
84
  end
73
85
  end
74
-
@@ -42,7 +42,6 @@ module Airbrake
42
42
  end
43
43
 
44
44
  end
45
-
46
45
  end
47
46
  end
48
47
  end
@@ -14,12 +14,16 @@ module Airbrake
14
14
  end
15
15
 
16
16
  def render_exception_with_airbrake(env,exception)
17
- controller = env['action_controller.instance']
18
- env['airbrake.error_id'] = Airbrake.
19
- notify_or_ignore(exception,
20
- controller.try(:airbrake_request_data) || {:rack_env => env}) unless skip_user_agent?(env)
21
- if defined?(controller.rescue_action_in_public_without_airbrake)
22
- controller.rescue_action_in_public_without_airbrake(exception)
17
+ begin
18
+ controller = env['action_controller.instance']
19
+ env['airbrake.error_id'] = Airbrake.
20
+ notify_or_ignore(exception,
21
+ controller.try(:airbrake_request_data) || {:rack_env => env}) unless skip_user_agent?(env)
22
+ if defined?(controller.rescue_action_in_public_without_airbrake)
23
+ controller.rescue_action_in_public_without_airbrake(exception)
24
+ end
25
+ rescue
26
+ # do nothing
23
27
  end
24
28
  render_exception_without_airbrake(env,exception)
25
29
  end
@@ -7,7 +7,7 @@ namespace :airbrake do
7
7
  Rails.logger = defined?(ActiveSupport::TaggedLogging) ?
8
8
  ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) :
9
9
  Logger.new(STDOUT)
10
-
10
+
11
11
  Rails.logger.level = Logger::DEBUG
12
12
  Airbrake.configure(true) do |config|
13
13
  config.logger = Rails.logger
@@ -82,4 +82,3 @@ namespace :airbrake do
82
82
  Rails.application.call(env)
83
83
  end
84
84
  end
85
-
@@ -10,7 +10,7 @@ module Airbrake::RakeHandler
10
10
 
11
11
  def display_error_message_with_airbrake(ex)
12
12
  if Airbrake.sender && Airbrake.configuration &&
13
- (Airbrake.configuration.rescue_rake_exceptions ||
13
+ (Airbrake.configuration.rescue_rake_exceptions ||
14
14
  (Airbrake.configuration.rescue_rake_exceptions===nil && !self.tty_output?))
15
15
 
16
16
  Airbrake.notify_or_ignore(ex, :component => reconstruct_command_line, :cgi_data => ENV)
@@ -22,11 +22,11 @@ module Airbrake::RakeHandler
22
22
  def reconstruct_command_line
23
23
  "rake #{ARGV.join( ' ' )}"
24
24
  end
25
-
25
+
26
26
  # This module brings Rake 0.8.7 error handling to 0.9.0 standards
27
27
  module Rake087Methods
28
28
  # Method taken from Rake 0.9.0 source
29
- #
29
+ #
30
30
  # Provide standard exception handling for the given block.
31
31
  def standard_exception_handling
32
32
  begin
@@ -63,4 +63,3 @@ Rake.application.instance_eval do
63
63
  include Airbrake::RakeHandler
64
64
  end
65
65
  end
66
-
@@ -1,7 +1,7 @@
1
1
  module Airbrake
2
2
  # Sends out the notice to Airbrake
3
3
  class Sender
4
-
4
+
5
5
  NOTICES_URI = '/notifier_api/v2/notices/'.freeze
6
6
  HTTP_ERRORS = [Timeout::Error,
7
7
  Errno::EINVAL,
@@ -14,15 +14,15 @@ module Airbrake
14
14
 
15
15
  def initialize(options = {})
16
16
  [ :proxy_host,
17
- :proxy_port,
18
- :proxy_user,
19
- :proxy_pass,
17
+ :proxy_port,
18
+ :proxy_user,
19
+ :proxy_pass,
20
20
  :protocol,
21
- :host,
22
- :port,
23
- :secure,
24
- :use_system_ssl_cert_chain,
25
- :http_open_timeout,
21
+ :host,
22
+ :port,
23
+ :secure,
24
+ :use_system_ssl_cert_chain,
25
+ :http_open_timeout,
26
26
  :http_read_timeout
27
27
  ].each do |option|
28
28
  instance_variable_set("@#{option}", options[option])
@@ -31,22 +31,29 @@ module Airbrake
31
31
 
32
32
  # Sends the notice data off to Airbrake for processing.
33
33
  #
34
- # @param [String] data The XML notice to be sent off
35
- def send_to_airbrake(data)
34
+ # @param [Notice] notice The notice to be sent off
35
+ def send_to_airbrake(notice)
36
+ data = notice.to_xml
36
37
  http = setup_http_connection
37
38
 
38
39
  response = begin
39
40
  http.post(url.path, data, HEADERS)
40
41
  rescue *HTTP_ERRORS => e
41
- log :error, "Unable to contact the Airbrake server. HTTP Error=#{e}"
42
+ log :level => :error,
43
+ :message => "Unable to contact the Airbrake server. HTTP Error=#{e}"
42
44
  nil
43
45
  end
44
46
 
45
47
  case response
46
48
  when Net::HTTPSuccess then
47
- log :info, "Success: #{response.class}", response
49
+ log :level => :info,
50
+ :message => "Success: #{response.class}",
51
+ :response => response
48
52
  else
49
- log :error, "Failure: #{response.class}", response
53
+ log :level => :error,
54
+ :message => "Failure: #{response.class}",
55
+ :response => response,
56
+ :notice => notice
50
57
  end
51
58
 
52
59
  if response && response.respond_to?(:body)
@@ -54,7 +61,10 @@ module Airbrake
54
61
  error_id[1] if error_id
55
62
  end
56
63
  rescue => e
57
- log :error, "[Airbrake::Sender#send_to_airbrake] Cannot send notification. Error: #{e.class} - #{e.message}\nBacktrace:\n#{e.backtrace.join("\n\t")}"
64
+ log :level => :error,
65
+ :message => "[Airbrake::Sender#send_to_airbrake] Cannot send notification. Error: #{e.class}" +
66
+ " - #{e.message}\nBacktrace:\n#{e.backtrace.join("\n\t")}"
67
+
58
68
  nil
59
69
  end
60
70
 
@@ -72,23 +82,24 @@ module Airbrake
72
82
 
73
83
  alias_method :secure?, :secure
74
84
  alias_method :use_system_ssl_cert_chain?, :use_system_ssl_cert_chain
75
-
85
+
76
86
  private
77
87
 
78
88
  def url
79
89
  URI.parse("#{protocol}://#{host}:#{port}").merge(NOTICES_URI)
80
90
  end
81
91
 
82
- def log(level, message, response = nil)
83
- logger.send level, LOG_PREFIX + message if logger
92
+ def log(opts = {})
93
+ opts[:logger].send opts[:level], LOG_PREFIX + opts[:message] if opts[:logger]
84
94
  Airbrake.report_environment_info
85
- Airbrake.report_response_body(response.body) if response && response.respond_to?(:body)
95
+ Airbrake.report_response_body(opts[:response].body) if opts[:response] && opts[:response].respond_to?(:body)
96
+ Airbrake.report_notice(opts[:notice]) if opts[:notice]
86
97
  end
87
98
 
88
99
  def logger
89
100
  Airbrake.logger
90
101
  end
91
-
102
+
92
103
  def setup_http_connection
93
104
  http =
94
105
  Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass).
@@ -105,12 +116,13 @@ module Airbrake
105
116
  else
106
117
  http.use_ssl = false
107
118
  end
108
-
119
+
109
120
  http
110
121
  rescue => e
111
- log :error, "[Airbrake::Sender#setup_http_connection] Failure initializing the HTTP connection.\nError: #{e.class} - #{e.message}\nBacktrace:\n#{e.backtrace.join("\n\t")}"
122
+ log :level => :error,
123
+ :message => "[Airbrake::Sender#setup_http_connection] Failure initializing the HTTP connection.\n" +
124
+ "Error: #{e.class} - #{e.message}\nBacktrace:\n#{e.backtrace.join("\n\t")}"
112
125
  raise e
113
126
  end
114
-
115
127
  end
116
128
  end