├── rails └── init.rb ├── .yardopts ├── Gemfile ├── install.rb ├── lib ├── airbrake │ ├── version.rb │ ├── user_informer.rb │ ├── railtie.rb │ ├── rack.rb │ ├── shared_tasks.rb │ ├── rails │ │ ├── error_lookup.rb │ │ ├── action_controller_catcher.rb │ │ ├── javascript_notifier.rb │ │ └── controller_methods.rb │ ├── capistrano.rb │ ├── rails.rb │ ├── rake_handler.rb │ ├── rails3_tasks.rb │ ├── backtrace.rb │ ├── sender.rb │ ├── tasks.rb │ ├── configuration.rb │ └── notice.rb ├── templates │ ├── javascript_notifier.erb │ └── rescue.erb ├── airbrake_tasks.rb ├── rails │ └── generators │ │ └── airbrake │ │ └── airbrake_generator.rb └── airbrake.rb ├── SUPPORTED_RAILS_VERSIONS ├── .gitignore ├── generators └── airbrake │ ├── templates │ ├── initializer.rb │ ├── capistrano_hook.rb │ └── airbrake_tasks.rake │ ├── lib │ ├── rake_commands.rb │ └── insert_commands.rb │ └── airbrake_generator.rb ├── README.md ├── test ├── recursion_test.rb ├── user_informer_test.rb ├── capistrano_test.rb ├── rails_initializer_test.rb ├── javascript_notifier_test.rb ├── rack_test.rb ├── logger_test.rb ├── airbrake_2_2.xsd ├── backtrace_test.rb ├── airbrake_tasks_test.rb ├── sender_test.rb ├── helper.rb ├── notifier_test.rb ├── configuration_test.rb └── catcher_test.rb ├── features ├── step_definitions │ ├── file_steps.rb │ ├── airbrake_shim.rb.template │ ├── rake_steps.rb │ ├── rack_steps.rb │ ├── metal_steps.rb │ └── rails_application_steps.rb ├── support │ ├── airbrake_shim.rb.template │ ├── env.rb │ ├── matchers.rb │ ├── rake │ │ └── Rakefile │ ├── terminal.rb │ └── rails.rb ├── rake.feature ├── rack.feature ├── metal.feature ├── sinatra.feature ├── user_informer.feature ├── rails_with_js_notifier.feature └── rails.feature ├── TESTING.md ├── INSTALL ├── script └── integration_test.rb ├── MIT-LICENSE ├── airbrake.gemspec ├── README_FOR_HEROKU_ADDON.md └── Rakefile /rails/init.rb: -------------------------------------------------------------------------------- 1 | require 'airbrake/rails' 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | - 2 | TESTING.rdoc 3 | MIT-LICENSE 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- 1 | puts IO.read(File.join(File.dirname(__FILE__), 'INSTALL')) 2 | -------------------------------------------------------------------------------- /lib/airbrake/version.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | VERSION = "3.0.4" 3 | end 4 | -------------------------------------------------------------------------------- /SUPPORTED_RAILS_VERSIONS: -------------------------------------------------------------------------------- 1 | 2.3.2 2 | 2.3.4 3 | 2.3.5 4 | 2.3.8 5 | 2.3.9 6 | 2.3.10 7 | 3.0.0 8 | 3.0.1 9 | 3.0.2 10 | 3.0.3 11 | 3.0.4 12 | 3.0.5 13 | 3.0.6 14 | 3.0.7 15 | 3.0.8 16 | 3.0.9 17 | 3.0.10 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log/* 2 | tmp 3 | db/schema.rb 4 | db/*.sqlite3 5 | public/system 6 | *.swp 7 | *.DS_Store 8 | coverage/* 9 | rdoc/ 10 | tags 11 | .yardoc 12 | doc 13 | pkg 14 | 15 | Gemfile.lock 16 | .bundle 17 | 18 | *.rbc 19 | -------------------------------------------------------------------------------- /generators/airbrake/templates/initializer.rb: -------------------------------------------------------------------------------- 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 7 | -------------------------------------------------------------------------------- /generators/airbrake/templates/capistrano_hook.rb: -------------------------------------------------------------------------------- 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' 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NOTE 2 | ==== 3 | 4 | This repository is a fork. Users should fork, make pull requests and create issues from [https://github.com/airbrake/airbrake](https://github.com/airbrake/airbrake) instead. 5 | 6 | Additionally, the `hoptoad_notifier` gem is depracated. Users should use the new [airbrake gem](http://rubygems.org/gems/airbrake) instead. 7 | -------------------------------------------------------------------------------- /test/recursion_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class RecursionTest < Test::Unit::TestCase 4 | should "not allow infinite recursion" do 5 | hash = {:a => :a} 6 | hash[:hash] = hash 7 | notice = Airbrake::Notice.new(:parameters => hash) 8 | assert_equal "[possible infinite recursion halted]", notice.parameters[:hash] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /features/step_definitions/file_steps.rb: -------------------------------------------------------------------------------- 1 | Then /^"([^\"]*)" should not contain text of "([^\"]*)"$/ do |target_file, contents_file| 2 | notifier_root = File.join(File.dirname(__FILE__), '..', '..') 3 | full_path_contents = File.join(notifier_root, contents_file) 4 | contents_text = File.open(full_path_contents).read 5 | 6 | full_path_target = File.join(rails_root, target_file) 7 | target_text = File.open(full_path_target).read 8 | 9 | target_text.should_not include(contents_text) 10 | end 11 | -------------------------------------------------------------------------------- /features/support/airbrake_shim.rb.template: -------------------------------------------------------------------------------- 1 | require 'sham_rack' 2 | 3 | ShamRack.at("airbrakeapp.com") do |env| 4 | xml = env['rack.input'].read 5 | puts "Recieved the following exception:\n#{xml}" 6 | response = <<-end_xml 7 | 8 | 9 | 3799307 10 | http://sample.airbrakeapp.com/errors/3799307/notices/643732254 11 | 643732254 12 | 13 | end_xml 14 | ["200 OK", { "Content-type" => "text/xml" }, response] 15 | end 16 | -------------------------------------------------------------------------------- /features/step_definitions/airbrake_shim.rb.template: -------------------------------------------------------------------------------- 1 | require 'sham_rack' 2 | 3 | ShamRack.at("airbrakeapp.com") do |env| 4 | xml = env['rack.input'].read 5 | puts "Recieved the following exception:\n#{xml}" 6 | response = <<-end_xml 7 | 8 | 9 | 3799307 10 | http://sample.airbrakeapp.com/errors/3799307/notices/643732254 11 | 643732254 12 | 13 | end_xml 14 | ["200 OK", { "Content-type" => "text/xml" }, response] 15 | end 16 | -------------------------------------------------------------------------------- /features/step_definitions/rake_steps.rb: -------------------------------------------------------------------------------- 1 | When /I run rake with (.+)/ do |command| 2 | @rake_command = "rake #{command.gsub(' ','_')}" 3 | @rake_result = `cd features/support/rake && GEM_HOME=#{BUILT_GEM_ROOT} #{@rake_command} 2>&1` 4 | end 5 | 6 | Then /Airbrake should (|not) ?catch the exception/ do |condition| 7 | if condition=='not' 8 | @rake_result.should_not =~ /^airbrake/ 9 | else 10 | @rake_result.should =~ /^airbrake/ 11 | end 12 | end 13 | 14 | Then /Airbrake should send the rake command line as the component name/ do 15 | component = @rake_result.match(/^airbrake (.*)$/)[1] 16 | component.should == @rake_command 17 | end 18 | -------------------------------------------------------------------------------- /generators/airbrake/lib/rake_commands.rb: -------------------------------------------------------------------------------- 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 25 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'nokogiri' 3 | require 'rspec' 4 | 5 | PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze 6 | TEMP_DIR = File.join(PROJECT_ROOT, 'tmp').freeze 7 | LOCAL_RAILS_ROOT = File.join(TEMP_DIR, 'rails_root').freeze 8 | BUILT_GEM_ROOT = File.join(TEMP_DIR, 'built_gems').freeze 9 | LOCAL_GEM_ROOT = File.join(TEMP_DIR, 'local_gems').freeze 10 | RACK_FILE = File.join(TEMP_DIR, 'rack_app.rb').freeze 11 | 12 | Before do 13 | FileUtils.mkdir_p(TEMP_DIR) 14 | FileUtils.rm_rf(BUILT_GEM_ROOT) 15 | FileUtils.rm_rf(LOCAL_RAILS_ROOT) 16 | FileUtils.rm_f(RACK_FILE) 17 | FileUtils.mkdir_p(BUILT_GEM_ROOT) 18 | end 19 | -------------------------------------------------------------------------------- /lib/templates/javascript_notifier.erb: -------------------------------------------------------------------------------- 1 | <%= javascript_tag %Q{ 2 | (function(){ 3 | var notifierJsScheme = (("https:" == document.location.protocol) ? "https://" : "http://"); 4 | document.write(unescape("%3Cscript src='" + notifierJsScheme + "#{host}/javascripts/notifier.js' type='text/javascript'%3E%3C/script%3E")); 5 | })(); 6 | }%> 7 | 8 | <%= javascript_tag %Q{ 9 | window.Airbrake = (typeof(Airbrake) == 'undefined' && typeof(Hoptoad) != 'undefined') ? Hoptoad : Airbrake 10 | Airbrake.setKey('#{api_key}'); 11 | Airbrake.setHost('#{host}'); 12 | Airbrake.setEnvironment('#{environment}'); 13 | Airbrake.setErrorDefaults({ url: "#{escape_javascript url}", component: "#{controller_name}", action: "#{action_name}" }); 14 | } 15 | %> 16 | -------------------------------------------------------------------------------- /lib/airbrake/user_informer.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | class UserInformer 3 | def initialize(app) 4 | @app = app 5 | end 6 | 7 | def replacement(with) 8 | @replacement ||= Airbrake.configuration.user_information.gsub(/\{\{\s*error_id\s*\}\}/, with.to_s) 9 | end 10 | 11 | def call(env) 12 | status, headers, body = @app.call(env) 13 | if env['airbrake.error_id'] && Airbrake.configuration.user_information 14 | new_body = [] 15 | body.each do |chunk| 16 | new_body << chunk.gsub("", replacement(env['airbrake.error_id'])) 17 | end 18 | headers['Content-Length'] = new_body.sum(&:length).to_s 19 | body = new_body 20 | end 21 | [status, headers, body] 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /features/step_definitions/rack_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^the following Rack app:$/ do |definition| 2 | File.open(RACK_FILE, 'w') { |file| file.write(definition) } 3 | end 4 | 5 | When /^I perform a Rack request to "([^\"]*)"$/ do |url| 6 | shim_file = File.join(PROJECT_ROOT, 'features', 'support', 'airbrake_shim.rb.template') 7 | request_file = File.join(TEMP_DIR, 'rack_request.rb') 8 | File.open(request_file, 'w') do |file| 9 | file.puts "require 'rubygems'" 10 | file.puts IO.read(shim_file) 11 | file.puts IO.read(RACK_FILE) 12 | file.puts "env = Rack::MockRequest.env_for(#{url.inspect})" 13 | file.puts "status, headers, body = app.call(env)" 14 | file.puts %{puts "HTTP \#{status}"} 15 | file.puts %{headers.each { |key, value| puts "\#{key}: \#{value}"}} 16 | file.puts "body.each { |part| print part }" 17 | end 18 | @terminal.run("ruby #{request_file}") 19 | end 20 | 21 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | Running the suite 2 | ================= 3 | 4 | Since the notifier must run on many versions of Rails, running its test suite is slightly different than you may be used to. 5 | 6 | First execute the following command: 7 | 8 | rake vendor_test_gems 9 | # NOT: bundle exec rake vendor_test_gems 10 | 11 | This command will download the various versions of Rails that the notifier must be tested against. 12 | 13 | Then, to start the suite, run 14 | 15 | rake 16 | 17 | Note: do NOT use 'bundle exec rake'. 18 | 19 | For Maintainers 20 | ================ 21 | 22 | When developing the Hoptoad Notifier, be sure to use the integration test against an existing project on staging before pushing to master. 23 | 24 | ./script/integration_test.rb 25 | 26 | ./script/integration_test.rb secure 27 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | === Configuration 2 | 3 | You should have something like this in config/initializers/hoptoad.rb. 4 | 5 | HoptoadNotifier.configure do |config| 6 | config.api_key = '1234567890abcdef' 7 | end 8 | 9 | (Please note that this configuration should be in a global configuration, and 10 | is *not* environment-specific. Hoptoad is smart enough to know what errors are 11 | caused by what environments, so your staging errors don't get mixed in with 12 | your production errors.) 13 | 14 | You can test that Hoptoad is working in your production environment by using 15 | this rake task (from RAILS_ROOT): 16 | 17 | rake hoptoad:test 18 | 19 | If everything is configured properly, that task will send a notice to Hoptoad 20 | which will be visible immediately. 21 | 22 | NOTE FOR RAILS 1.2.* USERS: 23 | 24 | You will need to copy the hoptoad_notifier_tasks.rake file into your 25 | RAILS_ROOT/lib/tasks directory in order for the rake hoptoad:test task to work. 26 | -------------------------------------------------------------------------------- /features/rake.feature: -------------------------------------------------------------------------------- 1 | Feature: Use the Gem to catch errors in a Rake application 2 | Background: 3 | Given I have built and installed the "airbrake" gem 4 | 5 | Scenario: Catching exceptions in Rake 6 | When I run rake with airbrake 7 | Then Airbrake should catch the exception 8 | 9 | Scenario: Disabling Rake exception catcher 10 | When I run rake with airbrake disabled 11 | Then Airbrake should not catch the exception 12 | 13 | Scenario: Autodetect, running from terminal 14 | When I run rake with airbrake autodetect from terminal 15 | Then Airbrake should not catch the exception 16 | 17 | Scenario: Autodetect, not running from terminal 18 | When I run rake with airbrake autodetect not from terminal 19 | Then Airbrake should catch the exception 20 | 21 | Scenario: Sendind the correct component name 22 | When I run rake with airbrake 23 | Then Airbrake should send the rake command line as the component name 24 | -------------------------------------------------------------------------------- /generators/airbrake/templates/airbrake_tasks.rake: -------------------------------------------------------------------------------- 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 26 | -------------------------------------------------------------------------------- /features/support/matchers.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_content do |xpath, content| 2 | match do |document| 3 | @elements = document.search(xpath) 4 | 5 | if @elements.empty? 6 | false 7 | else 8 | element_with_content = document.at("#{xpath}[contains(.,'#{content}')]") 9 | 10 | if element_with_content.nil? 11 | @found = @elements.collect { |element| element.content } 12 | 13 | false 14 | else 15 | true 16 | end 17 | end 18 | end 19 | 20 | failure_message_for_should do |document| 21 | if @elements.empty? 22 | "In XML:\n#{document}\nNo element at #{xpath}" 23 | else 24 | "In XML:\n#{document}\nGot content #{@found.inspect} at #{xpath} instead of #{content.inspect}" 25 | end 26 | end 27 | 28 | failure_message_for_should_not do |document| 29 | unless @elements.empty? 30 | "In XML:\n#{document}\nExpcted no content #{content.inspect} at #{xpath}" 31 | end 32 | end 33 | end 34 | 35 | World(RSpec::Matchers) 36 | -------------------------------------------------------------------------------- /features/rack.feature: -------------------------------------------------------------------------------- 1 | Feature: Use the notifier in a plain Rack app 2 | 3 | Background: 4 | Given I have built and installed the "airbrake" gem 5 | 6 | Scenario: Rescue and exception in a Rack app 7 | Given the following Rack app: 8 | """ 9 | require 'rack' 10 | require 'airbrake' 11 | 12 | Airbrake.configure do |config| 13 | config.api_key = 'my_api_key' 14 | end 15 | 16 | app = Rack::Builder.app do 17 | use Airbrake::Rack 18 | run lambda { |env| raise "Rack down" } 19 | end 20 | """ 21 | When I perform a Rack request to "http://example.com:123/test/index?param=value" 22 | Then I should receive the following Airbrake notification: 23 | | error message | RuntimeError: Rack down | 24 | | error class | RuntimeError | 25 | | parameters | param: value | 26 | | url | http://example.com:123/test/index?param=value | 27 | 28 | -------------------------------------------------------------------------------- /features/step_definitions/metal_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I define a Metal endpoint called "([^\"]*)":$/ do |class_name, definition| 2 | FileUtils.mkdir_p(File.join(rails_root, 'app', 'metal')) 3 | file_name = File.join(rails_root, 'app', 'metal', "#{class_name.underscore}.rb") 4 | File.open(file_name, "w") do |file| 5 | file.puts "class #{class_name}" 6 | file.puts definition 7 | file.puts "end" 8 | end 9 | When %{the metal endpoint "#{class_name}" is mounted in the Rails 3 routes.rb} if rails3? 10 | end 11 | 12 | When /^the metal endpoint "([^\"]*)" is mounted in the Rails 3 routes.rb$/ do |class_name| 13 | routesrb = File.join(rails_root, "config", "routes.rb") 14 | routes = IO.readlines(routesrb) 15 | rack_route = "match '/metal(/*other)' => #{class_name}" 16 | routes = routes[0..-2] + [rack_route, routes[-1]] 17 | File.open(routesrb, "w") do |f| 18 | f.puts "require 'app/metal/#{class_name.underscore}'" 19 | routes.each do |route_line| 20 | f.puts route_line 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /features/metal.feature: -------------------------------------------------------------------------------- 1 | Feature: Rescue errors in Rails middleware 2 | 3 | Background: 4 | Given I have built and installed the "airbrake" gem 5 | And I generate a new Rails application 6 | And I configure the Airbrake shim 7 | And I configure my application to require the "airbrake" gem 8 | And I run "script/generate airbrake -k myapikey" 9 | 10 | Scenario: Rescue an exception in the dispatcher 11 | When I define a Metal endpoint called "Exploder": 12 | """ 13 | def self.call(env) 14 | raise "Explode" 15 | end 16 | """ 17 | When I perform a request to "http://example.com:123/metal/index?param=value" 18 | Then I should receive the following Airbrake notification: 19 | | error message | RuntimeError: Explode | 20 | | error class | RuntimeError | 21 | | parameters | param: value | 22 | | url | http://example.com:123/metal/index?param=value | 23 | 24 | -------------------------------------------------------------------------------- /generators/airbrake/lib/insert_commands.rb: -------------------------------------------------------------------------------- 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 35 | -------------------------------------------------------------------------------- /test/user_informer_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class UserInformerTest < Test::Unit::TestCase 4 | should "modify output if there is an airbrake id" do 5 | main_app = lambda do |env| 6 | env['airbrake.error_id'] = 1 7 | [200, {}, [""]] 8 | end 9 | informer_app = Airbrake::UserInformer.new(main_app) 10 | 11 | ShamRack.mount(informer_app, "example.com") 12 | 13 | response = Net::HTTP.get_response(URI.parse("http://example.com/")) 14 | assert_equal "Airbrake Error 1", response.body 15 | assert_equal 16, response["Content-Length"].to_i 16 | end 17 | 18 | should "not modify output if there is no airbrake id" do 19 | main_app = lambda do |env| 20 | [200, {}, [""]] 21 | end 22 | informer_app = Airbrake::UserInformer.new(main_app) 23 | 24 | ShamRack.mount(informer_app, "example.com") 25 | 26 | response = Net::HTTP.get_response(URI.parse("http://example.com/")) 27 | assert_equal "", response.body 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /script/integration_test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'logger' 4 | require 'fileutils' 5 | 6 | RAILS_ENV = "production" 7 | RAILS_ROOT = FileUtils.pwd 8 | RAILS_DEFAULT_LOGGER = Logger.new(STDOUT) 9 | 10 | $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 11 | require 'airbrake' 12 | require 'rails/init' 13 | 14 | fail "Please supply an API Key as the first argument" if ARGV.empty? 15 | 16 | host = ARGV[1] 17 | host ||= "airbrakeapp.com" 18 | 19 | secure = (ARGV[2] == "secure") 20 | 21 | exception = begin 22 | raise "Testing airbrake notifier with secure = #{secure}. If you can see this, it works." 23 | rescue => foo 24 | foo 25 | end 26 | 27 | Airbrake.configure do |config| 28 | config.secure = secure 29 | config.host = host 30 | config.api_key = ARGV.first 31 | end 32 | puts "Configuration:" 33 | Airbrake.configuration.to_hash.each do |key, value| 34 | puts sprintf("%25s: %s", key.to_s, value.inspect.slice(0, 55)) 35 | end 36 | puts "Sending #{secure ? "" : "in"}secure notification to project with key #{ARGV.first}" 37 | Airbrake.notify(exception) 38 | 39 | -------------------------------------------------------------------------------- /test/capistrano_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | require 'capistrano/configuration' 4 | require 'airbrake/capistrano' 5 | 6 | class CapistranoTest < Test::Unit::TestCase 7 | def setup 8 | super 9 | reset_config 10 | 11 | @configuration = Capistrano::Configuration.new 12 | Airbrake::Capistrano.load_into(@configuration) 13 | @configuration.dry_run = true 14 | end 15 | 16 | should "define airbrake:deploy task" do 17 | assert_not_nil @configuration.find_task('airbrake:deploy') 18 | end 19 | 20 | should "log when calling airbrake:deploy task" do 21 | @configuration.set(:current_revision, '084505b1c0e0bcf1526e673bb6ac99fbcb18aecc') 22 | @configuration.set(:repository, 'repository') 23 | io = StringIO.new 24 | logger = Capistrano::Logger.new(:output => io) 25 | logger.level = Capistrano::Logger::MAX_LEVEL 26 | 27 | @configuration.logger = logger 28 | @configuration.find_and_execute_task('airbrake:deploy') 29 | 30 | assert io.string.include?('** Notifying Airbrake of Deploy') 31 | assert io.string.include?('** Airbrake Notification Complete') 32 | end 33 | end -------------------------------------------------------------------------------- /lib/airbrake/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'airbrake' 2 | require 'rails' 3 | 4 | module Airbrake 5 | class Railtie < Rails::Railtie 6 | rake_tasks do 7 | require 'airbrake/rake_handler' 8 | require "airbrake/rails3_tasks" 9 | end 10 | 11 | initializer "airbrake.use_rack_middleware" do |app| 12 | app.config.middleware.use "Airbrake::Rack" 13 | app.config.middleware.insert 0, "Airbrake::UserInformer" 14 | end 15 | 16 | config.after_initialize do 17 | Airbrake.configure(true) do |config| 18 | config.logger ||= Rails.logger 19 | config.environment_name ||= Rails.env 20 | config.project_root ||= Rails.root 21 | config.framework = "Rails: #{::Rails::VERSION::STRING}" 22 | end 23 | 24 | if defined?(::ActionController::Base) 25 | require 'airbrake/rails/javascript_notifier' 26 | require 'airbrake/rails/controller_methods' 27 | 28 | ::ActionController::Base.send(:include, Airbrake::Rails::ControllerMethods) 29 | ::ActionController::Base.send(:include, Airbrake::Rails::JavascriptNotifier) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /features/sinatra.feature: -------------------------------------------------------------------------------- 1 | Feature: Use the notifier in a Sinatra app 2 | 3 | Background: 4 | Given I have built and installed the "airbrake" gem 5 | 6 | Scenario: Rescue an exception in a Sinatra app 7 | Given the following Rack app: 8 | """ 9 | require 'sinatra/base' 10 | require 'airbrake' 11 | 12 | Airbrake.configure do |config| 13 | config.api_key = 'my_api_key' 14 | end 15 | 16 | class FontaneApp < Sinatra::Base 17 | use Airbrake::Rack 18 | enable :raise_errors 19 | 20 | get "/test/index" do 21 | raise "Sinatra has left the building" 22 | end 23 | end 24 | 25 | app = FontaneApp 26 | """ 27 | When I perform a Rack request to "http://example.com:123/test/index?param=value" 28 | Then I should receive the following Airbrake notification: 29 | | error message | RuntimeError: Sinatra has left the building | 30 | | error class | RuntimeError | 31 | | parameters | param: value | 32 | | url | http://example.com:123/test/index?param=value | 33 | 34 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007, Tammer Saleh, Thoughtbot, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/airbrake/rack.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | # Middleware for Rack applications. Any errors raised by the upstream 3 | # application will be delivered to Airbrake and re-raised. 4 | # 5 | # Synopsis: 6 | # 7 | # require 'rack' 8 | # require 'airbrake' 9 | # 10 | # Airbrake.configure do |config| 11 | # config.api_key = 'my_api_key' 12 | # end 13 | # 14 | # app = Rack::Builder.app do 15 | # use Airbrake::Rack 16 | # run lambda { |env| raise "Rack down" } 17 | # end 18 | # 19 | # Use a standard Airbrake.configure call to configure your api key. 20 | class Rack 21 | def initialize(app) 22 | @app = app 23 | end 24 | 25 | def call(env) 26 | begin 27 | response = @app.call(env) 28 | rescue Exception => raised 29 | error_id = Airbrake.notify_or_ignore(raised, :rack_env => env) 30 | env['airbrake.error_id'] = error_id 31 | raise 32 | end 33 | 34 | if env['rack.exception'] 35 | error_id = Airbrake.notify_or_ignore(env['rack.exception'], :rack_env => env) 36 | env['airbrake.error_id'] = error_id 37 | end 38 | 39 | response 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/rails_initializer_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | require 'airbrake/rails' 4 | 5 | class RailsInitializerTest < Test::Unit::TestCase 6 | include DefinesConstants 7 | 8 | should "trigger use of Rails' logger if logger isn't set and Rails' logger exists" do 9 | rails = Module.new do 10 | def self.logger 11 | "RAILS LOGGER" 12 | end 13 | end 14 | define_constant("Rails", rails) 15 | Airbrake::Rails.initialize 16 | assert_equal "RAILS LOGGER", Airbrake.logger 17 | end 18 | 19 | should "trigger use of Rails' default logger if logger isn't set and Rails.logger doesn't exist" do 20 | define_constant("RAILS_DEFAULT_LOGGER", "RAILS DEFAULT LOGGER") 21 | 22 | Airbrake::Rails.initialize 23 | assert_equal "RAILS DEFAULT LOGGER", Airbrake.logger 24 | end 25 | 26 | should "allow overriding of the logger if already assigned" do 27 | define_constant("RAILS_DEFAULT_LOGGER", "RAILS DEFAULT LOGGER") 28 | Airbrake::Rails.initialize 29 | 30 | Airbrake.configure(true) do |config| 31 | config.logger = "OVERRIDDEN LOGGER" 32 | end 33 | 34 | assert_equal "OVERRIDDEN LOGGER", Airbrake.logger 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/airbrake/shared_tasks.rb: -------------------------------------------------------------------------------- 1 | namespace :airbrake do 2 | desc "Notify Airbrake of a new deploy." 3 | task :deploy => :environment do 4 | require 'airbrake_tasks' 5 | AirbrakeTasks.deploy(:rails_env => ENV['TO'], 6 | :scm_revision => ENV['REVISION'], 7 | :scm_repository => ENV['REPO'], 8 | :local_username => ENV['USER'], 9 | :api_key => ENV['API_KEY'], 10 | :dry_run => ENV['DRY_RUN']) 11 | end 12 | 13 | task :log_stdout do 14 | require 'logger' 15 | RAILS_DEFAULT_LOGGER = Logger.new(STDOUT) 16 | end 17 | 18 | namespace :heroku do 19 | desc "Install Heroku deploy notifications addon" 20 | task :add_deploy_notification => [:environment] do 21 | heroku_api_key = `heroku console 'puts ENV[%{HOPTOAD_API_KEY}]' | head -n 1`.strip 22 | heroku_rails_env = `heroku console 'puts RAILS_ENV' | head -n 1`.strip 23 | 24 | command = %Q(heroku addons:add deployhooks:http url="http://airbrakeapp.com/deploys.txt?deploy[rails_env]=#{heroku_rails_env}&api_key=#{heroku_api_key}") 25 | 26 | puts "\nRunning:\n#{command}\n" 27 | puts `#{command}` 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/airbrake/rails/error_lookup.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | module Rails 3 | module ErrorLookup 4 | 5 | # Sets up an alias chain to catch exceptions when Rails does 6 | def self.included(base) #:nodoc: 7 | base.send(:alias_method, :rescue_action_locally_without_airbrake, :rescue_action_locally) 8 | base.send(:alias_method, :rescue_action_locally, :rescue_action_locally_with_airbrake) 9 | end 10 | 11 | private 12 | 13 | def rescue_action_locally_with_airbrake(exception) 14 | result = rescue_action_locally_without_airbrake(exception) 15 | 16 | if Airbrake.configuration.development_lookup 17 | path = File.join(File.dirname(__FILE__), '..', '..', 'templates', 'rescue.erb') 18 | notice = Airbrake.build_lookup_hash_for(exception, airbrake_request_data) 19 | 20 | result << @template.render( 21 | :file => path, 22 | :use_full_path => false, 23 | :locals => { :host => Airbrake.configuration.host, 24 | :api_key => Airbrake.configuration.api_key, 25 | :notice => notice }) 26 | end 27 | 28 | result 29 | end 30 | end 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /lib/airbrake/rails/action_controller_catcher.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | module Rails 3 | module ActionControllerCatcher 4 | 5 | # Sets up an alias chain to catch exceptions when Rails does 6 | def self.included(base) #:nodoc: 7 | base.send(:alias_method, :rescue_action_in_public_without_airbrake, :rescue_action_in_public) 8 | base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_airbrake) 9 | end 10 | 11 | private 12 | 13 | # Overrides the rescue_action method in ActionController::Base, but does not inhibit 14 | # any custom processing that is defined with Rails 2's exception helpers. 15 | def rescue_action_in_public_with_airbrake(exception) 16 | unless airbrake_ignore_user_agent? 17 | error_id = Airbrake.notify_or_ignore(exception, airbrake_request_data) 18 | request.env['airbrake.error_id'] = error_id 19 | end 20 | rescue_action_in_public_without_airbrake(exception) 21 | end 22 | 23 | def airbrake_ignore_user_agent? #:nodoc: 24 | # Rails 1.2.6 doesn't have request.user_agent, so check for it here 25 | user_agent = request.respond_to?(:user_agent) ? request.user_agent : request.env["HTTP_USER_AGENT"] 26 | Airbrake.configuration.ignore_user_agent.flatten.any? { |ua| ua === user_agent } 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/airbrake/rails/javascript_notifier.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | module Rails 3 | module JavascriptNotifier 4 | def self.included(base) #:nodoc: 5 | base.send :helper_method, :airbrake_javascript_notifier 6 | end 7 | 8 | private 9 | 10 | def airbrake_javascript_notifier 11 | return unless Airbrake.configuration.public? 12 | 13 | path = File.join File.dirname(__FILE__), '..', '..', 'templates', 'javascript_notifier.erb' 14 | host = Airbrake.configuration.host.dup 15 | port = Airbrake.configuration.port 16 | host << ":#{port}" unless [80, 443].include?(port) 17 | 18 | options = { 19 | :file => path, 20 | :layout => false, 21 | :use_full_path => false, 22 | :locals => { 23 | :host => host, 24 | :api_key => Airbrake.configuration.api_key, 25 | :environment => Airbrake.configuration.environment_name, 26 | :action_name => action_name, 27 | :controller_name => controller_name, 28 | :url => request.url 29 | } 30 | } 31 | 32 | if @template 33 | @template.render(options) 34 | else 35 | render_to_string(options) 36 | end 37 | 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/javascript_notifier_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | require 'airbrake/rails/javascript_notifier' 3 | require 'ostruct' 4 | 5 | class JavascriptNotifierTest < Test::Unit::TestCase 6 | module FakeRenderer 7 | def javascript_tag(text) 8 | "" 9 | end 10 | def escape_javascript(text) 11 | "ESC#{text}ESC" 12 | end 13 | end 14 | 15 | class FakeController 16 | def self.helper_method(*args) 17 | end 18 | 19 | include Airbrake::Rails::JavascriptNotifier 20 | 21 | def action_name 22 | "action" 23 | end 24 | 25 | def controller_name 26 | "controller" 27 | end 28 | 29 | def request 30 | @request ||= OpenStruct.new 31 | end 32 | 33 | def render_to_string(options) 34 | context = OpenStruct.new(options[:locals]) 35 | context.extend(FakeRenderer) 36 | context.instance_eval do 37 | erb = ERB.new(IO.read(options[:file])) 38 | erb.result(binding) 39 | end 40 | end 41 | end 42 | 43 | should "make sure escape_javacript is called on the request.url" do 44 | Airbrake.configure do 45 | end 46 | controller = FakeController.new 47 | controller.request.url = "bad_javascript" 48 | assert controller.send(:airbrake_javascript_notifier)['"ESCbad_javascriptESC"'] 49 | assert ! controller.send(:airbrake_javascript_notifier)['"bad_javascript"'] 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /lib/airbrake/capistrano.rb: -------------------------------------------------------------------------------- 1 | # Defines deploy:notify_airbrake which will send information about the deploy to Airbrake. 2 | require 'capistrano' 3 | 4 | module Airbrake 5 | module Capistrano 6 | def self.load_into(configuration) 7 | configuration.load do 8 | after "deploy", "airbrake:deploy" 9 | after "deploy:migrations", "airbrake:deploy" 10 | 11 | namespace :airbrake do 12 | desc "Notify Airbrake of the deployment" 13 | task :deploy, :except => { :no_release => true } do 14 | rails_env = fetch(:airbrake_env, fetch(:rails_env, "production")) 15 | local_user = ENV['USER'] || ENV['USERNAME'] 16 | executable = RUBY_PLATFORM.downcase.include?('mswin') ? fetch(:rake, 'rake.bat') : fetch(:rake, 'rake') 17 | notify_command = "#{executable} airbrake:deploy TO=#{rails_env} REVISION=#{current_revision} REPO=#{repository} USER=#{local_user}" 18 | notify_command << " DRY_RUN=true" if dry_run 19 | notify_command << " API_KEY=#{ENV['API_KEY']}" if ENV['API_KEY'] 20 | logger.info "Notifying Airbrake of Deploy (#{notify_command})" 21 | `#{notify_command}` if !configuration.dry_run 22 | logger.info "Airbrake Notification Complete." 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | 30 | if Capistrano::Configuration.instance 31 | Airbrake::Capistrano.load_into(Capistrano::Configuration.instance) 32 | end 33 | -------------------------------------------------------------------------------- /airbrake.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "airbrake/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = %q{airbrake} 7 | s.version = Airbrake::VERSION.dup 8 | s.summary = %q{Send your application errors to our hosted service and reclaim your inbox.} 9 | 10 | s.require_paths = ["lib"] 11 | s.files = `git ls-files`.split("\n") 12 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 13 | 14 | s.add_runtime_dependency("builder") 15 | s.add_runtime_dependency("activesupport") 16 | 17 | s.add_development_dependency("actionpack", "~> 2.3.8") 18 | s.add_development_dependency("activerecord", "~> 2.3.8") 19 | s.add_development_dependency("activesupport", "~> 2.3.8") 20 | s.add_development_dependency("bourne", ">= 1.0") 21 | s.add_development_dependency("cucumber", "~> 0.10.6") 22 | s.add_development_dependency("fakeweb", "~> 1.3.0") 23 | s.add_development_dependency("nokogiri", "~> 1.4.3.1") 24 | s.add_development_dependency("rspec", "~> 2.6.0") 25 | s.add_development_dependency("sham_rack", "~> 1.3.0") 26 | s.add_development_dependency("shoulda", "~> 2.11.3") 27 | s.add_development_dependency("capistrano", "~> 2.8.0") 28 | 29 | s.authors = ["thoughtbot, inc"] 30 | s.email = %q{support@airbrakeapp.com} 31 | s.homepage = "http://www.airbrakeapp.com" 32 | 33 | s.platform = Gem::Platform::RUBY 34 | end 35 | -------------------------------------------------------------------------------- /features/support/rake/Rakefile: -------------------------------------------------------------------------------- 1 | # A test harness for RakeHandler 2 | # 3 | require 'rake' 4 | require 'rubygems' 5 | require 'airbrake' 6 | require 'airbrake/rake_handler' 7 | 8 | Airbrake.configure do |c| 9 | end 10 | 11 | # Should catch exception 12 | task :airbrake do 13 | Airbrake.configuration.rescue_rake_exceptions = true 14 | stub_tty_output(true) 15 | raise_exception 16 | end 17 | 18 | # Should not catch exception 19 | task :airbrake_disabled do 20 | Airbrake.configuration.rescue_rake_exceptions = false 21 | stub_tty_output(true) 22 | raise_exception 23 | end 24 | 25 | # Should not catch exception as tty_output is true 26 | task :airbrake_autodetect_from_terminal do 27 | Airbrake.configuration.rescue_rake_exceptions = nil 28 | stub_tty_output(true) 29 | raise_exception 30 | end 31 | 32 | # Should catch exception as tty_output is false 33 | task :airbrake_autodetect_not_from_terminal do 34 | Airbrake.configuration.rescue_rake_exceptions = nil 35 | stub_tty_output(false) 36 | raise_exception 37 | end 38 | 39 | module Airbrake 40 | def self.notify(*args) 41 | # TODO if you need to check more params, you'll have to use json.dump or something 42 | $stderr.puts "airbrake #{args[1][:component]}" 43 | end 44 | end 45 | 46 | def stub_tty_output(value) 47 | Rake.application.instance_eval do 48 | @tty_output_stub = value 49 | def tty_output? 50 | @tty_output_stub 51 | end 52 | end 53 | end 54 | 55 | def raise_exception 56 | raise 'TEST' 57 | end 58 | -------------------------------------------------------------------------------- /test/rack_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class RackTest < Test::Unit::TestCase 4 | 5 | should "call the upstream app with the environment" do 6 | environment = { 'key' => 'value' } 7 | app = lambda { |env| ['response', {}, env] } 8 | stack = Airbrake::Rack.new(app) 9 | 10 | response = stack.call(environment) 11 | 12 | assert_equal ['response', {}, environment], response 13 | end 14 | 15 | should "deliver an exception raised while calling an upstream app" do 16 | Airbrake.stubs(:notify_or_ignore) 17 | 18 | exception = build_exception 19 | environment = { 'key' => 'value' } 20 | app = lambda do |env| 21 | raise exception 22 | end 23 | 24 | begin 25 | stack = Airbrake::Rack.new(app) 26 | stack.call(environment) 27 | rescue Exception => raised 28 | assert_equal exception, raised 29 | else 30 | flunk "Didn't raise an exception" 31 | end 32 | 33 | assert_received(Airbrake, :notify_or_ignore) do |expect| 34 | expect.with(exception, :rack_env => environment) 35 | end 36 | end 37 | 38 | should "deliver an exception in rack.exception" do 39 | Airbrake.stubs(:notify_or_ignore) 40 | exception = build_exception 41 | environment = { 'key' => 'value' } 42 | 43 | response = [200, {}, ['okay']] 44 | app = lambda do |env| 45 | env['rack.exception'] = exception 46 | response 47 | end 48 | stack = Airbrake::Rack.new(app) 49 | 50 | actual_response = stack.call(environment) 51 | 52 | assert_equal response, actual_response 53 | assert_received(Airbrake, :notify_or_ignore) do |expect| 54 | expect.with(exception, :rack_env => environment) 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/airbrake/rails.rb: -------------------------------------------------------------------------------- 1 | require 'airbrake' 2 | require 'airbrake/rails/controller_methods' 3 | require 'airbrake/rails/action_controller_catcher' 4 | require 'airbrake/rails/error_lookup' 5 | require 'airbrake/rails/javascript_notifier' 6 | 7 | module Airbrake 8 | module Rails 9 | def self.initialize 10 | if defined?(ActionController::Base) 11 | ActionController::Base.send(:include, Airbrake::Rails::ActionControllerCatcher) 12 | ActionController::Base.send(:include, Airbrake::Rails::ErrorLookup) 13 | ActionController::Base.send(:include, Airbrake::Rails::ControllerMethods) 14 | ActionController::Base.send(:include, Airbrake::Rails::JavascriptNotifier) 15 | end 16 | 17 | rails_logger = if defined?(::Rails.logger) 18 | ::Rails.logger 19 | elsif defined?(RAILS_DEFAULT_LOGGER) 20 | RAILS_DEFAULT_LOGGER 21 | end 22 | 23 | if defined?(::Rails.configuration) && ::Rails.configuration.respond_to?(:middleware) 24 | ::Rails.configuration.middleware.insert_after 'ActionController::Failsafe', 25 | Airbrake::Rack 26 | ::Rails.configuration.middleware.insert_after 'Rack::Lock', 27 | Airbrake::UserInformer 28 | end 29 | 30 | Airbrake.configure(true) do |config| 31 | config.logger = rails_logger 32 | config.environment_name = RAILS_ENV if defined?(RAILS_ENV) 33 | config.project_root = RAILS_ROOT if defined?(RAILS_ROOT) 34 | config.framework = "Rails: #{::Rails::VERSION::STRING}" if defined?(::Rails::VERSION) 35 | end 36 | end 37 | end 38 | end 39 | 40 | Airbrake::Rails.initialize 41 | 42 | -------------------------------------------------------------------------------- /lib/airbrake_tasks.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'uri' 3 | require 'active_support' 4 | 5 | # Capistrano tasks for notifying Airbrake of deploys 6 | module AirbrakeTasks 7 | 8 | # Alerts Airbrake of a deploy. 9 | # 10 | # @param [Hash] opts Data about the deploy that is set to Airbrake 11 | # 12 | # @option opts [String] :rails_env Environment of the deploy (production, staging) 13 | # @option opts [String] :scm_revision The given revision/sha that is being deployed 14 | # @option opts [String] :scm_repository Address of your repository to help with code lookups 15 | # @option opts [String] :local_username Who is deploying 16 | def self.deploy(opts = {}) 17 | if Airbrake.configuration.api_key.blank? 18 | puts "I don't seem to be configured with an API key. Please check your configuration." 19 | return false 20 | end 21 | 22 | if opts[:rails_env].blank? 23 | puts "I don't know to which Rails environment you are deploying (use the TO=production option)." 24 | return false 25 | end 26 | 27 | dry_run = opts.delete(:dry_run) 28 | params = {'api_key' => opts.delete(:api_key) || 29 | Airbrake.configuration.api_key} 30 | opts.each {|k,v| params["deploy[#{k}]"] = v } 31 | 32 | url = URI.parse("http://#{Airbrake.configuration.host || 'airbrakeapp.com'}/deploys.txt") 33 | 34 | proxy = Net::HTTP.Proxy(Airbrake.configuration.proxy_host, 35 | Airbrake.configuration.proxy_port, 36 | Airbrake.configuration.proxy_user, 37 | Airbrake.configuration.proxy_pass) 38 | 39 | if dry_run 40 | puts url, params.inspect 41 | return true 42 | else 43 | response = proxy.post_form(url, params) 44 | 45 | puts response.body 46 | return Net::HTTPSuccess === response 47 | end 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /lib/airbrake/rake_handler.rb: -------------------------------------------------------------------------------- 1 | # Patch Rake::Application to handle errors with Airbrake 2 | module Airbrake::RakeHandler 3 | def self.included(klass) 4 | klass.class_eval do 5 | include Rake087Methods unless defined?(Rake::VERSION) && Rake::VERSION >= '0.9.0' 6 | alias_method :display_error_message_without_airbrake, :display_error_message 7 | alias_method :display_error_message, :display_error_message_with_airbrake 8 | end 9 | end 10 | 11 | def display_error_message_with_airbrake(ex) 12 | if Airbrake.configuration.rescue_rake_exceptions || 13 | (Airbrake.configuration.rescue_rake_exceptions===nil && !self.tty_output?) 14 | 15 | Airbrake.notify(ex, :component => reconstruct_command_line, :cgi_data => ENV) 16 | end 17 | 18 | display_error_message_without_airbrake(ex) 19 | end 20 | 21 | def reconstruct_command_line 22 | "rake #{ARGV.join( ' ' )}" 23 | end 24 | 25 | # This module brings Rake 0.8.7 error handling to 0.9.0 standards 26 | module Rake087Methods 27 | # Method taken from Rake 0.9.0 source 28 | # 29 | # Provide standard exception handling for the given block. 30 | def standard_exception_handling 31 | begin 32 | yield 33 | rescue SystemExit => ex 34 | # Exit silently with current status 35 | raise 36 | rescue OptionParser::InvalidOption => ex 37 | $stderr.puts ex.message 38 | exit(false) 39 | rescue Exception => ex 40 | # Exit with error message 41 | display_error_message(ex) 42 | exit(false) 43 | end 44 | end 45 | 46 | # Method extracted from Rake 0.8.7 source 47 | def display_error_message(ex) 48 | $stderr.puts "#{name} aborted!" 49 | $stderr.puts ex.message 50 | if options.trace 51 | $stderr.puts ex.backtrace.join("\n") 52 | else 53 | $stderr.puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || "" 54 | $stderr.puts "(See full trace by running task with --trace)" 55 | end 56 | end 57 | end 58 | end 59 | 60 | Rake.application.instance_eval do 61 | class << self 62 | include Airbrake::RakeHandler 63 | end 64 | end 65 | 66 | -------------------------------------------------------------------------------- /lib/airbrake/rails/controller_methods.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | module Rails 3 | module ControllerMethods 4 | private 5 | 6 | # This method should be used for sending manual notifications while you are still 7 | # inside the controller. Otherwise it works like Airbrake.notify. 8 | def notify_airbrake(hash_or_exception) 9 | unless airbrake_local_request? 10 | Airbrake.notify(hash_or_exception, airbrake_request_data) 11 | end 12 | end 13 | 14 | def airbrake_local_request? 15 | if defined?(::Rails.application.config) 16 | ::Rails.application.config.consider_all_requests_local || request.local? 17 | else 18 | consider_all_requests_local || local_request? 19 | end 20 | end 21 | 22 | def airbrake_ignore_user_agent? #:nodoc: 23 | # Rails 1.2.6 doesn't have request.user_agent, so check for it here 24 | user_agent = request.respond_to?(:user_agent) ? request.user_agent : request.env["HTTP_USER_AGENT"] 25 | Airbrake.configuration.ignore_user_agent.flatten.any? { |ua| ua === user_agent } 26 | end 27 | 28 | def airbrake_request_data 29 | { :parameters => airbrake_filter_if_filtering(params.to_hash), 30 | :session_data => airbrake_filter_if_filtering(airbrake_session_data), 31 | :controller => params[:controller], 32 | :action => params[:action], 33 | :url => airbrake_request_url, 34 | :cgi_data => airbrake_filter_if_filtering(request.env) } 35 | end 36 | 37 | def airbrake_filter_if_filtering(hash) 38 | return hash if ! hash.is_a?(Hash) 39 | 40 | if respond_to?(:filter_parameters) 41 | filter_parameters(hash) rescue hash 42 | else 43 | hash 44 | end 45 | end 46 | 47 | def airbrake_session_data 48 | if session.respond_to?(:to_hash) 49 | session.to_hash 50 | else 51 | session.data 52 | end 53 | end 54 | 55 | def airbrake_request_url 56 | url = "#{request.protocol}#{request.host}" 57 | 58 | unless [80, 443].include?(request.port) 59 | url << ":#{request.port}" 60 | end 61 | 62 | url << request.fullpath 63 | url 64 | end 65 | end 66 | end 67 | end 68 | 69 | -------------------------------------------------------------------------------- /test/logger_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class LoggerTest < Test::Unit::TestCase 4 | def stub_http(response, body = nil) 5 | response.stubs(:body => body) if body 6 | @http = stub(:post => response, 7 | :read_timeout= => nil, 8 | :open_timeout= => nil, 9 | :use_ssl= => nil) 10 | Net::HTTP.stubs(:new).returns(@http) 11 | end 12 | 13 | def send_notice 14 | Airbrake.sender.send_to_airbrake('data') 15 | end 16 | 17 | def stub_verbose_log 18 | Airbrake.stubs(:write_verbose_log) 19 | end 20 | 21 | def assert_logged(expected) 22 | assert_received(Airbrake, :write_verbose_log) do |expect| 23 | expect.with {|actual| actual =~ expected } 24 | end 25 | end 26 | 27 | def assert_not_logged(expected) 28 | assert_received(Airbrake, :write_verbose_log) do |expect| 29 | expect.with {|actual| actual =~ expected }.never 30 | end 31 | end 32 | 33 | def configure 34 | Airbrake.configure { |config| } 35 | end 36 | 37 | should "report that notifier is ready when configured" do 38 | stub_verbose_log 39 | configure 40 | assert_logged /Notifier (.*) ready/ 41 | end 42 | 43 | should "not report that notifier is ready when internally configured" do 44 | stub_verbose_log 45 | Airbrake.configure(true) { |config| } 46 | assert_not_logged /.*/ 47 | end 48 | 49 | should "print environment info a successful notification without a body" do 50 | reset_config 51 | stub_verbose_log 52 | stub_http(Net::HTTPSuccess) 53 | send_notice 54 | assert_logged /Environment Info:/ 55 | assert_not_logged /Response from Airbrake:/ 56 | end 57 | 58 | should "print environment info on a failed notification without a body" do 59 | reset_config 60 | stub_verbose_log 61 | stub_http(Net::HTTPError) 62 | send_notice 63 | assert_logged /Environment Info:/ 64 | assert_not_logged /Response from Airbrake:/ 65 | end 66 | 67 | should "print environment info and response on a success with a body" do 68 | reset_config 69 | stub_verbose_log 70 | stub_http(Net::HTTPSuccess, 'test') 71 | send_notice 72 | assert_logged /Environment Info:/ 73 | assert_logged /Response from Airbrake:/ 74 | end 75 | 76 | should "print environment info and response on a failure with a body" do 77 | reset_config 78 | stub_verbose_log 79 | stub_http(Net::HTTPError, 'test') 80 | send_notice 81 | assert_logged /Environment Info:/ 82 | assert_logged /Response from Airbrake:/ 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /features/user_informer.feature: -------------------------------------------------------------------------------- 1 | Feature: Inform the user of the airbrake notice that was just created 2 | 3 | Background: 4 | Given I have built and installed the "airbrake" gem 5 | 6 | Scenario: Rescue an exception in a controller 7 | When I generate a new Rails application 8 | And I configure the Airbrake shim 9 | And I configure my application to require the "airbrake" gem 10 | And I run the airbrake generator with "-k myapikey" 11 | And I define a response for "TestController#index": 12 | """ 13 | raise RuntimeError, "some message" 14 | """ 15 | And the response page for a "500" error is 16 | """ 17 | 18 | """ 19 | And I route "/test/index" to "test#index" 20 | And I perform a request to "http://example.com:123/test/index?param=value" 21 | Then I should see "Airbrake Error 3799307" 22 | 23 | Scenario: Rescue an exception in a controller with a custom error string 24 | When I generate a new Rails application 25 | And I configure the Airbrake shim 26 | And I configure my application to require the "airbrake" gem 27 | And I configure the notifier to use the following configuration lines: 28 | """ 29 | config.user_information = 'Error #{{ error_id }}' 30 | """ 31 | And I run the airbrake generator with "-k myapikey" 32 | And I define a response for "TestController#index": 33 | """ 34 | raise RuntimeError, "some message" 35 | """ 36 | And the response page for a "500" error is 37 | """ 38 | 39 | """ 40 | And I route "/test/index" to "test#index" 41 | And I perform a request to "http://example.com:123/test/index?param=value" 42 | Then I should see "Error #3799307" 43 | 44 | Scenario: Don't inform them user 45 | When I generate a new Rails application 46 | And I configure the Airbrake shim 47 | And I configure my application to require the "airbrake" gem 48 | And I configure the notifier to use the following configuration lines: 49 | """ 50 | config.user_information = false 51 | """ 52 | And I run the airbrake generator with "-k myapikey" 53 | And I define a response for "TestController#index": 54 | """ 55 | raise RuntimeError, "some message" 56 | """ 57 | And the response page for a "500" error is 58 | """ 59 | 60 | """ 61 | And I route "/test/index" to "test#index" 62 | And I perform a request to "http://example.com:123/test/index?param=value" 63 | Then I should not see "Airbrake Error 3799307" 64 | -------------------------------------------------------------------------------- /lib/airbrake/rails3_tasks.rb: -------------------------------------------------------------------------------- 1 | require 'airbrake' 2 | require File.join(File.dirname(__FILE__), 'shared_tasks') 3 | 4 | namespace :airbrake do 5 | desc "Verify your gem installation by sending a test exception to the airbrake service" 6 | task :test => [:environment] do 7 | Rails.logger = Logger.new(STDOUT) 8 | Rails.logger.level = Logger::DEBUG 9 | Airbrake.configure(true) do |config| 10 | config.logger = Rails.logger 11 | end 12 | 13 | require './app/controllers/application_controller' 14 | 15 | class AirbrakeTestingException < RuntimeError; end 16 | 17 | unless Airbrake.configuration.api_key 18 | puts "Airbrake needs an API key configured! Check the README to see how to add it." 19 | exit 20 | end 21 | 22 | Airbrake.configuration.development_environments = [] 23 | 24 | puts "Configuration:" 25 | Airbrake.configuration.to_hash.each do |key, value| 26 | puts sprintf("%25s: %s", key.to_s, value.inspect.slice(0, 55)) 27 | end 28 | 29 | unless defined?(ApplicationController) 30 | puts "No ApplicationController found" 31 | exit 32 | end 33 | 34 | puts 'Setting up the Controller.' 35 | class ApplicationController 36 | # This is to bypass any filters that may prevent access to the action. 37 | prepend_before_filter :test_airbrake 38 | def test_airbrake 39 | puts "Raising '#{exception_class.name}' to simulate application failure." 40 | raise exception_class.new, 'Testing airbrake via "rake airbrake:test". If you can see this, it works.' 41 | end 42 | 43 | # def rescue_action(exception) 44 | # rescue_action_in_public exception 45 | # end 46 | 47 | # Ensure we actually have an action to go to. 48 | def verify; end 49 | 50 | # def consider_all_requests_local 51 | # false 52 | # end 53 | 54 | # def local_request? 55 | # false 56 | # end 57 | 58 | def exception_class 59 | exception_name = ENV['EXCEPTION'] || "AirbrakeTestingException" 60 | Object.const_get(exception_name) 61 | rescue 62 | Object.const_set(exception_name, Class.new(Exception)) 63 | end 64 | 65 | def logger 66 | nil 67 | end 68 | end 69 | class AirbrakeVerificationController < ApplicationController; end 70 | 71 | Rails.application.routes_reloader.execute_if_updated 72 | Rails.application.routes.draw do 73 | match 'verify' => 'application#verify', :as => 'verify' 74 | end 75 | 76 | puts 'Processing request.' 77 | env = Rack::MockRequest.env_for("/verify") 78 | 79 | Rails.application.call(env) 80 | end 81 | end 82 | 83 | -------------------------------------------------------------------------------- /features/support/terminal.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | Before do 4 | @terminal = Terminal.new 5 | end 6 | 7 | After do |story| 8 | if story.failed? 9 | # puts @terminal.output 10 | end 11 | end 12 | 13 | class Terminal 14 | attr_reader :output, :status 15 | attr_accessor :environment_variables, :invoke_heroku_rake_tasks_locally 16 | 17 | def initialize 18 | @cwd = FileUtils.pwd 19 | @output = "" 20 | @status = 0 21 | @logger = Logger.new(File.join(TEMP_DIR, 'terminal.log')) 22 | 23 | @invoke_heroku_rake_tasks_locally = false 24 | 25 | @environment_variables = { 26 | "GEM_HOME" => LOCAL_GEM_ROOT, 27 | "GEM_PATH" => "#{LOCAL_GEM_ROOT}:#{BUILT_GEM_ROOT}", 28 | "PATH" => "#{gem_bin_path}:#{ENV['PATH']}" 29 | } 30 | end 31 | 32 | def cd(directory) 33 | @cwd = directory 34 | end 35 | 36 | def run(command) 37 | command = optionally_invoke_heroku_rake_tasks_locally(command) 38 | 39 | output << "#{command}\n" 40 | FileUtils.cd(@cwd) do 41 | # The ; forces ruby to shell out so the env settings work right 42 | cmdline = "#{environment_settings} #{command} 2>&1 ; " 43 | logger.debug(cmdline) 44 | result = `#{cmdline}` 45 | logger.debug(result) 46 | output << result 47 | end 48 | @status = $? 49 | end 50 | 51 | def optionally_invoke_heroku_rake_tasks_locally(command) 52 | if invoke_heroku_rake_tasks_locally 53 | command.sub(/^heroku /, '') 54 | else 55 | command 56 | end 57 | end 58 | 59 | def echo(string) 60 | logger.debug(string) 61 | end 62 | 63 | def build_and_install_gem(gemspec) 64 | pkg_dir = File.join(TEMP_DIR, 'pkg') 65 | FileUtils.mkdir_p(pkg_dir) 66 | output = `gem build #{gemspec} 2>&1` 67 | gem_file = Dir.glob("*.gem").first 68 | unless gem_file 69 | raise "Gem didn't build:\n#{output}" 70 | end 71 | target = File.join(pkg_dir, gem_file) 72 | FileUtils.mv(gem_file, target) 73 | install_gem_to(LOCAL_GEM_ROOT, target) 74 | end 75 | 76 | def install_gem(gem) 77 | install_gem_to(LOCAL_GEM_ROOT, gem) 78 | end 79 | 80 | def uninstall_gem(gem) 81 | `gem uninstall -i #{LOCAL_GEM_ROOT} #{gem}` 82 | end 83 | 84 | def prepend_path(path) 85 | @environment_variables['PATH'] = path + ":" + @environment_variables['PATH'] 86 | end 87 | 88 | private 89 | 90 | def install_gem_to(root, gem) 91 | `gem install -i #{root} --no-ri --no-rdoc #{gem}` 92 | end 93 | 94 | def environment_settings 95 | @environment_variables.map { |key, value| "#{key}=#{value}" }.join(' ') 96 | end 97 | 98 | def gem_bin_path 99 | File.join(LOCAL_GEM_ROOT, "bin") 100 | end 101 | 102 | attr_reader :logger 103 | end 104 | -------------------------------------------------------------------------------- /lib/templates/rescue.erb: -------------------------------------------------------------------------------- 1 | 76 | 77 | 92 | -------------------------------------------------------------------------------- /lib/airbrake/backtrace.rb: -------------------------------------------------------------------------------- 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 | "" 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 | "" 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 101 | -------------------------------------------------------------------------------- /lib/airbrake/sender.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | # Sends out the notice to Airbrake 3 | class Sender 4 | 5 | NOTICES_URI = '/notifier_api/v2/notices/'.freeze 6 | HTTP_ERRORS = [Timeout::Error, 7 | Errno::EINVAL, 8 | Errno::ECONNRESET, 9 | EOFError, 10 | Net::HTTPBadResponse, 11 | Net::HTTPHeaderSyntaxError, 12 | Net::ProtocolError, 13 | Errno::ECONNREFUSED].freeze 14 | 15 | def initialize(options = {}) 16 | [:proxy_host, :proxy_port, :proxy_user, :proxy_pass, :protocol, 17 | :host, :port, :secure, :http_open_timeout, :http_read_timeout].each do |option| 18 | instance_variable_set("@#{option}", options[option]) 19 | end 20 | end 21 | 22 | # Sends the notice data off to Airbrake for processing. 23 | # 24 | # @param [String] data The XML notice to be sent off 25 | def send_to_airbrake(data) 26 | logger.debug { "Sending request to #{url.to_s}:\n#{data}" } if logger 27 | 28 | http = 29 | Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass). 30 | new(url.host, url.port) 31 | 32 | http.read_timeout = http_read_timeout 33 | http.open_timeout = http_open_timeout 34 | 35 | if secure 36 | http.use_ssl = true 37 | http.ca_file = OpenSSL::X509::DEFAULT_CERT_FILE if File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) 38 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 39 | else 40 | http.use_ssl = false 41 | end 42 | 43 | response = begin 44 | http.post(url.path, data, HEADERS) 45 | rescue *HTTP_ERRORS => e 46 | log :error, "Timeout while contacting the Airbrake server." 47 | nil 48 | end 49 | 50 | case response 51 | when Net::HTTPSuccess then 52 | log :info, "Success: #{response.class}", response 53 | else 54 | log :error, "Failure: #{response.class}", response 55 | end 56 | 57 | if response && response.respond_to?(:body) 58 | error_id = response.body.match(%r{]*>(.*?)}) 59 | error_id[1] if error_id 60 | end 61 | end 62 | 63 | private 64 | 65 | attr_reader :proxy_host, :proxy_port, :proxy_user, :proxy_pass, :protocol, 66 | :host, :port, :secure, :http_open_timeout, :http_read_timeout 67 | 68 | def url 69 | URI.parse("#{protocol}://#{host}:#{port}").merge(NOTICES_URI) 70 | end 71 | 72 | def log(level, message, response = nil) 73 | logger.send level, LOG_PREFIX + message if logger 74 | Airbrake.report_environment_info 75 | Airbrake.report_response_body(response.body) if response && response.respond_to?(:body) 76 | end 77 | 78 | def logger 79 | Airbrake.logger 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/airbrake/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'airbrake' 2 | require File.join(File.dirname(__FILE__), 'shared_tasks') 3 | 4 | namespace :airbrake do 5 | desc "Verify your gem installation by sending a test exception to the airbrake service" 6 | task :test => ['airbrake:log_stdout', :environment] do 7 | RAILS_DEFAULT_LOGGER.level = Logger::DEBUG 8 | 9 | require 'action_controller/test_process' 10 | 11 | Dir["app/controllers/application*.rb"].each { |file| require(File.expand_path(file)) } 12 | 13 | class AirbrakeTestingException < RuntimeError; end 14 | 15 | unless Airbrake.configuration.api_key 16 | puts "Airbrake needs an API key configured! Check the README to see how to add it." 17 | exit 18 | end 19 | 20 | Airbrake.configuration.development_environments = [] 21 | 22 | catcher = Airbrake::Rails::ActionControllerCatcher 23 | in_controller = ApplicationController.included_modules.include?(catcher) 24 | in_base = ActionController::Base.included_modules.include?(catcher) 25 | if !in_controller || !in_base 26 | puts "Rails initialization did not occur" 27 | exit 28 | end 29 | 30 | puts "Configuration:" 31 | Airbrake.configuration.to_hash.each do |key, value| 32 | puts sprintf("%25s: %s", key.to_s, value.inspect.slice(0, 55)) 33 | end 34 | 35 | unless defined?(ApplicationController) 36 | puts "No ApplicationController found" 37 | exit 38 | end 39 | 40 | puts 'Setting up the Controller.' 41 | class ApplicationController 42 | # This is to bypass any filters that may prevent access to the action. 43 | prepend_before_filter :test_airbrake 44 | def test_airbrake 45 | puts "Raising '#{exception_class.name}' to simulate application failure." 46 | raise exception_class.new, 'Testing airbrake via "rake airbrake:test". If you can see this, it works.' 47 | end 48 | 49 | def rescue_action(exception) 50 | rescue_action_in_public exception 51 | end 52 | 53 | # Ensure we actually have an action to go to. 54 | def verify; end 55 | 56 | def consider_all_requests_local 57 | false 58 | end 59 | 60 | def local_request? 61 | false 62 | end 63 | 64 | def exception_class 65 | exception_name = ENV['EXCEPTION'] || "AirbrakeTestingException" 66 | exception_name.split("::").inject(Object){|klass, name| klass.const_get(name)} 67 | rescue 68 | Object.const_set(exception_name.gsub(/:+/, "_"), Class.new(Exception)) 69 | end 70 | 71 | def logger 72 | nil 73 | end 74 | end 75 | class AirbrakeVerificationController < ApplicationController; end 76 | 77 | puts 'Processing request.' 78 | request = ActionController::TestRequest.new("REQUEST_URI" => "/airbrake_verification_controller") 79 | response = ActionController::TestResponse.new 80 | AirbrakeVerificationController.new.process(request, response) 81 | end 82 | end 83 | 84 | -------------------------------------------------------------------------------- /test/airbrake_2_2.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /lib/rails/generators/airbrake/airbrake_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | 3 | class AirbrakeGenerator < Rails::Generators::Base 4 | 5 | class_option :api_key, :aliases => "-k", :type => :string, :desc => "Your Airbrake API key" 6 | class_option :heroku, :type => :boolean, :desc => "Use the Heroku addon to provide your Airbrake API key" 7 | class_option :app, :aliases => "-a", :type => :string, :desc => "Your Heroku app name (only required if deploying to >1 Heroku app)" 8 | 9 | def self.source_root 10 | @_airbrake_source_root ||= File.expand_path("../../../../../generators/airbrake/templates", __FILE__) 11 | end 12 | 13 | def install 14 | ensure_api_key_was_configured 15 | ensure_plugin_is_not_present 16 | append_capistrano_hook 17 | generate_initializer unless api_key_configured? 18 | determine_api_key if heroku? 19 | test_airbrake 20 | end 21 | 22 | private 23 | 24 | def ensure_api_key_was_configured 25 | if !options[:api_key] && !options[:heroku] && !api_key_configured? 26 | puts "Must pass --api-key or --heroku or create config/initializers/airbrake.rb" 27 | exit 28 | end 29 | end 30 | 31 | def ensure_plugin_is_not_present 32 | if plugin_is_present? 33 | puts "You must first remove the airbrake plugin. Please run: script/plugin remove airbrake" 34 | exit 35 | end 36 | end 37 | 38 | def append_capistrano_hook 39 | if File.exists?('config/deploy.rb') && File.exists?('Capfile') 40 | append_file('config/deploy.rb', <<-HOOK) 41 | 42 | require './config/boot' 43 | require 'airbrake/capistrano' 44 | HOOK 45 | end 46 | end 47 | 48 | def api_key_expression 49 | s = if options[:api_key] 50 | "'#{options[:api_key]}'" 51 | elsif options[:heroku] 52 | "ENV['HOPTOAD_API_KEY']" 53 | end 54 | end 55 | 56 | def generate_initializer 57 | template 'initializer.rb', 'config/initializers/airbrake.rb' 58 | end 59 | 60 | def determine_api_key 61 | puts "Attempting to determine your API Key from Heroku..." 62 | ENV['HOPTOAD_API_KEY'] = heroku_api_key 63 | if ENV['HOPTOAD_API_KEY'].blank? 64 | puts "... Failed." 65 | puts "WARNING: We were unable to detect the Airbrake API Key from your Heroku environment." 66 | puts "Your Heroku application environment may not be configured correctly." 67 | exit 1 68 | else 69 | puts "... Done." 70 | puts "Heroku's Airbrake API Key is '#{ENV['HOPTOAD_API_KEY']}'" 71 | end 72 | end 73 | 74 | def heroku_api_key 75 | app = options[:app] ? " --app #{options[:app]}" : '' 76 | `heroku console#{app} 'puts ENV[%{HOPTOAD_API_KEY}]'`.split("\n").first 77 | end 78 | 79 | def heroku? 80 | options[:heroku] || 81 | system("grep HOPTOAD_API_KEY config/initializers/airbrake.rb") || 82 | system("grep HOPTOAD_API_KEY config/environment.rb") 83 | end 84 | 85 | def api_key_configured? 86 | File.exists?('config/initializers/airbrake.rb') 87 | end 88 | 89 | def test_airbrake 90 | puts run("rake airbrake:test --trace") 91 | end 92 | 93 | def plugin_is_present? 94 | File.exists?('vendor/plugins/airbrake') 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /generators/airbrake/airbrake_generator.rb: -------------------------------------------------------------------------------- 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['HOPTOAD_API_KEY']" 46 | end 47 | end 48 | 49 | def determine_api_key 50 | puts "Attempting to determine your API Key from Heroku..." 51 | ENV['HOPTOAD_API_KEY'] = heroku_api_key 52 | if ENV['HOPTOAD_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['HOPTOAD_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[%{HOPTOAD_API_KEY}]'`.split("\n").first 66 | end 67 | 68 | def heroku? 69 | options[:heroku] || 70 | system("grep HOPTOAD_API_KEY config/initializers/airbrake.rb") || 71 | system("grep HOPTOAD_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 91 | -------------------------------------------------------------------------------- /README_FOR_HEROKU_ADDON.md: -------------------------------------------------------------------------------- 1 | Hoptoad 2 | =========== 3 | Send your application errors to our hosted service and reclaim your inbox. 4 | 5 | 1. Installing the Heroku add-on 6 | ---------------------------- 7 | To use Hoptoad on Heroku, install the Hoptoad add-on: 8 | 9 | $ heroku addons:add hoptoad:basic # This adds the the basic plan. 10 | # If you'd like another plan, specify that instead. 11 | 12 | 2. Including the Hoptoad notifier in your application 13 | -------------------------------------------------- 14 | After adding the Hoptoad add-on, you will need to install and configure the Hoptoad notifier. 15 | 16 | Your application connects to Hoptoad with an API key. On Heroku, this is automatically provided to your 17 | application in `ENV['HOPTOAD_API_KEY']`, so installation should be a snap! 18 | 19 | ### Rails 3.x 20 | 21 | Add the hoptoad_notifier and heroku gems to your Gemfile. In Gemfile: 22 | 23 | gem 'hoptoad_notifier' 24 | gem 'heroku' 25 | 26 | Then from your project's RAILS_ROOT, run: 27 | 28 | $ bundle install 29 | $ script/rails generate hoptoad --heroku 30 | 31 | ### Rails 2.x 32 | 33 | Install the heroku gem if you haven't already: 34 | 35 | gem install heroku 36 | 37 | Add the hoptoad_notifier gem to your app. In config/environment.rb: 38 | 39 | config.gem 'hoptoad_notifier' 40 | 41 | Then from your project's RAILS_ROOT, run: 42 | 43 | $ rake gems:install 44 | $ rake gems:unpack GEM=hoptoad_notifier 45 | $ script/generate hoptoad --heroku 46 | 47 | As always, if you choose not to vendor the hoptoad_notifier gem, make sure 48 | every server you deploy to has the gem installed or your application won't start. 49 | 50 | ### Rack applications 51 | 52 | In order to use hoptoad_notifier in a non-Rails rack app, just load the hoptoad_notifier, configure your API key, and use the HoptoadNotifier::Rack middleware: 53 | 54 | require 'rubygems' 55 | require 'rack' 56 | require 'hoptoad_notifier' 57 | 58 | HoptoadNotifier.configure do |config| 59 | config.api_key = `ENV['HOPTOAD_API_KEY']` 60 | end 61 | 62 | app = Rack::Builder.app do 63 | use HoptoadNotifier::Rack 64 | run lambda { |env| raise "Rack down" } 65 | end 66 | 67 | ### Rails 1.x 68 | 69 | For Rails 1.x, visit the [Hoptoad notifier's README on GitHub](http://github.com/thoughtbot/hoptoad_notifier), 70 | and be sure to use `ENV['HOPTOAD_API_KEY']` where your API key is required in configuration code. 71 | 72 | 3. Configure your notification settings (important!) 73 | --------------------------------------------------- 74 | 75 | Once you have included and configured the notifier in your application, 76 | you will want to configure your notification settings. 77 | 78 | This is important - without setting your email address, you won't receive notification emails. 79 | 80 | Hoptoad can deliver exception notifications to your email inbox. To configure these delivery settings: 81 | 82 | 1. Visit your application's Hoptoad Add-on page, like [ http://api.heroku.com/myapps/my-great-app/addons/hoptoad:basic ](http://api.heroku.com/myapps/my-great-app/addons/hoptoad:basic) 83 | 2. Click "Go to Hoptoad admin" to configure the Hoptoad Add-on on the Hoptoadapp.com website 84 | 3. Click the "Profile" button in the header to edit your email address and notification settings. 85 | 86 | 4. Optionally: Set up deploy notification 87 | ----------------------------------------- 88 | 89 | If your Hoptoad plan supports deploy notification, set it up for your Heroku application like this: 90 | 91 | rake hoptoad:heroku:add_deploy_notification 92 | 93 | This will install a Heroku [HTTP Deploy Hook](http://docs.heroku.com/deploy-hooks) to notify Hoptoad of the deploy. 94 | -------------------------------------------------------------------------------- /features/rails_with_js_notifier.feature: -------------------------------------------------------------------------------- 1 | Feature: Install the Gem in a Rails application and enable the JavaScript notifier 2 | 3 | Background: 4 | Given I have built and installed the "airbrake" gem 5 | 6 | Scenario: Include the Javascript notifier when enabled 7 | When I generate a new Rails application 8 | And I configure the Airbrake shim 9 | And I configure my application to require the "airbrake" gem 10 | When I configure the notifier to use the following configuration lines: 11 | """ 12 | config.api_key = "myapikey" 13 | """ 14 | And I define a response for "TestController#index": 15 | """ 16 | render :inline => '<%= airbrake_javascript_notifier %>' 17 | """ 18 | And I route "/test/index" to "test#index" 19 | And I perform a request to "http://example.com:123/test/index" 20 | Then I should see the notifier JavaScript for the following: 21 | | api_key | environment | host | 22 | | myapikey | production | airbrakeapp.com | 23 | And the notifier JavaScript should provide the following errorDefaults: 24 | | url | component | action | 25 | | http://example.com:123/test/index | test | index | 26 | 27 | Scenario: Include the Javascript notifier when enabled using custom configuration settings 28 | When I generate a new Rails application 29 | And I configure the Airbrake shim 30 | And I configure my application to require the "airbrake" gem 31 | When I configure the notifier to use the following configuration lines: 32 | """ 33 | config.api_key = "myapikey!" 34 | config.host = "myairbrake.com" 35 | config.port = 3001 36 | """ 37 | And I define a response for "TestController#index": 38 | """ 39 | render :inline => '<%= airbrake_javascript_notifier %>' 40 | """ 41 | And I route "/test/index" to "test#index" 42 | And I perform a request to "http://example.com:123/test/index" 43 | Then I should see the notifier JavaScript for the following: 44 | | api_key | environment | host | 45 | | myapikey! | production | myairbrake.com:3001 | 46 | 47 | Scenario: Don't include the Javascript notifier by default 48 | When I generate a new Rails application 49 | And I configure the Airbrake shim 50 | And I configure my application to require the "airbrake" gem 51 | When I configure the notifier to use the following configuration lines: 52 | """ 53 | config.api_key = "myapikey!" 54 | """ 55 | And I define a response for "TestController#index": 56 | """ 57 | render :inline => "" 58 | """ 59 | And I route "/test/index" to "test#index" 60 | And I perform a request to "http://example.com:123/test/index" 61 | Then I should not see notifier JavaScript 62 | 63 | Scenario: Don't include the Javascript notifier when enabled in non-public environments 64 | When I generate a new Rails application 65 | And I configure the Airbrake shim 66 | And I configure my application to require the "airbrake" gem 67 | When I configure the notifier to use the following configuration lines: 68 | """ 69 | config.api_key = "myapikey!" 70 | config.environment_name = 'test' 71 | """ 72 | And I define a response for "TestController#index": 73 | """ 74 | render :inline => '<%= airbrake_javascript_notifier %>' 75 | """ 76 | And I route "/test/index" to "test#index" 77 | And I perform a request to "http://example.com:123/test/index" in the "test" environment 78 | Then I should not see notifier JavaScript 79 | -------------------------------------------------------------------------------- /lib/airbrake.rb: -------------------------------------------------------------------------------- 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.2" 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_writer :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 | yield(configuration) 81 | self.sender = Sender.new(configuration) 82 | report_ready unless silent 83 | end 84 | 85 | # The configuration object. 86 | # @see Airbrake.configure 87 | def configuration 88 | @configuration ||= Configuration.new 89 | end 90 | 91 | # Sends an exception manually using this method, even when you are not in a controller. 92 | # 93 | # @param [Exception] exception The exception you want to notify Airbrake about. 94 | # @param [Hash] opts Data that will be sent to Airbrake. 95 | # 96 | # @option opts [String] :api_key The API key for this project. The API key is a unique identifier that Airbrake uses for identification. 97 | # @option opts [String] :error_message The error returned by the exception (or the message you want to log). 98 | # @option opts [String] :backtrace A backtrace, usually obtained with +caller+. 99 | # @option opts [String] :rack_env The Rack environment. 100 | # @option opts [String] :session The contents of the user's session. 101 | # @option opts [String] :environment_name The application environment name. 102 | def notify(exception, opts = {}) 103 | send_notice(build_notice_for(exception, opts)) 104 | end 105 | 106 | # Sends the notice unless it is one of the default ignored exceptions 107 | # @see Airbrake.notify 108 | def notify_or_ignore(exception, opts = {}) 109 | notice = build_notice_for(exception, opts) 110 | send_notice(notice) unless notice.ignore? 111 | end 112 | 113 | def build_lookup_hash_for(exception, options = {}) 114 | notice = build_notice_for(exception, options) 115 | 116 | result = {} 117 | result[:action] = notice.action rescue nil 118 | result[:component] = notice.component rescue nil 119 | result[:error_class] = notice.error_class if notice.error_class 120 | result[:environment_name] = 'production' 121 | 122 | unless notice.backtrace.lines.empty? 123 | result[:file] = notice.backtrace.lines.first.file 124 | result[:line_number] = notice.backtrace.lines.first.number 125 | end 126 | 127 | result 128 | end 129 | 130 | private 131 | 132 | def send_notice(notice) 133 | if configuration.public? 134 | sender.send_to_airbrake(notice.to_xml) 135 | end 136 | end 137 | 138 | def build_notice_for(exception, opts = {}) 139 | exception = unwrap_exception(exception) 140 | if exception.respond_to?(:to_hash) 141 | opts = opts.merge(exception.to_hash) 142 | else 143 | opts = opts.merge(:exception => exception) 144 | end 145 | Notice.new(configuration.merge(opts)) 146 | end 147 | 148 | def unwrap_exception(exception) 149 | if exception.respond_to?(:original_exception) 150 | exception.original_exception 151 | elsif exception.respond_to?(:continued_exception) 152 | exception.continued_exception 153 | else 154 | exception 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /features/support/rails.rb: -------------------------------------------------------------------------------- 1 | module RailsHelpers 2 | def rails_root_exists? 3 | File.exists?(environment_path) 4 | end 5 | 6 | def application_controller_filename 7 | controller_filename = File.join(rails_root, 'app', 'controllers', "application_controller.rb") 8 | end 9 | 10 | def rails3? 11 | rails_version =~ /^3/ 12 | end 13 | 14 | def rails_root 15 | LOCAL_RAILS_ROOT 16 | end 17 | 18 | def rails_uses_rack? 19 | rails3? || rails_version =~ /^2\.3/ 20 | end 21 | 22 | def rails_version 23 | @rails_version ||= begin 24 | if bundler_manages_gems? 25 | rails_version = open(gemfile_path).read.match(/gem.*rails["'].*["'](.+)["']/)[1] 26 | else 27 | environment_file = File.join(rails_root, 'config', 'environment.rb') 28 | rails_version = `grep RAILS_GEM_VERSION #{environment_file}`.match(/[\d.]+/)[0] 29 | end 30 | end 31 | end 32 | 33 | def bundler_manages_gems? 34 | File.exists?(gemfile_path) 35 | end 36 | 37 | def gemfile_path 38 | gemfile = File.join(rails_root, 'Gemfile') 39 | end 40 | 41 | def rails_manages_gems? 42 | rails_version =~ /^2\.[123]/ 43 | end 44 | 45 | def rails_supports_initializers? 46 | rails3? || rails_version =~ /^2\./ 47 | end 48 | 49 | def rails_finds_generators_in_gems? 50 | rails3? || rails_version =~ /^2\./ 51 | end 52 | 53 | def environment_path 54 | File.join(rails_root, 'config', 'environment.rb') 55 | end 56 | 57 | def rakefile_path 58 | File.join(rails_root, 'Rakefile') 59 | end 60 | 61 | def bundle_gem(gem_name, version = nil) 62 | File.open(gemfile_path, 'a') do |file| 63 | gem = "gem '#{gem_name}'" 64 | gem += ", '#{version}'" if version 65 | file.puts(gem) 66 | end 67 | end 68 | 69 | def config_gem(gem_name, version = nil) 70 | run = "Rails::Initializer.run do |config|" 71 | insert = " config.gem '#{gem_name}'" 72 | insert += ", :version => '#{version}'" if version 73 | content = File.read(environment_path) 74 | content = "require 'thread'\n#{content}" 75 | if content.sub!(run, "#{run}\n#{insert}") 76 | File.open(environment_path, 'wb') { |file| file.write(content) } 77 | else 78 | raise "Couldn't find #{run.inspect} in #{environment_path}" 79 | end 80 | end 81 | 82 | def config_gem_dependencies 83 | insert = <<-END 84 | if Gem::VERSION >= "1.3.6" 85 | module Rails 86 | class GemDependency 87 | def requirement 88 | r = super 89 | (r == Gem::Requirement.default) ? nil : r 90 | end 91 | end 92 | end 93 | end 94 | END 95 | run = "Rails::Initializer.run do |config|" 96 | content = File.read(environment_path) 97 | if content.sub!(run, "#{insert}\n#{run}") 98 | File.open(environment_path, 'wb') { |file| file.write(content) } 99 | else 100 | raise "Couldn't find #{run.inspect} in #{environment_path}" 101 | end 102 | end 103 | 104 | def require_thread 105 | content = File.read(rakefile_path) 106 | content = "require 'thread'\n#{content}" 107 | File.open(rakefile_path, 'wb') { |file| file.write(content) } 108 | end 109 | 110 | def perform_request(uri, environment = 'production') 111 | if rails3? 112 | request_script = <<-SCRIPT 113 | require 'config/environment' 114 | 115 | env = Rack::MockRequest.env_for(#{uri.inspect}) 116 | response = RailsRoot::Application.call(env).last 117 | 118 | if response.is_a?(Array) 119 | puts response.join 120 | else 121 | puts response.body 122 | end 123 | SCRIPT 124 | File.open(File.join(rails_root, 'request.rb'), 'w') { |file| file.write(request_script) } 125 | @terminal.cd(rails_root) 126 | @terminal.run("ruby -rthread ./script/rails runner -e #{environment} request.rb") 127 | elsif rails_uses_rack? 128 | request_script = <<-SCRIPT 129 | require 'config/environment' 130 | 131 | env = Rack::MockRequest.env_for(#{uri.inspect}) 132 | app = Rack::Lint.new(ActionController::Dispatcher.new) 133 | 134 | status, headers, body = app.call(env) 135 | 136 | response = "" 137 | if body.respond_to?(:to_str) 138 | response << body 139 | else 140 | body.each { |part| response << part } 141 | end 142 | 143 | puts response 144 | SCRIPT 145 | File.open(File.join(rails_root, 'request.rb'), 'w') { |file| file.write(request_script) } 146 | @terminal.cd(rails_root) 147 | @terminal.run("ruby -rthread ./script/runner -e #{environment} request.rb") 148 | else 149 | uri = URI.parse(uri) 150 | request_script = <<-SCRIPT 151 | require 'cgi' 152 | class CGIWrapper < CGI 153 | def initialize(*args) 154 | @env_table = {} 155 | @stdinput = $stdin 156 | super(*args) 157 | end 158 | attr_reader :env_table 159 | end 160 | $stdin = StringIO.new("") 161 | cgi = CGIWrapper.new 162 | cgi.env_table.update({ 163 | 'HTTPS' => 'off', 164 | 'REQUEST_METHOD' => "GET", 165 | 'HTTP_HOST' => #{[uri.host, uri.port].join(':').inspect}, 166 | 'SERVER_PORT' => #{uri.port.inspect}, 167 | 'REQUEST_URI' => #{uri.request_uri.inspect}, 168 | 'PATH_INFO' => #{uri.path.inspect}, 169 | 'QUERY_STRING' => #{uri.query.inspect} 170 | }) 171 | require 'dispatcher' unless defined?(ActionController::Dispatcher) 172 | Dispatcher.dispatch(cgi) 173 | SCRIPT 174 | File.open(File.join(rails_root, 'request.rb'), 'w') { |file| file.write(request_script) } 175 | @terminal.cd(rails_root) 176 | @terminal.run("ruby -rthread ./script/runner -e #{environment} request.rb") 177 | end 178 | end 179 | end 180 | 181 | World(RailsHelpers) 182 | -------------------------------------------------------------------------------- /test/backtrace_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class BacktraceTest < Test::Unit::TestCase 4 | 5 | should "parse a backtrace into lines" do 6 | array = [ 7 | "app/models/user.rb:13:in `magic'", 8 | "app/controllers/users_controller.rb:8:in `index'" 9 | ] 10 | 11 | backtrace = Airbrake::Backtrace.parse(array) 12 | 13 | line = backtrace.lines.first 14 | assert_equal '13', line.number 15 | assert_equal 'app/models/user.rb', line.file 16 | assert_equal 'magic', line.method 17 | 18 | line = backtrace.lines.last 19 | assert_equal '8', line.number 20 | assert_equal 'app/controllers/users_controller.rb', line.file 21 | assert_equal 'index', line.method 22 | end 23 | 24 | should "parse a windows backtrace into lines" do 25 | array = [ 26 | "C:/Program Files/Server/app/models/user.rb:13:in `magic'", 27 | "C:/Program Files/Server/app/controllers/users_controller.rb:8:in `index'" 28 | ] 29 | 30 | backtrace = Airbrake::Backtrace.parse(array) 31 | 32 | line = backtrace.lines.first 33 | assert_equal '13', line.number 34 | assert_equal 'C:/Program Files/Server/app/models/user.rb', line.file 35 | assert_equal 'magic', line.method 36 | 37 | line = backtrace.lines.last 38 | assert_equal '8', line.number 39 | assert_equal 'C:/Program Files/Server/app/controllers/users_controller.rb', line.file 40 | assert_equal 'index', line.method 41 | end 42 | 43 | should "be equal with equal lines" do 44 | one = build_backtrace_array 45 | two = one.dup 46 | assert_equal one, two 47 | 48 | assert_equal Airbrake::Backtrace.parse(one), Airbrake::Backtrace.parse(two) 49 | end 50 | 51 | should "parse massive one-line exceptions into multiple lines" do 52 | original_backtrace = Airbrake::Backtrace. 53 | parse(["one:1:in `one'\n two:2:in `two'\n three:3:in `three`"]) 54 | expected_backtrace = Airbrake::Backtrace. 55 | parse(["one:1:in `one'", "two:2:in `two'", "three:3:in `three`"]) 56 | 57 | assert_equal expected_backtrace, original_backtrace 58 | end 59 | 60 | context "with a project root" do 61 | setup do 62 | @project_root = '/some/path' 63 | Airbrake.configure {|config| config.project_root = @project_root } 64 | end 65 | 66 | teardown do 67 | reset_config 68 | end 69 | 70 | should "filter out the project root" do 71 | backtrace_with_root = Airbrake::Backtrace.parse( 72 | ["#{@project_root}/app/models/user.rb:7:in `latest'", 73 | "#{@project_root}/app/controllers/users_controller.rb:13:in `index'", 74 | "/lib/something.rb:41:in `open'"], 75 | :filters => default_filters) 76 | backtrace_without_root = Airbrake::Backtrace.parse( 77 | ["[PROJECT_ROOT]/app/models/user.rb:7:in `latest'", 78 | "[PROJECT_ROOT]/app/controllers/users_controller.rb:13:in `index'", 79 | "/lib/something.rb:41:in `open'"]) 80 | 81 | assert_equal backtrace_without_root, backtrace_with_root 82 | end 83 | end 84 | 85 | context "with a project root equals to a part of file name" do 86 | setup do 87 | # Heroku-like 88 | @project_root = '/app' 89 | Airbrake.configure {|config| config.project_root = @project_root } 90 | end 91 | 92 | teardown do 93 | reset_config 94 | end 95 | 96 | should "filter out the project root" do 97 | backtrace_with_root = Airbrake::Backtrace.parse( 98 | ["#{@project_root}/app/models/user.rb:7:in `latest'", 99 | "#{@project_root}/app/controllers/users_controller.rb:13:in `index'", 100 | "/lib/something.rb:41:in `open'"], 101 | :filters => default_filters) 102 | backtrace_without_root = Airbrake::Backtrace.parse( 103 | ["[PROJECT_ROOT]/app/models/user.rb:7:in `latest'", 104 | "[PROJECT_ROOT]/app/controllers/users_controller.rb:13:in `index'", 105 | "/lib/something.rb:41:in `open'"]) 106 | 107 | assert_equal backtrace_without_root, backtrace_with_root 108 | end 109 | end 110 | 111 | context "with a blank project root" do 112 | setup do 113 | Airbrake.configure {|config| config.project_root = '' } 114 | end 115 | 116 | teardown do 117 | reset_config 118 | end 119 | 120 | should "not filter line numbers with respect to any project root" do 121 | backtrace = ["/app/models/user.rb:7:in `latest'", 122 | "/app/controllers/users_controller.rb:13:in `index'", 123 | "/lib/something.rb:41:in `open'"] 124 | 125 | backtrace_with_root = 126 | Airbrake::Backtrace.parse(backtrace, :filters => default_filters) 127 | 128 | backtrace_without_root = 129 | Airbrake::Backtrace.parse(backtrace) 130 | 131 | assert_equal backtrace_without_root, backtrace_with_root 132 | end 133 | end 134 | 135 | should "remove notifier trace" do 136 | inside_notifier = ['lib/airbrake.rb:13:in `voodoo`'] 137 | outside_notifier = ['users_controller:8:in `index`'] 138 | 139 | without_inside = Airbrake::Backtrace.parse(outside_notifier) 140 | with_inside = Airbrake::Backtrace.parse(inside_notifier + outside_notifier, 141 | :filters => default_filters) 142 | 143 | assert_equal without_inside, with_inside 144 | end 145 | 146 | should "run filters on the backtrace" do 147 | filters = [lambda { |line| line.sub('foo', 'bar') }] 148 | input = Airbrake::Backtrace.parse(["foo:13:in `one'", "baz:14:in `two'"], 149 | :filters => filters) 150 | expected = Airbrake::Backtrace.parse(["bar:13:in `one'", "baz:14:in `two'"]) 151 | assert_equal expected, input 152 | end 153 | 154 | def build_backtrace_array 155 | ["app/models/user.rb:13:in `magic'", 156 | "app/controllers/users_controller.rb:8:in `index'"] 157 | end 158 | 159 | def default_filters 160 | Airbrake::Configuration::DEFAULT_BACKTRACE_FILTERS 161 | end 162 | 163 | end 164 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rubygems/package_task' 4 | begin 5 | require 'cucumber/rake/task' 6 | rescue LoadError 7 | $stderr.puts "Please install cucumber: `gem install cucumber`" 8 | exit 1 9 | end 10 | 11 | desc 'Default: run unit tests.' 12 | task :default => [:test, "cucumber:rails:all"] 13 | 14 | desc "Clean out the tmp directory" 15 | task :clean do 16 | exec "rm -rf tmp" 17 | end 18 | 19 | desc 'Test the airbrake gem.' 20 | Rake::TestTask.new(:test) do |t| 21 | t.libs << 'lib' 22 | t.pattern = 'test/**/*_test.rb' 23 | t.verbose = true 24 | end 25 | 26 | namespace :changeling do 27 | desc "Bumps the version by a minor or patch version, depending on what was passed in." 28 | task :bump, :part do |t, args| 29 | # Thanks, Jeweler! 30 | if Airbrake::VERSION =~ /^(\d+)\.(\d+)\.(\d+)(?:\.(.*?))?$/ 31 | major = $1.to_i 32 | minor = $2.to_i 33 | patch = $3.to_i 34 | build = $4 35 | else 36 | abort 37 | end 38 | 39 | case args[:part] 40 | when /minor/ 41 | minor += 1 42 | patch = 0 43 | when /patch/ 44 | patch += 1 45 | else 46 | abort 47 | end 48 | 49 | version = [major, minor, patch, build].compact.join('.') 50 | 51 | File.open(File.join("lib", "airbrake", "notifier", "version.rb"), "w") do |f| 52 | f.write < 1.3.0)" 85 | task :minor do |t| 86 | Rake::Task['changeling:bump'].invoke(t.name) 87 | Rake::Task['changeling:change'].invoke 88 | end 89 | 90 | desc "Bump by a patch version, (1.2.3 => 1.2.4)" 91 | task :patch do |t| 92 | Rake::Task['changeling:bump'].invoke(t.name) 93 | Rake::Task['changeling:change'].invoke 94 | end 95 | 96 | desc "Push the latest version and tags" 97 | task :push do |t| 98 | system("git push origin master") 99 | system("git push origin $(git tag | tail -1)") 100 | end 101 | end 102 | 103 | begin 104 | require 'yard' 105 | YARD::Rake::YardocTask.new do |t| 106 | t.files = ['lib/**/*.rb', 'TESTING.rdoc'] 107 | end 108 | rescue LoadError 109 | end 110 | 111 | GEM_ROOT = File.dirname(__FILE__).freeze 112 | 113 | desc "Clean files generated by rake tasks" 114 | task :clobber => [:clobber_rdoc, :clobber_package] 115 | 116 | LOCAL_GEM_ROOT = File.join(GEM_ROOT, 'tmp', 'local_gems').freeze 117 | RAILS_VERSIONS = IO.read('SUPPORTED_RAILS_VERSIONS').strip.split("\n") 118 | LOCAL_GEMS = [['sham_rack', nil], ['capistrano', nil], ['sqlite3-ruby', nil], ['sinatra', nil], ['rake', '0.8.7']] + 119 | RAILS_VERSIONS.collect { |version| ['rails', version] } 120 | 121 | desc "Vendor test gems: Run this once to prepare your test environment" 122 | task :vendor_test_gems do 123 | old_gem_path = ENV['GEM_PATH'] 124 | old_gem_home = ENV['GEM_HOME'] 125 | ENV['GEM_PATH'] = LOCAL_GEM_ROOT 126 | ENV['GEM_HOME'] = LOCAL_GEM_ROOT 127 | LOCAL_GEMS.each do |gem_name, version| 128 | gem_file_pattern = [gem_name, version || '*'].compact.join('-') 129 | version_option = version ? "-v #{version}" : '' 130 | pattern = File.join(LOCAL_GEM_ROOT, 'gems', "#{gem_file_pattern}") 131 | existing = Dir.glob(pattern).first 132 | unless existing 133 | command = "gem install -i #{LOCAL_GEM_ROOT} --no-ri --no-rdoc --backtrace #{version_option} #{gem_name}" 134 | puts "Vendoring #{gem_file_pattern}..." 135 | unless system("#{command} 2>&1") 136 | puts "Command failed: #{command}" 137 | exit(1) 138 | end 139 | end 140 | end 141 | ENV['GEM_PATH'] = old_gem_path 142 | ENV['GEM_HOME'] = old_gem_home 143 | end 144 | 145 | Cucumber::Rake::Task.new(:cucumber) do |t| 146 | t.fork = true 147 | t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'progress')] 148 | end 149 | 150 | task :cucumber => [:vendor_test_gems] 151 | 152 | def run_rails_cucumbr_task(version, additional_cucumber_args) 153 | puts "Testing Rails #{version}" 154 | if version.empty? 155 | raise "No Rails version specified - make sure ENV['RAILS_VERSION'] is set, e.g. with `rake cucumber:rails:all`" 156 | end 157 | ENV['RAILS_VERSION'] = version 158 | system("cucumber --format #{ENV['CUCUMBER_FORMAT'] || 'progress'} #{additional_cucumber_args} features/rails.feature features/rails_with_js_notifier.feature") 159 | end 160 | 161 | def define_rails_cucumber_tasks(additional_cucumber_args = '') 162 | namespace :rails do 163 | RAILS_VERSIONS.each do |version| 164 | desc "Test integration of the gem with Rails #{version}" 165 | task version => [:vendor_test_gems] do 166 | exit 1 unless run_rails_cucumbr_task(version, additional_cucumber_args) 167 | end 168 | end 169 | 170 | desc "Test integration of the gem with all Rails versions" 171 | task :all do 172 | results = RAILS_VERSIONS.map do |version| 173 | run_rails_cucumbr_task(version, additional_cucumber_args) 174 | end 175 | 176 | exit 1 unless results.all? 177 | end 178 | end 179 | end 180 | 181 | namespace :cucumber do 182 | namespace :wip do 183 | define_rails_cucumber_tasks('--tags @wip') 184 | end 185 | 186 | define_rails_cucumber_tasks 187 | end 188 | 189 | -------------------------------------------------------------------------------- /test/airbrake_tasks_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | require 'rubygems' 3 | 4 | require File.dirname(__FILE__) + '/../lib/airbrake_tasks' 5 | require 'fakeweb' 6 | 7 | FakeWeb.allow_net_connect = false 8 | 9 | class AirbrakeTasksTest < Test::Unit::TestCase 10 | def successful_response(body = "") 11 | response = Net::HTTPSuccess.new('1.2', '200', 'OK') 12 | response.stubs(:body).returns(body) 13 | return response 14 | end 15 | 16 | def unsuccessful_response(body = "") 17 | response = Net::HTTPClientError.new('1.2', '200', 'OK') 18 | response.stubs(:body).returns(body) 19 | return response 20 | end 21 | 22 | context "being quiet" do 23 | setup { AirbrakeTasks.stubs(:puts) } 24 | 25 | context "in a configured project" do 26 | setup { Airbrake.configure { |config| config.api_key = "1234123412341234" } } 27 | 28 | context "on deploy({})" do 29 | setup { @output = AirbrakeTasks.deploy({}) } 30 | 31 | before_should "complain about missing rails env" do 32 | AirbrakeTasks.expects(:puts).with(regexp_matches(/rails environment/i)) 33 | end 34 | 35 | should "return false" do 36 | assert !@output 37 | end 38 | end 39 | 40 | context "given an optional HTTP proxy and valid options" do 41 | setup do 42 | @response = stub("response", :body => "stub body") 43 | @http_proxy = stub("proxy", :post_form => @response) 44 | 45 | Net::HTTP.expects(:Proxy). 46 | with(Airbrake.configuration.proxy_host, 47 | Airbrake.configuration.proxy_port, 48 | Airbrake.configuration.proxy_user, 49 | Airbrake.configuration.proxy_pass). 50 | returns(@http_proxy) 51 | 52 | @options = { :rails_env => "staging", :dry_run => false } 53 | end 54 | 55 | context "performing a dry run" do 56 | setup { @output = AirbrakeTasks.deploy(@options.merge(:dry_run => true)) } 57 | 58 | should "return true without performing any actual request" do 59 | assert_equal true, @output 60 | assert_received(@http_proxy, :post_form) do|expects| 61 | expects.never 62 | end 63 | end 64 | end 65 | 66 | context "on deploy(options)" do 67 | setup do 68 | @output = AirbrakeTasks.deploy(@options) 69 | end 70 | 71 | before_should "post to http://airbrakeapp.com/deploys.txt" do 72 | URI.stubs(:parse).with('http://airbrakeapp.com/deploys.txt').returns(:uri) 73 | @http_proxy.expects(:post_form).with(:uri, kind_of(Hash)).returns(successful_response) 74 | end 75 | 76 | before_should "use the project api key" do 77 | @http_proxy.expects(:post_form). 78 | with(kind_of(URI), has_entries('api_key' => "1234123412341234")). 79 | returns(successful_response) 80 | end 81 | 82 | before_should "use send the rails_env param" do 83 | @http_proxy.expects(:post_form). 84 | with(kind_of(URI), has_entries("deploy[rails_env]" => "staging")). 85 | returns(successful_response) 86 | end 87 | 88 | [:local_username, :scm_repository, :scm_revision].each do |key| 89 | before_should "use send the #{key} param if it's passed in." do 90 | @options[key] = "value" 91 | @http_proxy.expects(:post_form). 92 | with(kind_of(URI), has_entries("deploy[#{key}]" => "value")). 93 | returns(successful_response) 94 | end 95 | end 96 | 97 | before_should "use the :api_key param if it's passed in." do 98 | @options[:api_key] = "value" 99 | @http_proxy.expects(:post_form). 100 | with(kind_of(URI), has_entries("api_key" => "value")). 101 | returns(successful_response) 102 | end 103 | 104 | before_should "puts the response body on success" do 105 | AirbrakeTasks.expects(:puts).with("body") 106 | @http_proxy.expects(:post_form).with(any_parameters).returns(successful_response('body')) 107 | end 108 | 109 | before_should "puts the response body on failure" do 110 | AirbrakeTasks.expects(:puts).with("body") 111 | @http_proxy.expects(:post_form).with(any_parameters).returns(unsuccessful_response('body')) 112 | end 113 | 114 | should "return false on failure", :before => lambda { 115 | @http_proxy.expects(:post_form).with(any_parameters).returns(unsuccessful_response('body')) 116 | } do 117 | assert !@output 118 | end 119 | 120 | should "return true on success", :before => lambda { 121 | @http_proxy.expects(:post_form).with(any_parameters).returns(successful_response('body')) 122 | } do 123 | assert @output 124 | end 125 | end 126 | end 127 | end 128 | 129 | context "in a configured project with custom host" do 130 | setup do 131 | Airbrake.configure do |config| 132 | config.api_key = "1234123412341234" 133 | config.host = "custom.host" 134 | end 135 | end 136 | 137 | context "on deploy(:rails_env => 'staging')" do 138 | setup { @output = AirbrakeTasks.deploy(:rails_env => "staging") } 139 | 140 | before_should "post to the custom host" do 141 | URI.stubs(:parse).with('http://custom.host/deploys.txt').returns(:uri) 142 | Net::HTTP.expects(:post_form).with(:uri, kind_of(Hash)).returns(successful_response) 143 | end 144 | end 145 | end 146 | 147 | context "when not configured" do 148 | setup { Airbrake.configure { |config| config.api_key = "" } } 149 | 150 | context "on deploy(:rails_env => 'staging')" do 151 | setup { @output = AirbrakeTasks.deploy(:rails_env => "staging") } 152 | 153 | before_should "complain about missing api key" do 154 | AirbrakeTasks.expects(:puts).with(regexp_matches(/api key/i)) 155 | end 156 | 157 | should "return false" do 158 | assert !@output 159 | end 160 | end 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/sender_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class SenderTest < Test::Unit::TestCase 4 | 5 | def setup 6 | reset_config 7 | end 8 | 9 | def build_sender(opts = {}) 10 | config = Airbrake::Configuration.new 11 | opts.each {|opt, value| config.send(:"#{opt}=", value) } 12 | Airbrake::Sender.new(config) 13 | end 14 | 15 | def send_exception(args = {}) 16 | notice = args.delete(:notice) || build_notice_data 17 | sender = args.delete(:sender) || build_sender(args) 18 | sender.send_to_airbrake(notice) 19 | end 20 | 21 | def stub_http(options = {}) 22 | response = stub(:body => options[:body] || 'body') 23 | http = stub(:post => response, 24 | :read_timeout= => nil, 25 | :open_timeout= => nil, 26 | :ca_file= => nil, 27 | :verify_mode= => nil, 28 | :use_ssl= => nil) 29 | Net::HTTP.stubs(:new => http) 30 | http 31 | end 32 | 33 | should "post to Airbrake when using an HTTP proxy" do 34 | response = stub(:body => 'body') 35 | http = stub(:post => response, 36 | :read_timeout= => nil, 37 | :open_timeout= => nil, 38 | :use_ssl= => nil) 39 | proxy = stub(:new => http) 40 | Net::HTTP.stubs(:Proxy => proxy) 41 | 42 | url = "http://airbrakeapp.com:80#{Airbrake::Sender::NOTICES_URI}" 43 | uri = URI.parse(url) 44 | 45 | proxy_host = 'some.host' 46 | proxy_port = 88 47 | proxy_user = 'login' 48 | proxy_pass = 'passwd' 49 | 50 | send_exception(:proxy_host => proxy_host, 51 | :proxy_port => proxy_port, 52 | :proxy_user => proxy_user, 53 | :proxy_pass => proxy_pass) 54 | assert_received(http, :post) do |expect| 55 | expect.with(uri.path, anything, Airbrake::HEADERS) 56 | end 57 | assert_received(Net::HTTP, :Proxy) do |expect| 58 | expect.with(proxy_host, proxy_port, proxy_user, proxy_pass) 59 | end 60 | end 61 | 62 | should "return the created group's id on successful posting" do 63 | http = stub_http(:body => '3799307') 64 | assert_equal "3799307", send_exception(:secure => false) 65 | end 66 | 67 | should "return nil on failed posting" do 68 | http = stub_http 69 | http.stubs(:post).raises(Errno::ECONNREFUSED) 70 | assert_equal nil, send_exception(:secure => false) 71 | end 72 | 73 | should "not fail when posting and a timeout exception occurs" do 74 | http = stub_http 75 | http.stubs(:post).raises(TimeoutError) 76 | assert_nothing_thrown do 77 | send_exception(:secure => false) 78 | end 79 | end 80 | 81 | should "not fail when posting and a connection refused exception occurs" do 82 | http = stub_http 83 | http.stubs(:post).raises(Errno::ECONNREFUSED) 84 | assert_nothing_thrown do 85 | send_exception(:secure => false) 86 | end 87 | end 88 | 89 | should "not fail when posting any http exception occurs" do 90 | http = stub_http 91 | Airbrake::Sender::HTTP_ERRORS.each do |error| 92 | http.stubs(:post).raises(error) 93 | assert_nothing_thrown do 94 | send_exception(:secure => false) 95 | end 96 | end 97 | end 98 | 99 | should "post to the right url for non-ssl" do 100 | http = stub_http 101 | url = "http://airbrakeapp.com:80#{Airbrake::Sender::NOTICES_URI}" 102 | uri = URI.parse(url) 103 | send_exception(:secure => false) 104 | assert_received(http, :post) {|expect| expect.with(uri.path, anything, Airbrake::HEADERS) } 105 | end 106 | 107 | should "post to the right path for ssl" do 108 | http = stub_http 109 | send_exception(:secure => true) 110 | assert_received(http, :post) {|expect| expect.with(Airbrake::Sender::NOTICES_URI, anything, Airbrake::HEADERS) } 111 | end 112 | 113 | should "verify the SSL peer when the use_ssl option is set to true" do 114 | url = "https://airbrakeapp.com#{Airbrake::Sender::NOTICES_URI}" 115 | uri = URI.parse(url) 116 | 117 | real_http = Net::HTTP.new(uri.host, uri.port) 118 | real_http.stubs(:post => nil) 119 | proxy = stub(:new => real_http) 120 | Net::HTTP.stubs(:Proxy => proxy) 121 | File.stubs(:exist?).with(OpenSSL::X509::DEFAULT_CERT_FILE).returns(false) 122 | 123 | send_exception(:secure => true) 124 | assert(real_http.use_ssl) 125 | assert_equal(OpenSSL::SSL::VERIFY_PEER, real_http.verify_mode) 126 | assert_nil real_http.ca_file 127 | end 128 | 129 | should "verify the SSL peer when the use_ssl option is set to true and the default cert exists" do 130 | url = "https://airbrakeapp.com#{Airbrake::Sender::NOTICES_URI}" 131 | uri = URI.parse(url) 132 | 133 | real_http = Net::HTTP.new(uri.host, uri.port) 134 | real_http.stubs(:post => nil) 135 | proxy = stub(:new => real_http) 136 | Net::HTTP.stubs(:Proxy => proxy) 137 | File.stubs(:exist?).with(OpenSSL::X509::DEFAULT_CERT_FILE).returns(true) 138 | 139 | send_exception(:secure => true) 140 | assert(real_http.use_ssl) 141 | assert_equal(OpenSSL::SSL::VERIFY_PEER, real_http.verify_mode) 142 | assert_equal(OpenSSL::X509::DEFAULT_CERT_FILE, real_http.ca_file) 143 | end 144 | 145 | should "default the open timeout to 2 seconds" do 146 | http = stub_http 147 | send_exception 148 | assert_received(http, :open_timeout=) {|expect| expect.with(2) } 149 | end 150 | 151 | should "default the read timeout to 5 seconds" do 152 | http = stub_http 153 | send_exception 154 | assert_received(http, :read_timeout=) {|expect| expect.with(5) } 155 | end 156 | 157 | should "allow override of the open timeout" do 158 | http = stub_http 159 | send_exception(:http_open_timeout => 4) 160 | assert_received(http, :open_timeout=) {|expect| expect.with(4) } 161 | end 162 | 163 | should "allow override of the read timeout" do 164 | http = stub_http 165 | send_exception(:http_read_timeout => 10) 166 | assert_received(http, :read_timeout=) {|expect| expect.with(10) } 167 | end 168 | 169 | should "connect to the right port for ssl" do 170 | stub_http 171 | send_exception(:secure => true) 172 | assert_received(Net::HTTP, :new) {|expect| expect.with("airbrakeapp.com", 443) } 173 | end 174 | 175 | should "connect to the right port for non-ssl" do 176 | stub_http 177 | send_exception(:secure => false) 178 | assert_received(Net::HTTP, :new) {|expect| expect.with("airbrakeapp.com", 80) } 179 | end 180 | 181 | should "use ssl if secure" do 182 | stub_http 183 | send_exception(:secure => true, :host => 'example.org') 184 | assert_received(Net::HTTP, :new) {|expect| expect.with('example.org', 443) } 185 | end 186 | 187 | should "not use ssl if not secure" do 188 | stub_http 189 | send_exception(:secure => false, :host => 'example.org') 190 | assert_received(Net::HTTP, :new) {|expect| expect.with('example.org', 80) } 191 | end 192 | 193 | end 194 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'rubygems' 3 | 4 | $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")) 5 | 6 | require 'thread' 7 | 8 | require "bundler/setup" 9 | 10 | require 'shoulda' 11 | require 'mocha' 12 | 13 | require 'action_controller' 14 | require 'action_controller/test_process' 15 | require 'active_record' 16 | require 'active_support' 17 | require 'nokogiri' 18 | require 'rack' 19 | require 'bourne' 20 | require 'sham_rack' 21 | 22 | require "airbrake" 23 | 24 | begin require 'redgreen'; rescue LoadError; end 25 | 26 | module TestMethods 27 | def rescue_action e 28 | raise e 29 | end 30 | 31 | def do_raise 32 | raise "Airbrake" 33 | end 34 | 35 | def do_not_raise 36 | render :text => "Success" 37 | end 38 | 39 | def do_raise_ignored 40 | raise ActiveRecord::RecordNotFound.new("404") 41 | end 42 | 43 | def do_raise_not_ignored 44 | raise ActiveRecord::StatementInvalid.new("Statement invalid") 45 | end 46 | 47 | def manual_notify 48 | notify_airbrake(Exception.new) 49 | render :text => "Success" 50 | end 51 | 52 | def manual_notify_ignored 53 | notify_airbrake(ActiveRecord::RecordNotFound.new("404")) 54 | render :text => "Success" 55 | end 56 | end 57 | 58 | class AirbrakeController < ActionController::Base 59 | include TestMethods 60 | end 61 | 62 | class Test::Unit::TestCase 63 | def request(action = nil, method = :get, user_agent = nil, params = {}) 64 | @request = ActionController::TestRequest.new 65 | @request.action = action ? action.to_s : "" 66 | 67 | if user_agent 68 | if @request.respond_to?(:user_agent=) 69 | @request.user_agent = user_agent 70 | else 71 | @request.env["HTTP_USER_AGENT"] = user_agent 72 | end 73 | end 74 | @request.query_parameters = @request.query_parameters.merge(params) 75 | @response = ActionController::TestResponse.new 76 | @controller.process(@request, @response) 77 | end 78 | 79 | # Borrowed from ActiveSupport 2.3.2 80 | def assert_difference(expression, difference = 1, message = nil, &block) 81 | b = block.send(:binding) 82 | exps = Array.wrap(expression) 83 | before = exps.map { |e| eval(e, b) } 84 | 85 | yield 86 | 87 | exps.each_with_index do |e, i| 88 | error = "#{e.inspect} didn't change by #{difference}" 89 | error = "#{message}.\n#{error}" if message 90 | assert_equal(before[i] + difference, eval(e, b), error) 91 | end 92 | end 93 | 94 | def assert_no_difference(expression, message = nil, &block) 95 | assert_difference expression, 0, message, &block 96 | end 97 | 98 | def stub_sender 99 | stub('sender', :send_to_airbrake => nil) 100 | end 101 | 102 | def stub_sender! 103 | Airbrake.sender = stub_sender 104 | end 105 | 106 | def stub_notice 107 | stub('notice', :to_xml => 'some yaml', :ignore? => false) 108 | end 109 | 110 | def stub_notice! 111 | stub_notice.tap do |notice| 112 | Airbrake::Notice.stubs(:new => notice) 113 | end 114 | end 115 | 116 | def create_dummy 117 | Airbrake::DummySender.new 118 | end 119 | 120 | def reset_config 121 | Airbrake.configuration = nil 122 | Airbrake.configure do |config| 123 | config.api_key = 'abc123' 124 | end 125 | end 126 | 127 | def clear_backtrace_filters 128 | Airbrake.configuration.backtrace_filters.clear 129 | end 130 | 131 | def build_exception(opts = {}) 132 | backtrace = ["airbrake/test/helper.rb:132:in `build_exception'", 133 | "airbrake/test/backtrace.rb:4:in `build_notice_data'", 134 | "/var/lib/gems/1.8/gems/airbrake-2.4.5/rails/init.rb:2:in `send_exception'"] 135 | opts = {:backtrace => backtrace}.merge(opts) 136 | BacktracedException.new(opts) 137 | end 138 | 139 | class BacktracedException < Exception 140 | attr_accessor :backtrace 141 | def initialize(opts) 142 | @backtrace = opts[:backtrace] 143 | end 144 | def set_backtrace(bt) 145 | @backtrace = bt 146 | end 147 | end 148 | 149 | def build_notice_data(exception = nil) 150 | exception ||= build_exception 151 | { 152 | :api_key => 'abc123', 153 | :error_class => exception.class.name, 154 | :error_message => "#{exception.class.name}: #{exception.message}", 155 | :backtrace => exception.backtrace, 156 | :environment => { 'PATH' => '/bin', 'REQUEST_URI' => '/users/1' }, 157 | :request => { 158 | :params => { 'controller' => 'users', 'action' => 'show', 'id' => '1' }, 159 | :rails_root => '/path/to/application', 160 | :url => "http://test.host/users/1" 161 | }, 162 | :session => { 163 | :key => '123abc', 164 | :data => { 'user_id' => '5', 'flash' => { 'notice' => 'Logged in successfully' } } 165 | } 166 | } 167 | end 168 | 169 | def assert_caught_and_sent 170 | assert !Airbrake.sender.collected.empty? 171 | end 172 | 173 | def assert_caught_and_not_sent 174 | assert Airbrake.sender.collected.empty? 175 | end 176 | 177 | def assert_array_starts_with(expected, actual) 178 | assert_respond_to actual, :to_ary 179 | array = actual.to_ary.reverse 180 | expected.reverse.each_with_index do |value, i| 181 | assert_equal value, array[i] 182 | end 183 | end 184 | 185 | def assert_valid_node(document, xpath, content) 186 | nodes = document.xpath(xpath) 187 | assert nodes.any?{|node| node.content == content }, 188 | "Expected xpath #{xpath} to have content #{content}, " + 189 | "but found #{nodes.map { |n| n.content }} in #{nodes.size} matching nodes." + 190 | "Document:\n#{document.to_s}" 191 | end 192 | end 193 | 194 | module DefinesConstants 195 | def setup 196 | @defined_constants = [] 197 | end 198 | 199 | def teardown 200 | @defined_constants.each do |constant| 201 | Object.__send__(:remove_const, constant) 202 | end 203 | end 204 | 205 | def define_constant(name, value) 206 | Object.const_set(name, value) 207 | @defined_constants << name 208 | end 209 | end 210 | 211 | # Also stolen from AS 2.3.2 212 | class Array 213 | # Wraps the object in an Array unless it's an Array. Converts the 214 | # object to an Array using #to_ary if it implements that. 215 | def self.wrap(object) 216 | case object 217 | when nil 218 | [] 219 | when self 220 | object 221 | else 222 | if object.respond_to?(:to_ary) 223 | object.to_ary 224 | else 225 | [object] 226 | end 227 | end 228 | end 229 | 230 | end 231 | 232 | class CollectingSender 233 | attr_reader :collected 234 | 235 | def initialize 236 | @collected = [] 237 | end 238 | 239 | def send_to_airbrake(data) 240 | @collected << data 241 | end 242 | end 243 | 244 | class FakeLogger 245 | def info(*args); end 246 | def debug(*args); end 247 | def warn(*args); end 248 | def error(*args); end 249 | def fatal(*args); end 250 | end 251 | 252 | -------------------------------------------------------------------------------- /test/notifier_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class NotifierTest < Test::Unit::TestCase 4 | 5 | class OriginalException < Exception 6 | end 7 | 8 | class ContinuedException < Exception 9 | end 10 | 11 | include DefinesConstants 12 | 13 | def setup 14 | super 15 | reset_config 16 | end 17 | 18 | def assert_sent(notice, notice_args) 19 | assert_received(Airbrake::Notice, :new) {|expect| expect.with(has_entries(notice_args)) } 20 | assert_received(notice, :to_xml) 21 | assert_received(Airbrake.sender, :send_to_airbrake) {|expect| expect.with(notice.to_xml) } 22 | end 23 | 24 | def set_public_env 25 | Airbrake.configure { |config| config.environment_name = 'production' } 26 | end 27 | 28 | def set_development_env 29 | Airbrake.configure { |config| config.environment_name = 'development' } 30 | end 31 | 32 | should "yield and save a configuration when configuring" do 33 | yielded_configuration = nil 34 | Airbrake.configure do |config| 35 | yielded_configuration = config 36 | end 37 | 38 | assert_kind_of Airbrake::Configuration, yielded_configuration 39 | assert_equal yielded_configuration, Airbrake.configuration 40 | end 41 | 42 | should "not remove existing config options when configuring twice" do 43 | first_config = nil 44 | Airbrake.configure do |config| 45 | first_config = config 46 | end 47 | Airbrake.configure do |config| 48 | assert_equal first_config, config 49 | end 50 | end 51 | 52 | should "configure the sender" do 53 | sender = stub_sender 54 | Airbrake::Sender.stubs(:new => sender) 55 | configuration = nil 56 | 57 | Airbrake.configure { |yielded_config| configuration = yielded_config } 58 | 59 | assert_received(Airbrake::Sender, :new) { |expect| expect.with(configuration) } 60 | assert_equal sender, Airbrake.sender 61 | end 62 | 63 | should "create and send a notice for an exception" do 64 | set_public_env 65 | exception = build_exception 66 | stub_sender! 67 | notice = stub_notice! 68 | 69 | Airbrake.notify(exception) 70 | 71 | assert_sent notice, :exception => exception 72 | end 73 | 74 | should "create and send a notice for a hash" do 75 | set_public_env 76 | notice = stub_notice! 77 | notice_args = { :error_message => 'uh oh' } 78 | stub_sender! 79 | 80 | Airbrake.notify(notice_args) 81 | 82 | assert_sent(notice, notice_args) 83 | end 84 | 85 | should "create and sent a notice for an exception and hash" do 86 | set_public_env 87 | exception = build_exception 88 | notice = stub_notice! 89 | notice_args = { :error_message => 'uh oh' } 90 | stub_sender! 91 | 92 | Airbrake.notify(exception, notice_args) 93 | 94 | assert_sent(notice, notice_args.merge(:exception => exception)) 95 | end 96 | 97 | should "not create a notice in a development environment" do 98 | set_development_env 99 | sender = stub_sender! 100 | 101 | Airbrake.notify(build_exception) 102 | Airbrake.notify_or_ignore(build_exception) 103 | 104 | assert_received(sender, :send_to_airbrake) {|expect| expect.never } 105 | end 106 | 107 | should "not deliver an ignored exception when notifying implicitly" do 108 | set_public_env 109 | exception = build_exception 110 | sender = stub_sender! 111 | notice = stub_notice! 112 | notice.stubs(:ignore? => true) 113 | 114 | Airbrake.notify_or_ignore(exception) 115 | 116 | assert_received(sender, :send_to_airbrake) {|expect| expect.never } 117 | end 118 | 119 | should "deliver an ignored exception when notifying manually" do 120 | set_public_env 121 | exception = build_exception 122 | sender = stub_sender! 123 | notice = stub_notice! 124 | notice.stubs(:ignore? => true) 125 | 126 | Airbrake.notify(exception) 127 | 128 | assert_sent(notice, :exception => exception) 129 | end 130 | 131 | should "pass config to created notices" do 132 | exception = build_exception 133 | config_opts = { 'one' => 'two', 'three' => 'four' } 134 | notice = stub_notice! 135 | stub_sender! 136 | Airbrake.configuration = stub('config', :merge => config_opts, :public? => true) 137 | 138 | Airbrake.notify(exception) 139 | 140 | assert_received(Airbrake::Notice, :new) do |expect| 141 | expect.with(has_entries(config_opts)) 142 | end 143 | end 144 | 145 | context "building notice JSON for an exception" do 146 | setup do 147 | @params = { :controller => "users", :action => "create" } 148 | @exception = build_exception 149 | @hash = Airbrake.build_lookup_hash_for(@exception, @params) 150 | end 151 | 152 | should "set action" do 153 | assert_equal @params[:action], @hash[:action] 154 | end 155 | 156 | should "set controller" do 157 | assert_equal @params[:controller], @hash[:component] 158 | end 159 | 160 | should "set line number" do 161 | assert @hash[:line_number] =~ /\d+/ 162 | end 163 | 164 | should "set file" do 165 | assert_match /test\/helper\.rb$/, @hash[:file] 166 | end 167 | 168 | should "set rails_env to production" do 169 | assert_equal 'production', @hash[:environment_name] 170 | end 171 | 172 | should "set error class" do 173 | assert_equal @exception.class.to_s, @hash[:error_class] 174 | end 175 | 176 | should "not set file or line number with no backtrace" do 177 | @exception.stubs(:backtrace).returns([]) 178 | 179 | @hash = Airbrake.build_lookup_hash_for(@exception) 180 | 181 | assert_nil @hash[:line_number] 182 | assert_nil @hash[:file] 183 | end 184 | 185 | should "not set action or controller when not provided" do 186 | @hash = Airbrake.build_lookup_hash_for(@exception) 187 | 188 | assert_nil @hash[:action] 189 | assert_nil @hash[:controller] 190 | end 191 | 192 | context "when an exception that provides #original_exception is raised" do 193 | setup do 194 | @exception.stubs(:original_exception).returns(begin 195 | raise NotifierTest::OriginalException.new 196 | rescue Exception => e 197 | e 198 | end) 199 | end 200 | 201 | should "unwrap exceptions that provide #original_exception" do 202 | @hash = Airbrake.build_lookup_hash_for(@exception) 203 | assert_equal "NotifierTest::OriginalException", @hash[:error_class] 204 | end 205 | end 206 | 207 | context "when an exception that provides #continued_exception is raised" do 208 | setup do 209 | @exception.stubs(:continued_exception).returns(begin 210 | raise NotifierTest::ContinuedException.new 211 | rescue Exception => e 212 | e 213 | end) 214 | end 215 | 216 | should "unwrap exceptions that provide #continued_exception" do 217 | @hash = Airbrake.build_lookup_hash_for(@exception) 218 | assert_equal "NotifierTest::ContinuedException", @hash[:error_class] 219 | end 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /test/configuration_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class ConfigurationTest < Test::Unit::TestCase 4 | 5 | include DefinesConstants 6 | 7 | should "provide default values" do 8 | assert_config_default :proxy_host, nil 9 | assert_config_default :proxy_port, nil 10 | assert_config_default :proxy_user, nil 11 | assert_config_default :proxy_pass, nil 12 | assert_config_default :project_root, nil 13 | assert_config_default :environment_name, nil 14 | assert_config_default :logger, nil 15 | assert_config_default :notifier_version, Airbrake::VERSION 16 | assert_config_default :notifier_name, 'Airbrake Notifier' 17 | assert_config_default :notifier_url, 'http://airbrakeapp.com' 18 | assert_config_default :secure, false 19 | assert_config_default :host, 'airbrakeapp.com' 20 | assert_config_default :http_open_timeout, 2 21 | assert_config_default :http_read_timeout, 5 22 | assert_config_default :ignore_by_filters, [] 23 | assert_config_default :ignore_user_agent, [] 24 | assert_config_default :params_filters, 25 | Airbrake::Configuration::DEFAULT_PARAMS_FILTERS 26 | assert_config_default :backtrace_filters, 27 | Airbrake::Configuration::DEFAULT_BACKTRACE_FILTERS 28 | assert_config_default :ignore, 29 | Airbrake::Configuration::IGNORE_DEFAULT 30 | assert_config_default :development_lookup, true 31 | assert_config_default :framework, 'Standalone' 32 | end 33 | 34 | should "provide default values for secure connections" do 35 | config = Airbrake::Configuration.new 36 | config.secure = true 37 | assert_equal 443, config.port 38 | assert_equal 'https', config.protocol 39 | end 40 | 41 | should "provide default values for insecure connections" do 42 | config = Airbrake::Configuration.new 43 | config.secure = false 44 | assert_equal 80, config.port 45 | assert_equal 'http', config.protocol 46 | end 47 | 48 | should "not cache inferred ports" do 49 | config = Airbrake::Configuration.new 50 | config.secure = false 51 | config.port 52 | config.secure = true 53 | assert_equal 443, config.port 54 | end 55 | 56 | should "allow values to be overwritten" do 57 | assert_config_overridable :proxy_host 58 | assert_config_overridable :proxy_port 59 | assert_config_overridable :proxy_user 60 | assert_config_overridable :proxy_pass 61 | assert_config_overridable :secure 62 | assert_config_overridable :host 63 | assert_config_overridable :port 64 | assert_config_overridable :http_open_timeout 65 | assert_config_overridable :http_read_timeout 66 | assert_config_overridable :project_root 67 | assert_config_overridable :notifier_version 68 | assert_config_overridable :notifier_name 69 | assert_config_overridable :notifier_url 70 | assert_config_overridable :environment_name 71 | assert_config_overridable :development_lookup 72 | assert_config_overridable :logger 73 | end 74 | 75 | should "have an api key" do 76 | assert_config_overridable :api_key 77 | end 78 | 79 | should "act like a hash" do 80 | config = Airbrake::Configuration.new 81 | hash = config.to_hash 82 | [:api_key, :backtrace_filters, :development_environments, 83 | :environment_name, :host, :http_open_timeout, 84 | :http_read_timeout, :ignore, :ignore_by_filters, :ignore_user_agent, 85 | :notifier_name, :notifier_url, :notifier_version, :params_filters, 86 | :project_root, :port, :protocol, :proxy_host, :proxy_pass, :proxy_port, 87 | :proxy_user, :secure, :development_lookup].each do |option| 88 | assert_equal config[option], hash[option], "Wrong value for #{option}" 89 | end 90 | end 91 | 92 | should "be mergable" do 93 | config = Airbrake::Configuration.new 94 | hash = config.to_hash 95 | assert_equal hash.merge(:key => 'value'), config.merge(:key => 'value') 96 | end 97 | 98 | should "allow param filters to be appended" do 99 | assert_appends_value :params_filters 100 | end 101 | 102 | should "warn when attempting to read environment filters" do 103 | config = Airbrake::Configuration.new 104 | config. 105 | expects(:warn). 106 | with(regexp_matches(/deprecated/i)) 107 | assert_equal [], config.environment_filters 108 | end 109 | 110 | should "warn when attempting to write js_notifier" do 111 | config = Airbrake::Configuration.new 112 | config. 113 | expects(:warn). 114 | with(regexp_matches(/deprecated/i)) 115 | config.js_notifier = true 116 | end 117 | 118 | should "allow ignored user agents to be appended" do 119 | assert_appends_value :ignore_user_agent 120 | end 121 | 122 | should "allow backtrace filters to be appended" do 123 | assert_appends_value(:backtrace_filters) do |config| 124 | new_filter = lambda {} 125 | config.filter_backtrace(&new_filter) 126 | new_filter 127 | end 128 | end 129 | 130 | should "allow ignore by filters to be appended" do 131 | assert_appends_value(:ignore_by_filters) do |config| 132 | new_filter = lambda {} 133 | config.ignore_by_filter(&new_filter) 134 | new_filter 135 | end 136 | end 137 | 138 | should "allow ignored exceptions to be appended" do 139 | config = Airbrake::Configuration.new 140 | original_filters = config.ignore.dup 141 | new_filter = 'hello' 142 | config.ignore << new_filter 143 | assert_same_elements original_filters + [new_filter], config.ignore 144 | end 145 | 146 | should "allow ignored exceptions to be replaced" do 147 | assert_replaces(:ignore, :ignore_only=) 148 | end 149 | 150 | should "allow ignored user agents to be replaced" do 151 | assert_replaces(:ignore_user_agent, :ignore_user_agent_only=) 152 | end 153 | 154 | should "use development and test as development environments by default" do 155 | config = Airbrake::Configuration.new 156 | assert_same_elements %w(development test cucumber), config.development_environments 157 | end 158 | 159 | should "be public in a public environment" do 160 | config = Airbrake::Configuration.new 161 | config.development_environments = %w(development) 162 | config.environment_name = 'production' 163 | assert config.public? 164 | end 165 | 166 | should "not be public in a development environment" do 167 | config = Airbrake::Configuration.new 168 | config.development_environments = %w(staging) 169 | config.environment_name = 'staging' 170 | assert !config.public? 171 | end 172 | 173 | should "be public without an environment name" do 174 | config = Airbrake::Configuration.new 175 | assert config.public? 176 | end 177 | 178 | should "use the assigned logger if set" do 179 | config = Airbrake::Configuration.new 180 | config.logger = "CUSTOM LOGGER" 181 | assert_equal "CUSTOM LOGGER", config.logger 182 | end 183 | 184 | should 'give a new instance if non defined' do 185 | Airbrake.configuration = nil 186 | assert_kind_of Airbrake::Configuration, Airbrake.configuration 187 | end 188 | 189 | def assert_config_default(option, default_value, config = nil) 190 | config ||= Airbrake::Configuration.new 191 | assert_equal default_value, config.send(option) 192 | end 193 | 194 | def assert_config_overridable(option, value = 'a value') 195 | config = Airbrake::Configuration.new 196 | config.send(:"#{option}=", value) 197 | assert_equal value, config.send(option) 198 | end 199 | 200 | def assert_appends_value(option, &block) 201 | config = Airbrake::Configuration.new 202 | original_values = config.send(option).dup 203 | block ||= lambda do |config| 204 | new_value = 'hello' 205 | config.send(option) << new_value 206 | new_value 207 | end 208 | new_value = block.call(config) 209 | assert_same_elements original_values + [new_value], config.send(option) 210 | end 211 | 212 | def assert_replaces(option, setter) 213 | config = Airbrake::Configuration.new 214 | new_value = 'hello' 215 | config.send(setter, [new_value]) 216 | assert_equal [new_value], config.send(option) 217 | config.send(setter, new_value) 218 | assert_equal [new_value], config.send(option) 219 | end 220 | 221 | end 222 | -------------------------------------------------------------------------------- /lib/airbrake/configuration.rb: -------------------------------------------------------------------------------- 1 | module Airbrake 2 | # Used to set up and modify settings for the notifier. 3 | class Configuration 4 | 5 | OPTIONS = [:api_key, :backtrace_filters, :development_environments, 6 | :development_lookup, :environment_name, :host, 7 | :http_open_timeout, :http_read_timeout, :ignore, :ignore_by_filters, 8 | :ignore_user_agent, :notifier_name, :notifier_url, :notifier_version, 9 | :params_filters, :project_root, :port, :protocol, :proxy_host, 10 | :proxy_pass, :proxy_port, :proxy_user, :secure, :framework, 11 | :user_information, :rescue_rake_exceptions].freeze 12 | 13 | # The API key for your project, found on the project edit form. 14 | attr_accessor :api_key 15 | 16 | # The host to connect to (defaults to airbrakeapp.com). 17 | attr_accessor :host 18 | 19 | # The port on which your Airbrake server runs (defaults to 443 for secure 20 | # connections, 80 for insecure connections). 21 | attr_accessor :port 22 | 23 | # +true+ for https connections, +false+ for http connections. 24 | attr_accessor :secure 25 | 26 | # The HTTP open timeout in seconds (defaults to 2). 27 | attr_accessor :http_open_timeout 28 | 29 | # The HTTP read timeout in seconds (defaults to 5). 30 | attr_accessor :http_read_timeout 31 | 32 | # The hostname of your proxy server (if using a proxy) 33 | attr_accessor :proxy_host 34 | 35 | # The port of your proxy server (if using a proxy) 36 | attr_accessor :proxy_port 37 | 38 | # The username to use when logging into your proxy server (if using a proxy) 39 | attr_accessor :proxy_user 40 | 41 | # The password to use when logging into your proxy server (if using a proxy) 42 | attr_accessor :proxy_pass 43 | 44 | # A list of parameters that should be filtered out of what is sent to Airbrake. 45 | # By default, all "password" attributes will have their contents replaced. 46 | attr_reader :params_filters 47 | 48 | # A list of filters for cleaning and pruning the backtrace. See #filter_backtrace. 49 | attr_reader :backtrace_filters 50 | 51 | # A list of filters for ignoring exceptions. See #ignore_by_filter. 52 | attr_reader :ignore_by_filters 53 | 54 | # A list of exception classes to ignore. The array can be appended to. 55 | attr_reader :ignore 56 | 57 | # A list of user agents that are being ignored. The array can be appended to. 58 | attr_reader :ignore_user_agent 59 | 60 | # A list of environments in which notifications should not be sent. 61 | attr_accessor :development_environments 62 | 63 | # +true+ if you want to check for production errors matching development errors, +false+ otherwise. 64 | attr_accessor :development_lookup 65 | 66 | # The name of the environment the application is running in 67 | attr_accessor :environment_name 68 | 69 | # The path to the project in which the error occurred, such as the RAILS_ROOT 70 | attr_accessor :project_root 71 | 72 | # The name of the notifier library being used to send notifications (such as "Airbrake Notifier") 73 | attr_accessor :notifier_name 74 | 75 | # The version of the notifier library being used to send notifications (such as "1.0.2") 76 | attr_accessor :notifier_version 77 | 78 | # The url of the notifier library being used to send notifications 79 | attr_accessor :notifier_url 80 | 81 | # The logger used by Airbrake 82 | attr_accessor :logger 83 | 84 | # The text that the placeholder is replaced with. {{error_id}} is the actual error number. 85 | attr_accessor :user_information 86 | 87 | # The framework Airbrake is configured to use 88 | attr_accessor :framework 89 | 90 | # Should Airbrake catch exceptions from Rake tasks? 91 | # (boolean or nil; set to nil to catch exceptions when rake isn't running from a terminal; default is nil) 92 | attr_accessor :rescue_rake_exceptions 93 | 94 | DEFAULT_PARAMS_FILTERS = %w(password password_confirmation).freeze 95 | 96 | DEFAULT_BACKTRACE_FILTERS = [ 97 | lambda { |line| 98 | if defined?(Airbrake.configuration.project_root) && Airbrake.configuration.project_root.to_s != '' 99 | line.sub(/#{Airbrake.configuration.project_root}/, "[PROJECT_ROOT]") 100 | else 101 | line 102 | end 103 | }, 104 | lambda { |line| line.gsub(/^\.\//, "") }, 105 | lambda { |line| 106 | if defined?(Gem) 107 | Gem.path.inject(line) do |line, path| 108 | line.gsub(/#{path}/, "[GEM_ROOT]") 109 | end 110 | end 111 | }, 112 | lambda { |line| line if line !~ %r{lib/airbrake} } 113 | ].freeze 114 | 115 | IGNORE_DEFAULT = ['ActiveRecord::RecordNotFound', 116 | 'ActionController::RoutingError', 117 | 'ActionController::InvalidAuthenticityToken', 118 | 'CGI::Session::CookieStore::TamperedWithCookie', 119 | 'ActionController::UnknownAction', 120 | 'AbstractController::ActionNotFound'] 121 | 122 | alias_method :secure?, :secure 123 | 124 | def initialize 125 | @secure = false 126 | @host = 'airbrakeapp.com' 127 | @http_open_timeout = 2 128 | @http_read_timeout = 5 129 | @params_filters = DEFAULT_PARAMS_FILTERS.dup 130 | @backtrace_filters = DEFAULT_BACKTRACE_FILTERS.dup 131 | @ignore_by_filters = [] 132 | @ignore = IGNORE_DEFAULT.dup 133 | @ignore_user_agent = [] 134 | @development_environments = %w(development test cucumber) 135 | @development_lookup = true 136 | @notifier_name = 'Airbrake Notifier' 137 | @notifier_version = VERSION 138 | @notifier_url = 'http://airbrakeapp.com' 139 | @framework = 'Standalone' 140 | @user_information = 'Airbrake Error {{error_id}}' 141 | @rescue_rake_exceptions = nil 142 | end 143 | 144 | # Takes a block and adds it to the list of backtrace filters. When the filters 145 | # run, the block will be handed each line of the backtrace and can modify 146 | # it as necessary. 147 | # 148 | # @example 149 | # config.filter_bracktrace do |line| 150 | # line.gsub(/^#{Rails.root}/, "[RAILS_ROOT]") 151 | # end 152 | # 153 | # @param [Proc] block The new backtrace filter. 154 | # @yieldparam [String] line A line in the backtrace. 155 | def filter_backtrace(&block) 156 | self.backtrace_filters << block 157 | end 158 | 159 | # Takes a block and adds it to the list of ignore filters. 160 | # When the filters run, the block will be handed the exception. 161 | # @example 162 | # config.ignore_by_filter do |exception_data| 163 | # true if exception_data[:error_class] == "RuntimeError" 164 | # end 165 | # 166 | # @param [Proc] block The new ignore filter 167 | # @yieldparam [Hash] data The exception data given to +Airbrake.notify+ 168 | # @yieldreturn [Boolean] If the block returns true the exception will be ignored, otherwise it will be processed by airbrake. 169 | def ignore_by_filter(&block) 170 | self.ignore_by_filters << block 171 | end 172 | 173 | # Overrides the list of default ignored errors. 174 | # 175 | # @param [Array] names A list of exceptions to ignore. 176 | def ignore_only=(names) 177 | @ignore = [names].flatten 178 | end 179 | 180 | # Overrides the list of default ignored user agents 181 | # 182 | # @param [Array] A list of user agents to ignore 183 | def ignore_user_agent_only=(names) 184 | @ignore_user_agent = [names].flatten 185 | end 186 | 187 | # Allows config options to be read like a hash 188 | # 189 | # @param [Symbol] option Key for a given attribute 190 | def [](option) 191 | send(option) 192 | end 193 | 194 | # Returns a hash of all configurable options 195 | def to_hash 196 | OPTIONS.inject({}) do |hash, option| 197 | hash.merge(option.to_sym => send(option)) 198 | end 199 | end 200 | 201 | # Returns a hash of all configurable options merged with +hash+ 202 | # 203 | # @param [Hash] hash A set of configuration options that will take precedence over the defaults 204 | def merge(hash) 205 | to_hash.merge(hash) 206 | end 207 | 208 | # Determines if the notifier will send notices. 209 | # @return [Boolean] Returns +false+ if in a development environment, +true+ otherwise. 210 | def public? 211 | !development_environments.include?(environment_name) 212 | end 213 | 214 | def port 215 | @port || default_port 216 | end 217 | 218 | def protocol 219 | if secure? 220 | 'https' 221 | else 222 | 'http' 223 | end 224 | end 225 | 226 | def js_notifier=(*args) 227 | warn '[AIRBRAKE] config.js_notifier has been deprecated and has no effect. You should use <%= airbrake_javascript_notifier %> directly at the top of your layouts. Be sure to place it before all other javascript.' 228 | end 229 | 230 | def environment_filters 231 | warn 'config.environment_filters has been deprecated and has no effect.' 232 | [] 233 | end 234 | 235 | private 236 | 237 | def default_port 238 | if secure? 239 | 443 240 | else 241 | 80 242 | end 243 | end 244 | 245 | end 246 | 247 | end 248 | -------------------------------------------------------------------------------- /lib/airbrake/notice.rb: -------------------------------------------------------------------------------- 1 | require 'builder' 2 | require 'socket' 3 | 4 | module Airbrake 5 | class Notice 6 | 7 | # The exception that caused this notice, if any 8 | attr_reader :exception 9 | 10 | # The API key for the project to which this notice should be sent 11 | attr_reader :api_key 12 | 13 | # The backtrace from the given exception or hash. 14 | attr_reader :backtrace 15 | 16 | # The name of the class of error (such as RuntimeError) 17 | attr_reader :error_class 18 | 19 | # The name of the server environment (such as "production") 20 | attr_reader :environment_name 21 | 22 | # CGI variables such as HTTP_METHOD 23 | attr_reader :cgi_data 24 | 25 | # The message from the exception, or a general description of the error 26 | attr_reader :error_message 27 | 28 | # See Configuration#backtrace_filters 29 | attr_reader :backtrace_filters 30 | 31 | # See Configuration#params_filters 32 | attr_reader :params_filters 33 | 34 | # A hash of parameters from the query string or post body. 35 | attr_reader :parameters 36 | alias_method :params, :parameters 37 | 38 | # The component (if any) which was used in this request (usually the controller) 39 | attr_reader :component 40 | alias_method :controller, :component 41 | 42 | # The action (if any) that was called in this request 43 | attr_reader :action 44 | 45 | # A hash of session data from the request 46 | attr_reader :session_data 47 | 48 | # The path to the project that caused the error (usually RAILS_ROOT) 49 | attr_reader :project_root 50 | 51 | # The URL at which the error occurred (if any) 52 | attr_reader :url 53 | 54 | # See Configuration#ignore 55 | attr_reader :ignore 56 | 57 | # See Configuration#ignore_by_filters 58 | attr_reader :ignore_by_filters 59 | 60 | # The name of the notifier library sending this notice, such as "Airbrake Notifier" 61 | attr_reader :notifier_name 62 | 63 | # The version number of the notifier library sending this notice, such as "2.1.3" 64 | attr_reader :notifier_version 65 | 66 | # A URL for more information about the notifier library sending this notice 67 | attr_reader :notifier_url 68 | 69 | # The host name where this error occurred (if any) 70 | attr_reader :hostname 71 | 72 | def initialize(args) 73 | self.args = args 74 | self.exception = args[:exception] 75 | self.api_key = args[:api_key] 76 | self.project_root = args[:project_root] 77 | self.url = args[:url] || rack_env(:url) 78 | 79 | self.notifier_name = args[:notifier_name] 80 | self.notifier_version = args[:notifier_version] 81 | self.notifier_url = args[:notifier_url] 82 | 83 | self.ignore = args[:ignore] || [] 84 | self.ignore_by_filters = args[:ignore_by_filters] || [] 85 | self.backtrace_filters = args[:backtrace_filters] || [] 86 | self.params_filters = args[:params_filters] || [] 87 | self.parameters = args[:parameters] || 88 | action_dispatch_params || 89 | rack_env(:params) || 90 | {} 91 | self.component = args[:component] || args[:controller] || parameters['controller'] 92 | self.action = args[:action] || parameters['action'] 93 | 94 | self.environment_name = args[:environment_name] 95 | self.cgi_data = args[:cgi_data] || args[:rack_env] 96 | self.backtrace = Backtrace.parse(exception_attribute(:backtrace, caller), :filters => self.backtrace_filters) 97 | self.error_class = exception_attribute(:error_class) {|exception| exception.class.name } 98 | self.error_message = exception_attribute(:error_message, 'Notification') do |exception| 99 | "#{exception.class.name}: #{exception.message}" 100 | end 101 | 102 | self.hostname = local_hostname 103 | 104 | also_use_rack_params_filters 105 | find_session_data 106 | clean_params 107 | clean_rack_request_data 108 | end 109 | 110 | # Converts the given notice to XML 111 | def to_xml 112 | builder = Builder::XmlMarkup.new 113 | builder.instruct! 114 | xml = builder.notice(:version => Airbrake::API_VERSION) do |notice| 115 | notice.tag!("api-key", api_key) 116 | notice.notifier do |notifier| 117 | notifier.name(notifier_name) 118 | notifier.version(notifier_version) 119 | notifier.url(notifier_url) 120 | end 121 | notice.error do |error| 122 | error.tag!('class', error_class) 123 | error.message(error_message) 124 | error.backtrace do |backtrace| 125 | self.backtrace.lines.each do |line| 126 | backtrace.line(:number => line.number, 127 | :file => line.file, 128 | :method => line.method) 129 | end 130 | end 131 | end 132 | if url || 133 | controller || 134 | action || 135 | !parameters.blank? || 136 | !cgi_data.blank? || 137 | !session_data.blank? 138 | notice.request do |request| 139 | request.url(url) 140 | request.component(controller) 141 | request.action(action) 142 | unless parameters.nil? || parameters.empty? 143 | request.params do |params| 144 | xml_vars_for(params, parameters) 145 | end 146 | end 147 | unless session_data.nil? || session_data.empty? 148 | request.session do |session| 149 | xml_vars_for(session, session_data) 150 | end 151 | end 152 | unless cgi_data.nil? || cgi_data.empty? 153 | request.tag!("cgi-data") do |cgi_datum| 154 | xml_vars_for(cgi_datum, cgi_data) 155 | end 156 | end 157 | end 158 | end 159 | notice.tag!("server-environment") do |env| 160 | env.tag!("project-root", project_root) 161 | env.tag!("environment-name", environment_name) 162 | env.tag!("hostname", hostname) 163 | end 164 | end 165 | xml.to_s 166 | end 167 | 168 | # Determines if this notice should be ignored 169 | def ignore? 170 | ignored_class_names.include?(error_class) || 171 | ignore_by_filters.any? {|filter| filter.call(self) } 172 | end 173 | 174 | # Allows properties to be accessed using a hash-like syntax 175 | # 176 | # @example 177 | # notice[:error_message] 178 | # @param [String] method The given key for an attribute 179 | # @return The attribute value, or self if given +:request+ 180 | def [](method) 181 | case method 182 | when :request 183 | self 184 | else 185 | send(method) 186 | end 187 | end 188 | 189 | private 190 | 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 | 200 | # Gets a property named +attribute+ of an exception, either from an actual 201 | # exception or a hash. 202 | # 203 | # If an exception is available, #from_exception will be used. Otherwise, 204 | # a key named +attribute+ will be used from the #args. 205 | # 206 | # If no exception or hash key is available, +default+ will be used. 207 | def exception_attribute(attribute, default = nil, &block) 208 | (exception && from_exception(attribute, &block)) || args[attribute] || default 209 | end 210 | 211 | # Gets a property named +attribute+ from an exception. 212 | # 213 | # If a block is given, it will be used when getting the property from an 214 | # exception. The block should accept and exception and return the value for 215 | # the property. 216 | # 217 | # If no block is given, a method with the same name as +attribute+ will be 218 | # invoked for the value. 219 | def from_exception(attribute) 220 | if block_given? 221 | yield(exception) 222 | else 223 | exception.send(attribute) 224 | end 225 | end 226 | 227 | # Removes non-serializable data from the given attribute. 228 | # See #clean_unserializable_data 229 | def clean_unserializable_data_from(attribute) 230 | self.send(:"#{attribute}=", clean_unserializable_data(send(attribute))) 231 | end 232 | 233 | # Removes non-serializable data. Allowed data types are strings, arrays, 234 | # and hashes. All other types are converted to strings. 235 | # TODO: move this onto Hash 236 | def clean_unserializable_data(data, stack = []) 237 | return "[possible infinite recursion halted]" if stack.any?{|item| item == data.object_id } 238 | 239 | if data.respond_to?(:to_hash) 240 | data.to_hash.inject({}) do |result, (key, value)| 241 | result.merge(key => clean_unserializable_data(value, stack + [data.object_id])) 242 | end 243 | elsif data.respond_to?(:to_ary) 244 | data.collect do |value| 245 | clean_unserializable_data(value, stack + [data.object_id]) 246 | end 247 | else 248 | data.to_s 249 | end 250 | end 251 | 252 | # Replaces the contents of params that match params_filters. 253 | # TODO: extract this to a different class 254 | def clean_params 255 | clean_unserializable_data_from(:parameters) 256 | filter(parameters) 257 | if cgi_data 258 | clean_unserializable_data_from(:cgi_data) 259 | filter(cgi_data) 260 | end 261 | if session_data 262 | clean_unserializable_data_from(:session_data) 263 | filter(session_data) 264 | end 265 | end 266 | 267 | def clean_rack_request_data 268 | if cgi_data 269 | cgi_data.delete("rack.request.form_vars") 270 | end 271 | end 272 | 273 | def filter(hash) 274 | if params_filters 275 | hash.each do |key, value| 276 | if filter_key?(key) 277 | hash[key] = "[FILTERED]" 278 | elsif value.respond_to?(:to_hash) 279 | filter(hash[key]) 280 | end 281 | end 282 | end 283 | end 284 | 285 | def filter_key?(key) 286 | params_filters.any? do |filter| 287 | key.to_s.include?(filter.to_s) 288 | end 289 | end 290 | 291 | def find_session_data 292 | self.session_data = args[:session_data] || args[:session] || rack_session || {} 293 | self.session_data = session_data[:data] if session_data[:data] 294 | end 295 | 296 | # Converts the mixed class instances and class names into just names 297 | # TODO: move this into Configuration or another class 298 | def ignored_class_names 299 | ignore.collect do |string_or_class| 300 | if string_or_class.respond_to?(:name) 301 | string_or_class.name 302 | else 303 | string_or_class 304 | end 305 | end 306 | end 307 | 308 | def xml_vars_for(builder, hash) 309 | hash.each do |key, value| 310 | if value.respond_to?(:to_hash) 311 | builder.var(:key => key){|b| xml_vars_for(b, value.to_hash) } 312 | else 313 | builder.var(value.to_s, :key => key) 314 | end 315 | end 316 | end 317 | 318 | def rack_env(method) 319 | rack_request.send(method) if rack_request 320 | end 321 | 322 | def rack_request 323 | @rack_request ||= if args[:rack_env] 324 | ::Rack::Request.new(args[:rack_env]) 325 | end 326 | end 327 | 328 | def action_dispatch_params 329 | args[:rack_env]['action_dispatch.request.parameters'] if args[:rack_env] 330 | end 331 | 332 | def rack_session 333 | args[:rack_env]['rack.session'] if args[:rack_env] 334 | end 335 | 336 | def also_use_rack_params_filters 337 | if args[:rack_env] 338 | @params_filters ||= [] 339 | @params_filters += rack_request.env["action_dispatch.parameter_filter"] || [] 340 | end 341 | end 342 | 343 | def local_hostname 344 | Socket.gethostname 345 | end 346 | 347 | end 348 | end 349 | -------------------------------------------------------------------------------- /features/rails.feature: -------------------------------------------------------------------------------- 1 | Feature: Install the Gem in a Rails application 2 | 3 | Background: 4 | Given I have built and installed the "airbrake" gem 5 | 6 | Scenario: Use the gem without vendoring the gem in a Rails application 7 | When I generate a new Rails application 8 | And I configure the Airbrake shim 9 | And I configure my application to require the "airbrake" gem 10 | And I run the airbrake generator with "-k myapikey" 11 | Then the command should have run successfully 12 | And I should receive a Airbrake notification 13 | And I should see the Rails version 14 | 15 | Scenario: vendor the gem and uninstall 16 | When I generate a new Rails application 17 | And I configure the Airbrake shim 18 | And I configure my application to require the "airbrake" gem 19 | And I unpack the "airbrake" gem 20 | And I run the airbrake generator with "-k myapikey" 21 | Then the command should have run successfully 22 | When I uninstall the "airbrake" gem 23 | And I install cached gems 24 | And I run "rake airbrake:test" 25 | Then I should see "** [Airbrake] Success: Net::HTTPOK" 26 | And I should receive two Airbrake notifications 27 | 28 | Scenario: Configure the notifier by hand 29 | When I generate a new Rails application 30 | And I configure the Airbrake shim 31 | And I configure the notifier to use "myapikey" as an API key 32 | And I configure my application to require the "airbrake" gem 33 | And I run the airbrake generator with "" 34 | Then I should receive a Airbrake notification 35 | 36 | Scenario: Configuration within initializer isn't overridden by Railtie 37 | When I generate a new Rails application 38 | And I configure the Airbrake shim 39 | And I configure my application to require the "airbrake" gem 40 | And I run the airbrake generator with "-k myapikey" 41 | Then the command should have run successfully 42 | When I configure the notifier to use the following configuration lines: 43 | """ 44 | config.api_key = "myapikey" 45 | config.project_root = "argle/bargle" 46 | """ 47 | And I define a response for "TestController#index": 48 | """ 49 | session[:value] = "test" 50 | raise RuntimeError, "some message" 51 | """ 52 | And I route "/test/index" to "test#index" 53 | And I perform a request to "http://example.com:123/test/index?param=value" 54 | Then I should receive the following Airbrake notification: 55 | | project-root | argle/bargle | 56 | 57 | Scenario: Try to install without an api key 58 | When I generate a new Rails application 59 | And I configure my application to require the "airbrake" gem 60 | And I run the airbrake generator with "" 61 | Then I should see "Must pass --api-key or --heroku or create config/initializers/airbrake.rb" 62 | 63 | Scenario: Configure and deploy using only installed gem 64 | When I generate a new Rails application 65 | And I run "capify ." 66 | And I configure the Airbrake shim 67 | And I configure my application to require the "airbrake" gem 68 | And I run the airbrake generator with "-k myapikey" 69 | And I run "cap -T" 70 | Then I should see "airbrake:deploy" 71 | 72 | Scenario: Configure and deploy using only vendored gem 73 | When I generate a new Rails application 74 | And I run "capify ." 75 | And I configure the Airbrake shim 76 | And I configure my application to require the "airbrake" gem 77 | And I unpack the "airbrake" gem 78 | And I run the airbrake generator with "-k myapikey" 79 | And I uninstall the "airbrake" gem 80 | And I install cached gems 81 | And I run "cap -T" 82 | Then I should see "airbrake:deploy" 83 | 84 | Scenario: Try to install when the airbrake plugin still exists 85 | When I generate a new Rails application 86 | And I install the "airbrake" plugin 87 | And I configure the Airbrake shim 88 | And I configure the notifier to use "myapikey" as an API key 89 | And I configure my application to require the "airbrake" gem 90 | And I run the airbrake generator with "" 91 | Then I should see "You must first remove the airbrake plugin. Please run: script/plugin remove airbrake" 92 | 93 | Scenario: Rescue an exception in a controller 94 | When I generate a new Rails application 95 | And I configure the Airbrake shim 96 | And I configure my application to require the "airbrake" gem 97 | And I run the airbrake generator with "-k myapikey" 98 | And I define a response for "TestController#index": 99 | """ 100 | session[:value] = "test" 101 | raise RuntimeError, "some message" 102 | """ 103 | And I route "/test/index" to "test#index" 104 | And I perform a request to "http://example.com:123/test/index?param=value" 105 | Then I should receive the following Airbrake notification: 106 | | component | test | 107 | | action | index | 108 | | error message | RuntimeError: some message | 109 | | error class | RuntimeError | 110 | | session | value: test | 111 | | parameters | param: value | 112 | | url | http://example.com:123/test/index?param=value | 113 | 114 | Scenario: The gem should not be considered a framework gem 115 | When I generate a new Rails application 116 | And I configure the Airbrake shim 117 | And I configure my application to require the "airbrake" gem 118 | And I run the airbrake generator with "-k myapikey" 119 | And I run "rake gems" 120 | Then I should see that "airbrake" is not considered a framework gem 121 | 122 | Scenario: The app uses Vlad instead of Capistrano 123 | When I generate a new Rails application 124 | And I configure the Airbrake shim 125 | And I configure my application to require the "airbrake" gem 126 | And I run "touch config/deploy.rb" 127 | And I run "rm Capfile" 128 | And I run the airbrake generator with "-k myapikey" 129 | Then "config/deploy.rb" should not contain "capistrano" 130 | 131 | Scenario: Support the Heroku addon in the generator 132 | When I generate a new Rails application 133 | And I configure the Airbrake shim 134 | And I configure the Heroku rake shim 135 | And I configure the Heroku gem shim with "myapikey" 136 | And I configure my application to require the "airbrake" gem 137 | And I run the airbrake generator with "--heroku" 138 | Then the command should have run successfully 139 | And I should receive a Airbrake notification 140 | And I should see the Rails version 141 | And my Airbrake configuration should contain the following line: 142 | """ 143 | config.api_key = ENV['HOPTOAD_API_KEY'] 144 | """ 145 | 146 | Scenario: Support the --app option for the Heroku addon in the generator 147 | When I generate a new Rails application 148 | And I configure the Airbrake shim 149 | And I configure the Heroku rake shim 150 | And I configure the Heroku gem shim with "myapikey" and multiple app support 151 | And I configure my application to require the "airbrake" gem 152 | And I run the airbrake generator with "--heroku -a myapp" 153 | Then the command should have run successfully 154 | And I should receive a Airbrake notification 155 | And I should see the Rails version 156 | And my Airbrake configuration should contain the following line: 157 | """ 158 | config.api_key = ENV['HOPTOAD_API_KEY'] 159 | """ 160 | 161 | Scenario: Filtering parameters in a controller 162 | When I generate a new Rails application 163 | And I configure the Airbrake shim 164 | And I configure my application to require the "airbrake" gem 165 | And I run the airbrake generator with "-k myapikey" 166 | When I configure the notifier to use the following configuration lines: 167 | """ 168 | config.api_key = "myapikey" 169 | config.params_filters << "credit_card_number" 170 | """ 171 | And I define a response for "TestController#index": 172 | """ 173 | params[:credit_card_number] = "red23" 174 | raise RuntimeError, "some message" 175 | """ 176 | And I route "/test/index" to "test#index" 177 | And I perform a request to "http://example.com:123/test/index?param=value" 178 | Then I should receive the following Airbrake notification: 179 | | component | test | 180 | | action | index | 181 | | error message | RuntimeError: some message | 182 | | error class | RuntimeError | 183 | | parameters | credit_card_number: [FILTERED] | 184 | | url | http://example.com:123/test/index?param=value | 185 | 186 | Scenario: Filtering session in a controller 187 | When I generate a new Rails application 188 | And I configure the Airbrake shim 189 | And I configure my application to require the "airbrake" gem 190 | And I run the airbrake generator with "-k myapikey" 191 | When I configure the notifier to use the following configuration lines: 192 | """ 193 | config.api_key = "myapikey" 194 | config.params_filters << "secret" 195 | """ 196 | And I define a response for "TestController#index": 197 | """ 198 | session["secret"] = "blue42" 199 | raise RuntimeError, "some message" 200 | """ 201 | And I route "/test/index" to "test#index" 202 | And I perform a request to "http://example.com:123/test/index?param=value" 203 | Then I should receive the following Airbrake notification: 204 | | component | test | 205 | | action | index | 206 | | error message | RuntimeError: some message | 207 | | error class | RuntimeError | 208 | | session | secret: [FILTERED] | 209 | | url | http://example.com:123/test/index?param=value | 210 | 211 | Scenario: Filtering session and params based on Rails parameter filters 212 | When I generate a new Rails application 213 | And I configure the Airbrake shim 214 | And I configure my application to require the "airbrake" gem 215 | And I run the airbrake generator with "-k myapikey" 216 | And I configure the application to filter parameter "secret" 217 | And I define a response for "TestController#index": 218 | """ 219 | params["secret"] = "red23" 220 | session["secret"] = "blue42" 221 | raise RuntimeError, "some message" 222 | """ 223 | And I route "/test/index" to "test#index" 224 | And I perform a request to "http://example.com:123/test/index?param=value" 225 | Then I should receive the following Airbrake notification: 226 | | component | test | 227 | | action | index | 228 | | error message | RuntimeError: some message | 229 | | error class | RuntimeError | 230 | | params | secret: [FILTERED] | 231 | | session | secret: [FILTERED] | 232 | | url | http://example.com:123/test/index?param=value | 233 | 234 | Scenario: Notify airbrake within the controller 235 | When I generate a new Rails application 236 | And I configure the Airbrake shim 237 | And I configure my application to require the "airbrake" gem 238 | And I run the airbrake generator with "-k myapikey" 239 | And I define a response for "TestController#index": 240 | """ 241 | session[:value] = "test" 242 | notify_airbrake(RuntimeError.new("some message")) 243 | render :nothing => true 244 | """ 245 | And I route "/test/index" to "test#index" 246 | And I perform a request to "http://example.com:123/test/index?param=value" 247 | Then I should receive the following Airbrake notification: 248 | | component | test | 249 | | action | index | 250 | | error message | RuntimeError: some message | 251 | | error class | RuntimeError | 252 | | session | value: test | 253 | | parameters | param: value | 254 | | url | http://example.com:123/test/index?param=value | 255 | -------------------------------------------------------------------------------- /test/catcher_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class ActionControllerCatcherTest < Test::Unit::TestCase 4 | 5 | include DefinesConstants 6 | 7 | def setup 8 | super 9 | reset_config 10 | Airbrake.sender = CollectingSender.new 11 | define_constant('RAILS_ROOT', '/path/to/rails/root') 12 | end 13 | 14 | def ignore(exception_class) 15 | Airbrake.configuration.ignore << exception_class 16 | end 17 | 18 | def build_controller_class(&definition) 19 | Class.new(ActionController::Base).tap do |klass| 20 | klass.__send__(:include, Airbrake::Rails::ActionControllerCatcher) 21 | klass.class_eval(&definition) if definition 22 | define_constant('AirbrakeTestController', klass) 23 | end 24 | end 25 | 26 | def assert_sent_hash(hash, xpath) 27 | hash.each do |key, value| 28 | next if key.match(/^airbrake\./) # We added this key. 29 | 30 | element_xpath = "#{xpath}/var[@key = '#{key}']" 31 | if value.respond_to?(:to_hash) 32 | assert_sent_hash value.to_hash, element_xpath 33 | else 34 | assert_sent_element value, element_xpath 35 | end 36 | end 37 | end 38 | 39 | def assert_sent_element(value, xpath) 40 | assert_valid_node last_sent_notice_document, xpath, stringify_array_elements(value).to_s 41 | end 42 | 43 | def stringify_array_elements(data) 44 | if data.respond_to?(:to_ary) 45 | data.collect do |value| 46 | stringify_array_elements(value) 47 | end 48 | else 49 | data.to_s 50 | end 51 | end 52 | 53 | def assert_sent_request_info_for(request) 54 | params = request.parameters.to_hash 55 | assert_sent_hash params, '/notice/request/params' 56 | assert_sent_element params['controller'], '/notice/request/component' 57 | assert_sent_element params['action'], '/notice/request/action' 58 | assert_sent_element url_from_request(request), '/notice/request/url' 59 | assert_sent_hash request.env, '/notice/request/cgi-data' 60 | end 61 | 62 | def url_from_request(request) 63 | url = "#{request.protocol}#{request.host}" 64 | 65 | unless [80, 443].include?(request.port) 66 | url << ":#{request.port}" 67 | end 68 | 69 | url << request.request_uri 70 | url 71 | end 72 | 73 | def sender 74 | Airbrake.sender 75 | end 76 | 77 | def last_sent_notice_xml 78 | sender.collected.last 79 | end 80 | 81 | def last_sent_notice_document 82 | assert_not_nil xml = last_sent_notice_xml, "No xml was sent" 83 | Nokogiri::XML.parse(xml) 84 | end 85 | 86 | def process_action(opts = {}, &action) 87 | opts[:request] ||= ActionController::TestRequest.new 88 | opts[:response] ||= ActionController::TestResponse.new 89 | klass = build_controller_class do 90 | cattr_accessor :local 91 | define_method(:index, &action) 92 | def local_request? 93 | local 94 | end 95 | end 96 | if opts[:filters] 97 | klass.filter_parameter_logging *opts[:filters] 98 | end 99 | if opts[:user_agent] 100 | if opts[:request].respond_to?(:user_agent=) 101 | opts[:request].user_agent = opts[:user_agent] 102 | else 103 | opts[:request].env["HTTP_USER_AGENT"] = opts[:user_agent] 104 | end 105 | end 106 | if opts[:port] 107 | opts[:request].port = opts[:port] 108 | end 109 | klass.consider_all_requests_local = opts[:all_local] 110 | klass.local = opts[:local] 111 | controller = klass.new 112 | controller.stubs(:rescue_action_in_public_without_airbrake) 113 | opts[:request].query_parameters = opts[:request].query_parameters.merge(opts[:params] || {}) 114 | opts[:request].session = ActionController::TestSession.new(opts[:session] || {}) 115 | # Prevents request.fullpath from crashing Rails in tests 116 | opts[:request].env['REQUEST_URI'] = opts[:request].request_uri 117 | controller.process(opts[:request], opts[:response]) 118 | controller 119 | end 120 | 121 | def process_action_with_manual_notification(args = {}) 122 | process_action(args) do 123 | notify_airbrake(:error_message => 'fail') 124 | # Rails will raise a template error if we don't render something 125 | render :nothing => true 126 | end 127 | end 128 | 129 | def process_action_with_automatic_notification(args = {}) 130 | process_action(args) { raise "Hello" } 131 | end 132 | 133 | should "deliver notices from exceptions raised in public requests" do 134 | process_action_with_automatic_notification 135 | assert_caught_and_sent 136 | end 137 | 138 | should "not deliver notices from exceptions in local requests" do 139 | process_action_with_automatic_notification(:local => true) 140 | assert_caught_and_not_sent 141 | end 142 | 143 | should "not deliver notices from exceptions when all requests are local" do 144 | process_action_with_automatic_notification(:all_local => true) 145 | assert_caught_and_not_sent 146 | end 147 | 148 | should "not deliver notices from actions that don't raise" do 149 | controller = process_action { render :text => 'Hello' } 150 | assert_caught_and_not_sent 151 | assert_equal 'Hello', controller.response.body 152 | end 153 | 154 | should "not deliver ignored exceptions raised by actions" do 155 | ignore(RuntimeError) 156 | process_action_with_automatic_notification 157 | assert_caught_and_not_sent 158 | end 159 | 160 | should "deliver ignored exception raised manually" do 161 | ignore(RuntimeError) 162 | process_action_with_manual_notification 163 | assert_caught_and_sent 164 | end 165 | 166 | should "deliver manually sent notices in public requests" do 167 | process_action_with_manual_notification 168 | assert_caught_and_sent 169 | end 170 | 171 | should "not deliver manually sent notices in local requests" do 172 | process_action_with_manual_notification(:local => true) 173 | assert_caught_and_not_sent 174 | end 175 | 176 | should "not deliver manually sent notices when all requests are local" do 177 | process_action_with_manual_notification(:all_local => true) 178 | assert_caught_and_not_sent 179 | end 180 | 181 | should "continue with default behavior after delivering an exception" do 182 | controller = process_action_with_automatic_notification(:public => true) 183 | # TODO: can we test this without stubbing? 184 | assert_received(controller, :rescue_action_in_public_without_airbrake) 185 | end 186 | 187 | should "not create actions from Airbrake methods" do 188 | controller = build_controller_class.new 189 | assert_equal [], Airbrake::Rails::ActionControllerCatcher.instance_methods 190 | end 191 | 192 | should "ignore exceptions when user agent is being ignored by regular expression" do 193 | Airbrake.configuration.ignore_user_agent_only = [/Ignored/] 194 | process_action_with_automatic_notification(:user_agent => 'ShouldBeIgnored') 195 | assert_caught_and_not_sent 196 | end 197 | 198 | should "ignore exceptions when user agent is being ignored by string" do 199 | Airbrake.configuration.ignore_user_agent_only = ['IgnoredUserAgent'] 200 | process_action_with_automatic_notification(:user_agent => 'IgnoredUserAgent') 201 | assert_caught_and_not_sent 202 | end 203 | 204 | should "not ignore exceptions when user agent is not being ignored" do 205 | Airbrake.configuration.ignore_user_agent_only = ['IgnoredUserAgent'] 206 | process_action_with_automatic_notification(:user_agent => 'NonIgnoredAgent') 207 | assert_caught_and_sent 208 | end 209 | 210 | should "send session data for manual notifications" do 211 | data = { 'one' => 'two' } 212 | process_action_with_manual_notification(:session => data) 213 | assert_sent_hash data, "/notice/request/session" 214 | end 215 | 216 | should "send session data for automatic notification" do 217 | data = { 'one' => 'two' } 218 | process_action_with_automatic_notification(:session => data) 219 | assert_sent_hash data, "/notice/request/session" 220 | end 221 | 222 | should "send request data for manual notification" do 223 | params = { 'controller' => "airbrake_test", 'action' => "index" } 224 | controller = process_action_with_manual_notification(:params => params) 225 | assert_sent_request_info_for controller.request 226 | end 227 | 228 | should "send request data for manual notification with non-standard port" do 229 | params = { 'controller' => "airbrake_test", 'action' => "index" } 230 | controller = process_action_with_manual_notification(:params => params, :port => 81) 231 | assert_sent_request_info_for controller.request 232 | end 233 | 234 | should "send request data for automatic notification" do 235 | params = { 'controller' => "airbrake_test", 'action' => "index" } 236 | controller = process_action_with_automatic_notification(:params => params) 237 | assert_sent_request_info_for controller.request 238 | end 239 | 240 | should "send request data for automatic notification with non-standard port" do 241 | params = { 'controller' => "airbrake_test", 'action' => "index" } 242 | controller = process_action_with_automatic_notification(:params => params, :port => 81) 243 | assert_sent_request_info_for controller.request 244 | end 245 | 246 | should "use standard rails logging filters on params and session and env" do 247 | filtered_params = { "abc" => "123", 248 | "def" => "456", 249 | "ghi" => "[FILTERED]" } 250 | filtered_session = { "abc" => "123", 251 | "ghi" => "[FILTERED]" } 252 | ENV['ghi'] = 'abc' 253 | filtered_env = { 'ghi' => '[FILTERED]' } 254 | filtered_cgi = { 'REQUEST_METHOD' => '[FILTERED]' } 255 | 256 | process_action_with_automatic_notification(:filters => [:ghi, :request_method], 257 | :params => { "abc" => "123", 258 | "def" => "456", 259 | "ghi" => "789" }, 260 | :session => { "abc" => "123", 261 | "ghi" => "789" }) 262 | assert_sent_hash filtered_params, '/notice/request/params' 263 | assert_sent_hash filtered_cgi, '/notice/request/cgi-data' 264 | assert_sent_hash filtered_session, '/notice/request/session' 265 | end 266 | 267 | context "for a local error with development lookup enabled" do 268 | setup do 269 | Airbrake.configuration.development_lookup = true 270 | Airbrake.stubs(:build_lookup_hash_for).returns({ :awesome => 2 }) 271 | 272 | @controller = process_action_with_automatic_notification(:local => true) 273 | @response = @controller.response 274 | end 275 | 276 | should "append custom CSS and JS to response body for a local error" do 277 | assert_match /text\/css/, @response.body 278 | assert_match /text\/javascript/, @response.body 279 | end 280 | 281 | should "contain host, API key and notice JSON" do 282 | assert_match Airbrake.configuration.host.to_json, @response.body 283 | assert_match Airbrake.configuration.api_key.to_json, @response.body 284 | assert_match ({ :awesome => 2 }).to_json, @response.body 285 | end 286 | end 287 | 288 | context "for a local error with development lookup disabled" do 289 | setup do 290 | Airbrake.configuration.development_lookup = false 291 | 292 | @controller = process_action_with_automatic_notification(:local => true) 293 | @response = @controller.response 294 | end 295 | 296 | should "not append custom CSS and JS to response for a local error" do 297 | assert_no_match /text\/css/, @response.body 298 | assert_no_match /text\/javascript/, @response.body 299 | end 300 | end 301 | 302 | should "call session.to_hash if available" do 303 | hash_data = {:key => :value} 304 | 305 | session = ActionController::TestSession.new 306 | ActionController::TestSession.stubs(:new).returns(session) 307 | session.stubs(:to_hash).returns(hash_data) 308 | 309 | process_action_with_automatic_notification 310 | assert_received(session, :to_hash) 311 | assert_received(session, :data) { |expect| expect.never } 312 | assert_caught_and_sent 313 | end 314 | 315 | should "call session.data if session.to_hash is undefined" do 316 | hash_data = {:key => :value} 317 | 318 | session = ActionController::TestSession.new 319 | ActionController::TestSession.stubs(:new).returns(session) 320 | session.stubs(:data).returns(hash_data) 321 | if session.respond_to?(:to_hash) 322 | class << session 323 | undef to_hash 324 | end 325 | end 326 | 327 | process_action_with_automatic_notification 328 | assert_received(session, :to_hash) { |expect| expect.never } 329 | assert_received(session, :data) { |expect| expect.at_least_once } 330 | assert_caught_and_sent 331 | end 332 | 333 | end 334 | -------------------------------------------------------------------------------- /features/step_definitions/rails_application_steps.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'active_support/core_ext/string/inflections' 3 | 4 | When /^I generate a new Rails application$/ do 5 | @terminal.cd(TEMP_DIR) 6 | version_string = ENV['RAILS_VERSION'] 7 | 8 | rails3 = version_string =~ /^3/ 9 | 10 | if rails3 11 | rails_create_command = 'new' 12 | else 13 | rails_create_command = '' 14 | end 15 | 16 | load_rails = <<-RUBY 17 | gem 'rails', '#{version_string}'; \ 18 | load Gem.bin_path('rails', 'rails', '#{version_string}') 19 | RUBY 20 | 21 | @terminal.run(%{ruby -rrubygems -rthread -e "#{load_rails.strip!}" #{rails_create_command} rails_root}) 22 | if rails_root_exists? 23 | @terminal.echo("Generated a Rails #{version_string} application") 24 | else 25 | raise "Unable to generate a Rails application:\n#{@terminal.output}" 26 | end 27 | require_thread 28 | When %{I configure my application to require the "rake" gem with version "0.8.7"} 29 | config_gem_dependencies unless rails3 30 | end 31 | 32 | When /^I run the airbrake generator with "([^\"]*)"$/ do |generator_args| 33 | if rails3? 34 | When %{I run "script/rails generate airbrake #{generator_args}"} 35 | else 36 | When %{I run "script/generate airbrake #{generator_args}"} 37 | end 38 | end 39 | 40 | When /^I print the console output$/ do 41 | puts @terminal.output 42 | end 43 | 44 | Given /^I have installed the "([^\"]*)" gem$/ do |gem_name| 45 | @terminal.install_gem(gem_name) 46 | end 47 | 48 | Given /^I have built and installed the "([^\"]*)" gem$/ do |gem_name| 49 | @terminal.build_and_install_gem(File.join(PROJECT_ROOT, "#{gem_name}.gemspec")) 50 | end 51 | 52 | When /^I configure my application to require the "([^\"]*)" gem(?: with version "(.+)")?$/ do |gem_name, version| 53 | if rails_manages_gems? 54 | config_gem(gem_name, version) 55 | elsif bundler_manages_gems? 56 | bundle_gem(gem_name, version) 57 | else 58 | File.open(environment_path, 'a') do |file| 59 | file.puts 60 | file.puts("require 'airbrake'") 61 | file.puts("require 'airbrake/rails'") 62 | end 63 | 64 | unless rails_finds_generators_in_gems? 65 | FileUtils.cp_r(File.join(PROJECT_ROOT, 'generators'), File.join(rails_root, 'lib')) 66 | end 67 | end 68 | end 69 | 70 | When /^I run "([^\"]*)"$/ do |command| 71 | @terminal.cd(rails_root) 72 | @terminal.run(command) 73 | end 74 | 75 | Then /^I should receive a Airbrake notification$/ do 76 | Then %{I should see "[Airbrake] Success: Net::HTTPOK"} 77 | end 78 | 79 | Then /^I should receive two Airbrake notifications$/ do 80 | @terminal.output.scan(/\[Airbrake\] Success: Net::HTTPOK/).size.should == 2 81 | end 82 | 83 | When /^I configure the Airbrake shim$/ do 84 | if bundler_manages_gems? 85 | bundle_gem("sham_rack") 86 | end 87 | 88 | shim_file = File.join(PROJECT_ROOT, 'features', 'support', 'airbrake_shim.rb.template') 89 | if rails_supports_initializers? 90 | target = File.join(rails_root, 'config', 'initializers', 'airbrake_shim.rb') 91 | FileUtils.cp(shim_file, target) 92 | else 93 | File.open(environment_path, 'a') do |file| 94 | file.puts 95 | file.write IO.read(shim_file) 96 | end 97 | end 98 | end 99 | 100 | When /^I configure the notifier to use "([^\"]*)" as an API key$/ do |api_key| 101 | steps %{ 102 | When I configure the notifier to use the following configuration lines: 103 | """ 104 | config.api_key = #{api_key.inspect} 105 | """ 106 | } 107 | end 108 | 109 | When /^I configure the notifier to use the following configuration lines:$/ do |configuration_lines| 110 | if rails_manages_gems? 111 | requires = '' 112 | else 113 | requires = "require 'airbrake'" 114 | end 115 | 116 | initializer_code = <<-EOF 117 | #{requires} 118 | Airbrake.configure do |config| 119 | #{configuration_lines} 120 | end 121 | EOF 122 | 123 | if rails_supports_initializers? 124 | File.open(rails_initializer_file, 'w') { |file| file.write(initializer_code) } 125 | else 126 | File.open(environment_path, 'a') do |file| 127 | file.puts 128 | file.puts initializer_code 129 | end 130 | end 131 | 132 | end 133 | 134 | def rails_initializer_file 135 | File.join(rails_root, 'config', 'initializers', 'airbrake.rb') 136 | end 137 | 138 | def rails_non_initializer_airbrake_config_file 139 | File.join(rails_root, 'config', 'airbrake.rb') 140 | end 141 | 142 | Then /^I should see "([^\"]*)"$/ do |expected_text| 143 | unless @terminal.output.include?(expected_text) 144 | raise("Got terminal output:\n#{@terminal.output}\n\nExpected output:\n#{expected_text}") 145 | end 146 | end 147 | 148 | Then /^I should not see "([^\"]*)"$/ do |unexpected_text| 149 | if @terminal.output.include?(unexpected_text) 150 | raise("Got terminal output:\n#{@terminal.output}\n\nDid not expect the following output:\n#{unexpected_text}") 151 | end 152 | end 153 | 154 | When /^I uninstall the "([^\"]*)" gem$/ do |gem_name| 155 | @terminal.uninstall_gem(gem_name) 156 | end 157 | 158 | When /^I unpack the "([^\"]*)" gem$/ do |gem_name| 159 | if bundler_manages_gems? 160 | @terminal.cd(rails_root) 161 | @terminal.run("bundle pack") 162 | elsif rails_manages_gems? 163 | @terminal.cd(rails_root) 164 | @terminal.run("rake gems:unpack GEM=#{gem_name}") 165 | else 166 | vendor_dir = File.join(rails_root, 'vendor', 'gems') 167 | FileUtils.mkdir_p(vendor_dir) 168 | @terminal.cd(vendor_dir) 169 | @terminal.run("gem unpack #{gem_name}") 170 | gem_path = 171 | Dir.glob(File.join(rails_root, 'vendor', 'gems', "#{gem_name}-*", 'lib')).first 172 | File.open(environment_path, 'a') do |file| 173 | file.puts 174 | file.puts("$: << #{gem_path.inspect}") 175 | end 176 | end 177 | end 178 | 179 | When /^I install cached gems$/ do 180 | if bundler_manages_gems? 181 | When %{I run "bundle install"} 182 | end 183 | end 184 | 185 | When /^I install the "([^\"]*)" plugin$/ do |plugin_name| 186 | FileUtils.mkdir_p("#{rails_root}/vendor/plugins/#{plugin_name}") 187 | end 188 | 189 | When /^I define a response for "([^\"]*)":$/ do |controller_and_action, definition| 190 | controller_class_name, action = controller_and_action.split('#') 191 | controller_name = controller_class_name.underscore 192 | controller_file_name = File.join(rails_root, 'app', 'controllers', "#{controller_name}.rb") 193 | File.open(controller_file_name, "w") do |file| 194 | file.puts "class #{controller_class_name} < ApplicationController" 195 | file.puts "def consider_all_requests_local; false; end" 196 | file.puts "def local_request?; false; end" 197 | file.puts "def #{action}" 198 | file.puts definition 199 | file.puts "end" 200 | file.puts "end" 201 | end 202 | end 203 | 204 | When /^I perform a request to "([^\"]*)"$/ do |uri| 205 | perform_request(uri) 206 | end 207 | 208 | When /^I perform a request to "([^\"]*)" in the "([^\"]*)" environment$/ do |uri, environment| 209 | perform_request(uri, environment) 210 | end 211 | 212 | Given /^the response page for a "([^\"]*)" error is$/ do |error, html| 213 | File.open(File.join(rails_root, "public", "#{error}.html"), "w") do |file| 214 | file.write(html) 215 | end 216 | end 217 | 218 | Then /^I should receive the following Airbrake notification:$/ do |table| 219 | exceptions = @terminal.output.scan(%r{Recieved the following exception:\n([^\n]*)\n}m) 220 | exceptions.should_not be_empty 221 | 222 | xml = exceptions.last[0] 223 | doc = Nokogiri::XML.parse(xml) 224 | 225 | hash = table.transpose.hashes.first 226 | 227 | doc.should have_content('//error/message', hash['error message']) 228 | doc.should have_content('//error/class', hash['error class']) 229 | doc.should have_content('//request/url', hash['url']) 230 | 231 | doc.should have_content('//component', hash['component']) if hash['component'] 232 | doc.should have_content('//action', hash['action']) if hash['action'] 233 | doc.should have_content('//server-environment/project-root', hash['project-root']) if hash['project-root'] 234 | 235 | if hash['session'] 236 | session_key, session_value = hash['session'].split(': ') 237 | doc.should have_content('//request/session/var/@key', session_key) 238 | doc.should have_content('//request/session/var', session_value) 239 | end 240 | 241 | if hash['parameters'] 242 | param_key, param_value = hash['parameters'].split(': ') 243 | doc.should have_content('//request/params/var/@key', param_key) 244 | doc.should have_content('//request/params/var', param_value) 245 | end 246 | end 247 | 248 | Then /^I should see the Rails version$/ do 249 | Then %{I should see "[Rails: #{rails_version}]"} 250 | end 251 | 252 | Then /^I should see that "([^\"]*)" is not considered a framework gem$/ do |gem_name| 253 | Then %{I should not see "[R] #{gem_name}"} 254 | end 255 | 256 | Then /^the command should have run successfully$/ do 257 | @terminal.status.exitstatus.should == 0 258 | end 259 | 260 | When /^I route "([^\"]*)" to "([^\"]*)"$/ do |path, controller_action_pair| 261 | route = if rails3? 262 | %(match "#{path}", :to => "#{controller_action_pair}") 263 | else 264 | controller, action = controller_action_pair.split('#') 265 | %(map.connect "#{path}", :controller => "#{controller}", :action => "#{action}") 266 | end 267 | routes_file = File.join(rails_root, "config", "routes.rb") 268 | File.open(routes_file, "r+") do |file| 269 | content = file.read 270 | content.gsub!(/^end$/, " #{route}\nend") 271 | file.rewind 272 | file.write(content) 273 | end 274 | end 275 | 276 | Then /^"([^\"]*)" should not contain "([^\"]*)"$/ do |file_path, text| 277 | actual_text = IO.read(File.join(rails_root, file_path)) 278 | if actual_text.include?(text) 279 | raise "Didn't expect text:\n#{actual_text}\nTo include:\n#{text}" 280 | end 281 | end 282 | 283 | Then /^my Airbrake configuration should contain the following line:$/ do |line| 284 | configuration_file = if rails_supports_initializers? 285 | rails_initializer_file 286 | else 287 | rails_non_initializer_airbrake_config_file 288 | # environment_path 289 | end 290 | 291 | configuration = File.read(configuration_file) 292 | if ! configuration.include?(line.strip) 293 | raise "Expected text:\n#{configuration}\nTo include:\n#{line}\nBut it didn't." 294 | end 295 | end 296 | 297 | When /^I set the environment variable "([^\"]*)" to "([^\"]*)"$/ do |environment_variable, value| 298 | @terminal.environment_variables[environment_variable] = value 299 | end 300 | 301 | When /^I configure the Heroku rake shim$/ do 302 | @terminal.invoke_heroku_rake_tasks_locally = true 303 | end 304 | 305 | When /^I configure the Heroku gem shim with "([^\"]*)"( and multiple app support)?$/ do |api_key, multi_app| 306 | heroku_script_bin = File.join(TEMP_DIR, "bin") 307 | FileUtils.mkdir_p(heroku_script_bin) 308 | heroku_script = File.join(heroku_script_bin, "heroku") 309 | single_app_script = <<-SINGLE 310 | #!/bin/bash 311 | if [[ $1 == 'console' && $2 == 'puts ENV[%{HOPTOAD_API_KEY}]' ]]; then 312 | echo #{api_key} 313 | fi 314 | SINGLE 315 | 316 | multi_app_script = <<-MULTI 317 | #!/bin/bash 318 | if [[ $1 == 'console' && $2 == '--app' && $4 == 'puts ENV[%{HOPTOAD_API_KEY}]' ]]; then 319 | echo #{api_key} 320 | fi 321 | MULTI 322 | 323 | File.open(heroku_script, "w") do |f| 324 | if multi_app 325 | f.puts multi_app_script 326 | else 327 | f.puts single_app_script 328 | end 329 | end 330 | FileUtils.chmod(0755, heroku_script) 331 | @terminal.prepend_path(heroku_script_bin) 332 | end 333 | 334 | When /^I configure the application to filter parameter "([^\"]*)"$/ do |parameter| 335 | if rails3? 336 | application_filename = File.join(rails_root, 'config', 'application.rb') 337 | application_lines = File.open(application_filename).readlines 338 | 339 | application_definition_line = application_lines.detect { |line| line =~ /Application/ } 340 | application_definition_line_index = application_lines.index(application_definition_line) 341 | 342 | application_lines.insert(application_definition_line_index + 1, 343 | " config.filter_parameters += [#{parameter.inspect}]") 344 | 345 | File.open(application_filename, "w") do |file| 346 | file.puts application_lines.join("\n") 347 | end 348 | else 349 | controller_filename = application_controller_filename 350 | controller_lines = File.open(controller_filename).readlines 351 | 352 | controller_definition_line = controller_lines.detect { |line| line =~ /ApplicationController/ } 353 | controller_definition_line_index = controller_lines.index(controller_definition_line) 354 | 355 | controller_lines.insert(controller_definition_line_index + 1, 356 | " filter_parameter_logging #{parameter.inspect}") 357 | 358 | File.open(controller_filename, "w") do |file| 359 | file.puts controller_lines.join("\n") 360 | end 361 | end 362 | end 363 | 364 | Then /^I should see the notifier JavaScript for the following:$/ do |table| 365 | hash = table.hashes.first 366 | host = hash['host'] || 'airbrakeapp.com' 367 | secure = hash['secure'] || false 368 | api_key = hash['api_key'] 369 | environment = hash['environment'] || 'production' 370 | 371 | document_body = '' + @terminal.output.split('').last 372 | document_body.should include("#{host}/javascripts/notifier.js") 373 | 374 | response = Nokogiri::HTML.parse(document_body) 375 | response.css("script[type='text/javascript']:last-child").each do |element| 376 | content = element.content 377 | content.should include("Airbrake.setKey('#{api_key}');") 378 | content.should include("Airbrake.setHost('#{host}');") 379 | content.should include("Airbrake.setEnvironment('#{environment}');") 380 | end 381 | end 382 | 383 | Then "the notifier JavaScript should provide the following errorDefaults:" do |table| 384 | hash = table.hashes.first 385 | 386 | document_body = '' + @terminal.output.split('').last 387 | 388 | response = Nokogiri::HTML.parse(document_body) 389 | response.css("script[type='text/javascript']:last-child").each do |element| 390 | content = element.content 391 | 392 | hash.each do |key, value| 393 | content.should =~ %r{Airbrake\.setErrorDefaults.*#{key}: "#{value}}m 394 | end 395 | end 396 | end 397 | 398 | Then /^I should not see notifier JavaScript$/ do 399 | response = Nokogiri::HTML.parse('' + @terminal.output.split('').last) 400 | response.at_css("script[type='text/javascript'][src$='/javascripts/notifier.js']").should be_nil 401 | end 402 | --------------------------------------------------------------------------------