Rails 4: ActiveSupport::BacktraceCleaner, towards better documentation

The official documentation for the class can be found at http://api.rubyonrails.org/classes/ActiveSupport/BacktraceCleaner.html.

The problem

However, trying to implement the proposed example code throws an error.

# the original code example as of 2013-10-11
bc = BacktraceCleaner.new
bc.add_filter   { |line| line.gsub(Rails.root, '') }
bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems
# => TypeError: wrong argument type Pathname (expected Regexp)

Hmpfh. The problem here is the line

bc.add_filter { |line| line.gsub(Rails.root, '') }

"String".gsub expects a pattern as shown at http://www.ruby-doc.org/core-2.0.0/String.html#method-i-gsub. Rails.root, however, returns something of type Pathname.

Corrections

The solution is very easy: Just append

.to_s

The correct example snippet should then be:

bc = ActiveSupport::BacktraceCleaner.new
bc.add_filter { |line| line.gsub(Rails.root.to_s, '') }
bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems

Real-world examples

Another thing to note about the BacktraceCleaner is that filters are applied first, then silencers. That means if you want to remove for example all trace lines which do not belong to your app, and at the same time remove the long path to your app you might end up with nothing:

First – failed – try

# don't use this
    bc.add_filter { |line| line.gsub(Rails.root.to_s, '') }
    bc.add_silencer { |line| line.index(Rails.root.to_s).nil? }

This code first cleans the root path from all lines, then removes all as none contain it anymore.

Working example 1

Instead this is what I did:

    bc = ActiveSupport::BacktraceCleaner.new
    bc.add_filter { |line| line.gsub(Rails.root.to_s, '<root>') }
    bc.add_silencer { |line| line.index('<root>').nil? and line.index('/') == 0 }
    bc.add_silencer { |line| line.index('<root>/vendor/') == 0 }
    e.set_backtrace(bc.clean(e.backtrace))

I replace the root path with a placeholder <root>. The first silencer removes all lines that are do not contain the <root> part, but only if it is a path. That way console traces or the main method still stay in place. I also added a second silencer that removes all traces of installed gems if installed inside the /vendor folder.

Working example 2

Interestingly, the docs state

To reconfigure an existing BacktraceCleaner (like the default one in Rails) …

that there is a default backtrace cleaner. It turns out there exists a method

Rails.backtrace_cleaner

which returns the default Rails cleaner. So this is rather what I use now, as it quite nicely fits my purpose:

    bc = Rails.backtrace_cleaner
    e.set_backtrace(bc.clean(e.backtrace))

Currently, their setup can be found at https://github.com/rails/rails/blob/4-0-stable/railties/lib/rails/backtrace_cleaner.rb. This is what the official backtrace cleaner looks like:

class BacktraceCleaner < ActiveSupport::BacktraceCleaner
    APP_DIRS_PATTERN = /^\/?(app|config|lib|test)/
    RENDER_TEMPLATE_PATTERN = /:in `_render_template_\w*'/

    def initialize
      super
      add_filter   { |line| line.sub("#{Rails.root}/", '') }
      add_filter   { |line| line.sub(RENDER_TEMPLATE_PATTERN, '') }
      add_filter   { |line| line.sub('./', '/') } # for tests

      add_gem_filters
      add_silencer { |line| line !~ APP_DIRS_PATTERN }
    end

    private
      def add_gem_filters
        gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
        return if gems_paths.empty?

        gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)}
        add_filter { |line| line.sub(gems_regexp, '\2 (\3) \4') }
      end
  end