├── 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 |
--------------------------------------------------------------------------------