aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/backtrace_cleaner.rb
blob: e97bb25b9fcdc10005cc3bbcb474e21db6c017d6 (plain) (blame)
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
module ActiveSupport
  # Backtraces often include many lines that are not relevant for the context under review. This makes it hard to find the
  # signal amongst the backtrace noise, and adds debugging time. With a BacktraceCleaner, filters and silencers are used to
  # remove the noisy lines, so that only the most relevant lines remain.
  #
  # Filters are used to modify lines of data, while silencers are used to remove lines entirely.  The typical filter use case
  # is to remove lengthy path information from the start of each line, and view file paths relevant to the app directory
  # instead of the file system root.  The typical silencer use case is to exclude the output of a noisy library from the
  # backtrace, so that you can focus on the rest.
  #
  # ==== Example:
  #
  #   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
  #
  # To reconfigure an existing BacktraceCleaner (like the default one in Rails) and show as much data as possible, you can
  # always call <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the backtrace to a pristine state.  If you
  # need to reconfigure an existing BacktraceCleaner so that it does not filter or modify the paths of any lines of the
  # backtrace, you can call BacktraceCleaner#remove_filters! These two methods will give you a completely untouched backtrace.
  #
  # Inspired by the Quiet Backtrace gem by Thoughtbot.
  class BacktraceCleaner
    def initialize
      @filters, @silencers = [], []
    end

    # Returns the backtrace after all filters and silencers have been run against it. Filters run first, then silencers.
    def clean(backtrace, kind = :silent)
      filtered = filter(backtrace)

      case kind
      when :silent
        silence(filtered)
      when :noise
        noise(filtered)
      else
        filtered
      end
    end

    # Adds a filter from the block provided. Each line in the backtrace will be mapped against this filter.
    #
    # Example:
    #
    #   # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
    #   backtrace_cleaner.add_filter { |line| line.gsub(Rails.root, '') }
    def add_filter(&block)
      @filters << block
    end

    # Adds a silencer from the block provided. If the silencer returns true for a given line, it will be excluded from
    # the clean backtrace.
    #
    # Example:
    #
    #   # Will reject all lines that include the word "mongrel", like "/gems/mongrel/server.rb" or "/app/my_mongrel_server/rb"
    #   backtrace_cleaner.add_silencer { |line| line =~ /mongrel/ }
    def add_silencer(&block)
      @silencers << block
    end

    # Will remove all silencers, but leave in the filters. This is useful if your context of debugging suddenly expands as
    # you suspect a bug in one of the libraries you use.
    def remove_silencers!
      @silencers = []
    end

    def remove_filters!
      @filters = []
    end

    private
      def filter(backtrace)
        @filters.each do |f|
          backtrace = backtrace.map { |line| f.call(line) }
        end

        backtrace
      end

      def silence(backtrace)
        @silencers.each do |s|
          backtrace = backtrace.reject { |line| s.call(line) }
        end

        backtrace
      end

      def noise(backtrace)
        @silencers.each do |s|
          backtrace = backtrace.select { |line| s.call(line) }
        end

        backtrace
      end
  end
end