Upgrading to rails 3.0 — making sure you use rack headers everywhere

Normal headers like Accept or :authorization do not work in rails 3 integration tests and you need to convert everything to HTTP_ACCEPT etc, to help find all those places and make sure you do not introduce new bugs in rails 2 add this:

# https://grosser.it/2012/10/19/upgrading-to-rails-3-0-making-sure-you-use-rack-headers-everywhere/
# message can be changed on rails 3, but keep the warning, it's so hard to track down missing headers
# maybe try to remove in rails 3.1+
# can be tested by e.g. changing header to Accept instead of HTTP_ACCEPT
class ActionController::Integration::Session
  # headers that are only used by our code and not rails/rack can be whitelisted, but make sure they work on rails 2 and 3
  HEADER_WHITELIST = ['Funky-Headers-You-Have-To-Use']
  def process_with_header_warning(*args)
    if args[3] && bad = args[3].keys.detect{|k| !k.is_a?(String) || (!HEADER_WHITELIST.include?(k) && k !~ /^[A-Z_\d]+$/) }
      raise "Header #{bad} will not work on rails 3, please uppercase (Content-Type -> CONTENT_TYPE) and prefix HTTP_ (Accept -> HTTP_ACCEPT)"
    end
    process_without_header_warning(*args)
  end
  alias_method_chain :process, :header_warning
end

Rails 2: Your integration tests are lying

Integration tests call the whole rack middleware stack, but stubbornly return the last controller response, which can be completely different especially if you use warden or other middleware-tools.

See actionpack-2.3.14/lib/action_controller/integration.rb:342

To fix that and prepare for Rails 3 (which also relies on rack response) do this:

# test/test_helper.rb

if Rails::VERSION::MAJOR == 2
  # https://grosser.it/2012/10/19/rails-2-your-integration-tests-are-lying/
  # make integration tests use rack response, so we can test our middlewares
  # and not only the pure controller response

  ActionController::Base.class_eval do
    # this is usually done just-in-time by #process but we need to do it earlier
    include ActionController::Integration::ControllerCapture

    # then we hide last_instantiation from #process
    def self.last_instantiation;end
  end

  ActionController::Integration::Session.class_eval do
    def process_with_rackify(*args)
      process_without_rackify(*args)
    ensure
      # needed e.g. inside of assert_redirect_to
      capture = ActionController::Integration::ControllerCapture::ClassMethods

      # not set by original #process
      @response.redirected_to = @response.headers["Location"] if @response
      if @controller = capture.send(:class_variable_get, :@@last_instantiation)
        @request = @controller.request
        @response.template = @controller.response.template if @controller.response
        @controller.send(:set_test_assigns)
      end
    end
    alias_method_chain :process, :rackify
  end
end

Making sure you are not creating unwanted actions by including Modules

This will blow up if someone includes a module with public methods that would count as actions.

# application_controller_test.rb
class ApplicationControllerTest < ActionController::TestCase
  class CleanController < ApplicationController
  end

  test "should be clean" do
    assert_equal [], CleanController.action_methods.to_a.
      reject{|a| a =~ /^_conditional_callback_around_/}
  end
end

Airbrake error backtrace summary

We often have an error with a few thousand occurances and want to find out which code paths caused it.

Usage
Use auth-token from settings page, not your api-key.

ruby airbrake_backtraces.rb your-account your-auth-token error-id

Output

Trace 1: occurred 597 times
...funky backtraces...
Trace 2: occurred 119 times
...funky backtraces...
Trace 3: occurred 13 times
...funky backtraces...

Code

#! /usr/bin/env ruby
# lists all sources of a given error
# https://grosser.it/2012/09/08/airbrake-error-summary
#
# gem install airbrake-api
# USAGE: ruby airbrake_backtraces.rb your-account your-auth-token error-id
# https://your-account.airbrake.io/errors/ID

require "airbrake-api"

AirbrakeAPI.account = ARGV[0] || raise("need airbrake account as ARGV[0]")
AirbrakeAPI.auth_token = ARGV[1] || raise("need airbrake token as ARGV[1], go to airbrake -> settings, copy your auth token")
AirbrakeAPI.secure = true

error_id = ARGV[2] || raise("need error id")
compare_depth = (ARGV[3] || 4).to_i

notices = AirbrakeAPI.notices(error_id, :pages => 20)
backtraces = notices.select{|n| n.backtrace }.group_by do |notice|
  notice.backtrace.first[1][0..compare_depth]
end

backtraces.sort_by{|k,t| t.size }.reverse.each_with_index do |(key, traces), index|
  puts "Trace #{index + 1}: occurred #{traces.size} times"
  puts key
  puts ""
end

Airbrake search

We need to search for specific errors quiet often, this script helps us find them without having to go through the web interface.

Usage
auth_token can be found on your settings page, it is NOT the api-key.

ruby airbrake_search.rb your-account your-auth-token | grep foo

Code

#! /usr/bin/env ruby
# https://grosser.it/2012/09/08/airbrake-search
# search for errors given a name
#
# gem install airbrake-api
# USAGE: ruby airbrake_search.rb your-account your-auth-token | grep SOMETHING
# https://your-account.airbrake.io/errors/ID

require "airbrake-api"

AirbrakeAPI.account = ARGV[0] || raise("need airbrake account as ARGV[0]")
AirbrakeAPI.auth_token = ARGV[1] || raise("need airbrake token as ARGV[1], go to airbrake -> settings, copy your auth token")
AirbrakeAPI.secure = true

page = 1
while errors = AirbrakeAPI.errors(:page => page)
  errors.each do |error|
    puts "#{error.id} -- #{error.error_class} -- #{error.error_message} -- #{error.created_at}"
  end
  $stderr.puts "Page #{page} ----------\n"
  page += 1
end