airbrake 3.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/.gitignore +18 -0
  2. data/.yardopts +3 -0
  3. data/CHANGELOG +441 -0
  4. data/Gemfile +3 -0
  5. data/INSTALL +25 -0
  6. data/MIT-LICENSE +22 -0
  7. data/README.md +431 -0
  8. data/README_FOR_HEROKU_ADDON.md +93 -0
  9. data/Rakefile +188 -0
  10. data/SUPPORTED_RAILS_VERSIONS +14 -0
  11. data/TESTING.md +26 -0
  12. data/airbrake.gemspec +33 -0
  13. data/features/metal.feature +23 -0
  14. data/features/rack.feature +27 -0
  15. data/features/rails.feature +254 -0
  16. data/features/rails_with_js_notifier.feature +78 -0
  17. data/features/rake.feature +23 -0
  18. data/features/sinatra.feature +33 -0
  19. data/features/step_definitions/airbrake_shim.rb.template +15 -0
  20. data/features/step_definitions/file_steps.rb +10 -0
  21. data/features/step_definitions/metal_steps.rb +23 -0
  22. data/features/step_definitions/rack_steps.rb +20 -0
  23. data/features/step_definitions/rails_application_steps.rb +401 -0
  24. data/features/step_definitions/rake_steps.rb +17 -0
  25. data/features/support/airbrake_shim.rb.template +15 -0
  26. data/features/support/env.rb +18 -0
  27. data/features/support/matchers.rb +35 -0
  28. data/features/support/rails.rb +181 -0
  29. data/features/support/rake/Rakefile +57 -0
  30. data/features/support/terminal.rb +103 -0
  31. data/features/user_informer.feature +63 -0
  32. data/generators/airbrake/airbrake_generator.rb +90 -0
  33. data/generators/airbrake/lib/insert_commands.rb +34 -0
  34. data/generators/airbrake/lib/rake_commands.rb +24 -0
  35. data/generators/airbrake/templates/airbrake_tasks.rake +25 -0
  36. data/generators/airbrake/templates/capistrano_hook.rb +6 -0
  37. data/generators/airbrake/templates/initializer.rb +6 -0
  38. data/install.rb +1 -0
  39. data/lib/airbrake.rb +150 -0
  40. data/lib/airbrake/backtrace.rb +100 -0
  41. data/lib/airbrake/capistrano.rb +21 -0
  42. data/lib/airbrake/configuration.rb +247 -0
  43. data/lib/airbrake/notice.rb +348 -0
  44. data/lib/airbrake/rack.rb +42 -0
  45. data/lib/airbrake/rails.rb +41 -0
  46. data/lib/airbrake/rails/action_controller_catcher.rb +30 -0
  47. data/lib/airbrake/rails/controller_methods.rb +68 -0
  48. data/lib/airbrake/rails/error_lookup.rb +33 -0
  49. data/lib/airbrake/rails/javascript_notifier.rb +42 -0
  50. data/lib/airbrake/rails3_tasks.rb +82 -0
  51. data/lib/airbrake/railtie.rb +33 -0
  52. data/lib/airbrake/rake_handler.rb +65 -0
  53. data/lib/airbrake/sender.rb +83 -0
  54. data/lib/airbrake/shared_tasks.rb +30 -0
  55. data/lib/airbrake/tasks.rb +83 -0
  56. data/lib/airbrake/user_informer.rb +25 -0
  57. data/lib/airbrake/version.rb +3 -0
  58. data/lib/airbrake_tasks.rb +50 -0
  59. data/lib/rails/generators/airbrake/airbrake_generator.rb +96 -0
  60. data/lib/templates/javascript_notifier.erb +13 -0
  61. data/lib/templates/rescue.erb +91 -0
  62. data/rails/init.rb +1 -0
  63. data/script/integration_test.rb +38 -0
  64. data/test/airbrake_2_2.xsd +78 -0
  65. data/test/airbrake_tasks_test.rb +163 -0
  66. data/test/backtrace_test.rb +163 -0
  67. data/test/catcher_test.rb +333 -0
  68. data/test/configuration_test.rb +216 -0
  69. data/test/helper.rb +251 -0
  70. data/test/javascript_notifier_test.rb +52 -0
  71. data/test/logger_test.rb +85 -0
  72. data/test/notice_test.rb +459 -0
  73. data/test/notifier_test.rb +235 -0
  74. data/test/rack_test.rb +58 -0
  75. data/test/rails_initializer_test.rb +36 -0
  76. data/test/recursion_test.rb +10 -0
  77. data/test/sender_test.rb +193 -0
  78. data/test/user_informer_test.rb +29 -0
  79. metadata +365 -0
@@ -0,0 +1,90 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/lib/insert_commands.rb")
2
+ require File.expand_path(File.dirname(__FILE__) + "/lib/rake_commands.rb")
3
+
4
+ class AirbrakeGenerator < Rails::Generator::Base
5
+ def add_options!(opt)
6
+ opt.on('-k', '--api-key=key', String, "Your Airbrake API key") { |v| options[:api_key] = v}
7
+ opt.on('-h', '--heroku', "Use the Heroku addon to provide your Airbrake API key") { |v| options[:heroku] = v}
8
+ opt.on('-a', '--app=myapp', String, "Your Heroku app name (only required if deploying to >1 Heroku app)") { |v| options[:app] = v}
9
+ end
10
+
11
+ def manifest
12
+ if !api_key_configured? && !options[:api_key] && !options[:heroku]
13
+ puts "Must pass --api-key or --heroku or create config/initializers/airbrake.rb"
14
+ exit
15
+ end
16
+ if plugin_is_present?
17
+ puts "You must first remove the airbrake plugin. Please run: script/plugin remove airbrake"
18
+ exit
19
+ end
20
+ record do |m|
21
+ m.directory 'lib/tasks'
22
+ m.file 'airbrake_tasks.rake', 'lib/tasks/airbrake_tasks.rake'
23
+ if ['config/deploy.rb', 'Capfile'].all? { |file| File.exists?(file) }
24
+ m.append_to 'config/deploy.rb', capistrano_hook
25
+ end
26
+ if api_key_expression
27
+ if use_initializer?
28
+ m.template 'initializer.rb', 'config/initializers/airbrake.rb',
29
+ :assigns => {:api_key => api_key_expression}
30
+ else
31
+ m.template 'initializer.rb', 'config/airbrake.rb',
32
+ :assigns => {:api_key => api_key_expression}
33
+ m.append_to 'config/environment.rb', "require 'config/airbrake'"
34
+ end
35
+ end
36
+ determine_api_key if heroku?
37
+ m.rake "airbrake:test --trace", :generate_only => true
38
+ end
39
+ end
40
+
41
+ def api_key_expression
42
+ s = if options[:api_key]
43
+ "'#{options[:api_key]}'"
44
+ elsif options[:heroku]
45
+ "ENV['AIRBRAKE_API_KEY']"
46
+ end
47
+ end
48
+
49
+ def determine_api_key
50
+ puts "Attempting to determine your API Key from Heroku..."
51
+ ENV['AIRBRAKE_API_KEY'] = heroku_api_key
52
+ if ENV['AIRBRAKE_API_KEY'].blank?
53
+ puts "... Failed."
54
+ puts "WARNING: We were unable to detect the Airbrake API Key from your Heroku environment."
55
+ puts "Your Heroku application environment may not be configured correctly."
56
+ exit 1
57
+ else
58
+ puts "... Done."
59
+ puts "Heroku's Airbrake API Key is '#{ENV['AIRBRAKE_API_KEY']}'"
60
+ end
61
+ end
62
+
63
+ def heroku_api_key
64
+ app = options[:app] ? " --app #{options[:app]}" : ''
65
+ `heroku console#{app} 'puts ENV[%{AIRBRAKE_API_KEY}]'`.split("\n").first
66
+ end
67
+
68
+ def heroku?
69
+ options[:heroku] ||
70
+ system("grep AIRBRAKE_API_KEY config/initializers/airbrake.rb") ||
71
+ system("grep AIRBRAKE_API_KEY config/environment.rb")
72
+ end
73
+
74
+ def use_initializer?
75
+ Rails::VERSION::MAJOR > 1
76
+ end
77
+
78
+ def api_key_configured?
79
+ File.exists?('config/initializers/airbrake.rb') ||
80
+ system("grep Airbrake config/environment.rb")
81
+ end
82
+
83
+ def capistrano_hook
84
+ IO.read(source_path('capistrano_hook.rb'))
85
+ end
86
+
87
+ def plugin_is_present?
88
+ File.exists?('vendor/plugins/airbrake')
89
+ end
90
+ end
@@ -0,0 +1,34 @@
1
+ # Mostly pinched from http://github.com/ryanb/nifty-generators/tree/master
2
+
3
+ Rails::Generator::Commands::Base.class_eval do
4
+ def file_contains?(relative_destination, line)
5
+ File.read(destination_path(relative_destination)).include?(line)
6
+ end
7
+ end
8
+
9
+ Rails::Generator::Commands::Create.class_eval do
10
+ def append_to(file, line)
11
+ logger.insert "#{line} appended to #{file}"
12
+ unless options[:pretend] || file_contains?(file, line)
13
+ File.open(file, "a") do |file|
14
+ file.puts
15
+ file.puts line
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Rails::Generator::Commands::Destroy.class_eval do
22
+ def append_to(file, line)
23
+ logger.remove "#{line} removed from #{file}"
24
+ unless options[:pretend]
25
+ gsub_file file, "\n#{line}", ''
26
+ end
27
+ end
28
+ end
29
+
30
+ Rails::Generator::Commands::List.class_eval do
31
+ def append_to(file, line)
32
+ logger.insert "#{line} appended to #{file}"
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ Rails::Generator::Commands::Create.class_eval do
2
+ def rake(cmd, opts = {})
3
+ logger.rake "rake #{cmd}"
4
+ unless system("rake #{cmd}")
5
+ logger.rake "#{cmd} failed. Rolling back"
6
+ command(:destroy).invoke!
7
+ end
8
+ end
9
+ end
10
+
11
+ Rails::Generator::Commands::Destroy.class_eval do
12
+ def rake(cmd, opts = {})
13
+ unless opts[:generate_only]
14
+ logger.rake "rake #{cmd}"
15
+ system "rake #{cmd}"
16
+ end
17
+ end
18
+ end
19
+
20
+ Rails::Generator::Commands::List.class_eval do
21
+ def rake(cmd, opts = {})
22
+ logger.rake "rake #{cmd}"
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # Don't load anything when running the gems:* tasks.
2
+ # Otherwise, airbrake will be considered a framework gem.
3
+ # https://thoughtbot.lighthouseapp.com/projects/14221/tickets/629
4
+ unless ARGV.any? {|a| a =~ /^gems/}
5
+
6
+ Dir[File.join(RAILS_ROOT, 'vendor', 'gems', 'airbrake-*')].each do |vendored_notifier|
7
+ $: << File.join(vendored_notifier, 'lib')
8
+ end
9
+
10
+ begin
11
+ require 'airbrake/tasks'
12
+ rescue LoadError => exception
13
+ namespace :airbrake do
14
+ %w(deploy test log_stdout).each do |task_name|
15
+ desc "Missing dependency for airbrake:#{task_name}"
16
+ task task_name do
17
+ $stderr.puts "Failed to run airbrake:#{task_name} because of missing dependency."
18
+ $stderr.puts "You probably need to run `rake gems:install` to install the airbrake gem"
19
+ abort exception.inspect
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,6 @@
1
+
2
+ Dir[File.join(File.dirname(__FILE__), '..', 'vendor', 'gems', 'airbrake-*')].each do |vendored_notifier|
3
+ $: << File.join(vendored_notifier, 'lib')
4
+ end
5
+
6
+ require 'airbrake/capistrano'
@@ -0,0 +1,6 @@
1
+ <% if Rails::VERSION::MAJOR < 3 && Rails::VERSION::MINOR < 2 -%>
2
+ require 'airbrake/rails'
3
+ <% end -%>
4
+ Airbrake.configure do |config|
5
+ config.api_key = <%= api_key_expression %>
6
+ end
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ puts IO.read(File.join(File.dirname(__FILE__), 'INSTALL'))
data/lib/airbrake.rb ADDED
@@ -0,0 +1,150 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ 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
11
+ require 'airbrake/version'
12
+ require 'airbrake/configuration'
13
+ require 'airbrake/notice'
14
+ require 'airbrake/sender'
15
+ require 'airbrake/backtrace'
16
+ require 'airbrake/rack'
17
+ require 'airbrake/user_informer'
18
+
19
+ require 'airbrake/railtie' if defined?(Rails::Railtie)
20
+
21
+ # Gem for applications to automatically post errors to the Airbrake of their choice.
22
+ module Airbrake
23
+ API_VERSION = "2.0"
24
+ LOG_PREFIX = "** [Airbrake] "
25
+
26
+ HEADERS = {
27
+ 'Content-type' => 'text/xml',
28
+ 'Accept' => 'text/xml, application/xml'
29
+ }
30
+
31
+ class << self
32
+ # The sender object is responsible for delivering formatted data to the Airbrake server.
33
+ # Must respond to #send_to_airbrake. See Airbrake::Sender.
34
+ attr_accessor :sender
35
+
36
+ # A Airbrake configuration object. Must act like a hash and return sensible
37
+ # values for all Airbrake configuration options. See Airbrake::Configuration.
38
+ attr_accessor :configuration
39
+
40
+ # Tell the log that the Notifier is good to go
41
+ def report_ready
42
+ write_verbose_log("Notifier #{VERSION} ready to catch errors")
43
+ end
44
+
45
+ # Prints out the environment info to the log for debugging help
46
+ def report_environment_info
47
+ write_verbose_log("Environment Info: #{environment_info}")
48
+ end
49
+
50
+ # Prints out the response body from Airbrake for debugging help
51
+ def report_response_body(response)
52
+ write_verbose_log("Response from Airbrake: \n#{response}")
53
+ end
54
+
55
+ # Returns the Ruby version, Rails version, and current Rails environment
56
+ def environment_info
57
+ info = "[Ruby: #{RUBY_VERSION}]"
58
+ info << " [#{configuration.framework}]"
59
+ info << " [Env: #{configuration.environment_name}]"
60
+ end
61
+
62
+ # Writes out the given message to the #logger
63
+ def write_verbose_log(message)
64
+ logger.info LOG_PREFIX + message if logger
65
+ end
66
+
67
+ # Look for the Rails logger currently defined
68
+ def logger
69
+ self.configuration.logger
70
+ end
71
+
72
+ # Call this method to modify defaults in your initializers.
73
+ #
74
+ # @example
75
+ # Airbrake.configure do |config|
76
+ # config.api_key = '1234567890abcdef'
77
+ # config.secure = false
78
+ # end
79
+ def configure(silent = false)
80
+ self.configuration ||= Configuration.new
81
+ yield(configuration)
82
+ self.sender = Sender.new(configuration)
83
+ report_ready unless silent
84
+ end
85
+
86
+ # Sends an exception manually using this method, even when you are not in a controller.
87
+ #
88
+ # @param [Exception] exception The exception you want to notify Airbrake about.
89
+ # @param [Hash] opts Data that will be sent to Airbrake.
90
+ #
91
+ # @option opts [String] :api_key The API key for this project. The API key is a unique identifier that Airbrake uses for identification.
92
+ # @option opts [String] :error_message The error returned by the exception (or the message you want to log).
93
+ # @option opts [String] :backtrace A backtrace, usually obtained with +caller+.
94
+ # @option opts [String] :rack_env The Rack environment.
95
+ # @option opts [String] :session The contents of the user's session.
96
+ # @option opts [String] :environment_name The application environment name.
97
+ def notify(exception, opts = {})
98
+ send_notice(build_notice_for(exception, opts))
99
+ end
100
+
101
+ # Sends the notice unless it is one of the default ignored exceptions
102
+ # @see Airbrake.notify
103
+ def notify_or_ignore(exception, opts = {})
104
+ notice = build_notice_for(exception, opts)
105
+ send_notice(notice) unless notice.ignore?
106
+ end
107
+
108
+ def build_lookup_hash_for(exception, options = {})
109
+ notice = build_notice_for(exception, options)
110
+
111
+ result = {}
112
+ result[:action] = notice.action rescue nil
113
+ result[:component] = notice.component rescue nil
114
+ result[:error_class] = notice.error_class if notice.error_class
115
+ result[:environment_name] = 'production'
116
+
117
+ unless notice.backtrace.lines.empty?
118
+ result[:file] = notice.backtrace.lines.first.file
119
+ result[:line_number] = notice.backtrace.lines.first.number
120
+ end
121
+
122
+ result
123
+ end
124
+
125
+ private
126
+
127
+ def send_notice(notice)
128
+ if configuration.public?
129
+ sender.send_to_airbrake(notice.to_xml)
130
+ end
131
+ end
132
+
133
+ def build_notice_for(exception, opts = {})
134
+ exception = unwrap_exception(exception)
135
+ opts = opts.merge(:exception => exception)
136
+ opts = opts.merge(exception.to_hash) if exception.respond_to?(:to_hash)
137
+ Notice.new(configuration.merge(opts))
138
+ end
139
+
140
+ def unwrap_exception(exception)
141
+ if exception.respond_to?(:original_exception)
142
+ exception.original_exception
143
+ elsif exception.respond_to?(:continued_exception)
144
+ exception.continued_exception
145
+ else
146
+ exception
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,100 @@
1
+ module Airbrake
2
+ # Front end to parsing the backtrace for each notice
3
+ class Backtrace
4
+
5
+ # Handles backtrace parsing line by line
6
+ class Line
7
+
8
+ # regexp (optionnally allowing leading X: for windows support)
9
+ INPUT_FORMAT = %r{^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$}.freeze
10
+
11
+ # The file portion of the line (such as app/models/user.rb)
12
+ attr_reader :file
13
+
14
+ # The line number portion of the line
15
+ attr_reader :number
16
+
17
+ # The method of the line (such as index)
18
+ attr_reader :method
19
+
20
+ # Parses a single line of a given backtrace
21
+ # @param [String] unparsed_line The raw line from +caller+ or some backtrace
22
+ # @return [Line] The parsed backtrace line
23
+ def self.parse(unparsed_line)
24
+ _, file, number, method = unparsed_line.match(INPUT_FORMAT).to_a
25
+ new(file, number, method)
26
+ end
27
+
28
+ def initialize(file, number, method)
29
+ self.file = file
30
+ self.number = number
31
+ self.method = method
32
+ end
33
+
34
+ # Reconstructs the line in a readable fashion
35
+ def to_s
36
+ "#{file}:#{number}:in `#{method}'"
37
+ end
38
+
39
+ def ==(other)
40
+ to_s == other.to_s
41
+ end
42
+
43
+ def inspect
44
+ "<Line:#{to_s}>"
45
+ end
46
+
47
+ private
48
+
49
+ attr_writer :file, :number, :method
50
+ end
51
+
52
+ # holder for an Array of Backtrace::Line instances
53
+ attr_reader :lines
54
+
55
+ def self.parse(ruby_backtrace, opts = {})
56
+ ruby_lines = split_multiline_backtrace(ruby_backtrace)
57
+
58
+ filters = opts[:filters] || []
59
+ filtered_lines = ruby_lines.to_a.map do |line|
60
+ filters.inject(line) do |line, proc|
61
+ proc.call(line)
62
+ end
63
+ end.compact
64
+
65
+ lines = filtered_lines.collect do |unparsed_line|
66
+ Line.parse(unparsed_line)
67
+ end
68
+
69
+ instance = new(lines)
70
+ end
71
+
72
+ def initialize(lines)
73
+ self.lines = lines
74
+ end
75
+
76
+ def inspect
77
+ "<Backtrace: " + lines.collect { |line| line.inspect }.join(", ") + ">"
78
+ end
79
+
80
+ def ==(other)
81
+ if other.respond_to?(:lines)
82
+ lines == other.lines
83
+ else
84
+ false
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ attr_writer :lines
91
+
92
+ def self.split_multiline_backtrace(backtrace)
93
+ if backtrace.to_a.size == 1
94
+ backtrace.to_a.first.split(/\n\s*/)
95
+ else
96
+ backtrace
97
+ end
98
+ end
99
+ end
100
+ end