diff --git a/.rubocop.yml b/.rubocop.yml index 7be53812d..73807ba5d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -55,6 +55,9 @@ Style/CommentedKeyword: Style/RescueModifier: Enabled: false +Style/RegexpLiteral: + Enabled: false + Style/StringLiterals: Enabled: false diff --git a/lib/raven/backtrace.rb b/lib/raven/backtrace.rb index 687849d9c..4afed54db 100644 --- a/lib/raven/backtrace.rb +++ b/lib/raven/backtrace.rb @@ -94,6 +94,8 @@ def self.in_app_pattern def self.parse(backtrace, opts = {}) ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/) + ruby_lines = opts[:configuration].backtrace_cleanup_callback.call(ruby_lines) if opts[:configuration]&.backtrace_cleanup_callback + filters = opts[:filters] || [] filtered_lines = ruby_lines.to_a.map do |line| filters.reduce(line) do |nested_line, proc| diff --git a/lib/raven/configuration.rb b/lib/raven/configuration.rb index 5c710dee1..73f669c84 100644 --- a/lib/raven/configuration.rb +++ b/lib/raven/configuration.rb @@ -118,6 +118,19 @@ class Configuration # Otherwise, can be one of "http", "https", or "dummy" attr_accessor :scheme + # a proc/lambda that takes an array of stack traces + # it'll be used to silence (reduce) backtrace of the exception + # + # for example: + # + # ```ruby + # Raven.configuration.backtrace_cleanup_callback = lambda do |backtrace| + # Rails.backtrace_cleaner.clean(backtrace) + # end + # ``` + # + attr_accessor :backtrace_cleanup_callback + # Secret key for authentication with the Sentry server # If you provide a DSN, this will be set automatically. # diff --git a/lib/raven/event.rb b/lib/raven/event.rb index 99fbf0feb..9d55606c3 100644 --- a/lib/raven/event.rb +++ b/lib/raven/event.rb @@ -175,7 +175,7 @@ def add_exception_interface(exc) end def stacktrace_interface_from(backtrace) - Backtrace.parse(backtrace).lines.reverse.each_with_object([]) do |line, memo| + Backtrace.parse(backtrace, { configuration: configuration }).lines.reverse.each_with_object([]) do |line, memo| frame = StacktraceInterface::Frame.new frame.abs_path = line.file if line.file frame.function = line.method if line.method diff --git a/lib/raven/integrations/rails.rb b/lib/raven/integrations/rails.rb index ac799ea79..03daf7de5 100644 --- a/lib/raven/integrations/rails.rb +++ b/lib/raven/integrations/rails.rb @@ -5,6 +5,7 @@ class Rails < ::Rails::Railtie require 'raven/integrations/rails/overrides/streaming_reporter' require 'raven/integrations/rails/controller_methods' require 'raven/integrations/rails/controller_transaction' + require 'raven/integrations/rails/backtrace_cleaner' require 'raven/integrations/rack' initializer "raven.use_rack_middleware" do |app| @@ -37,6 +38,12 @@ class Rails < ::Rails::Railtie config.before_initialize do Raven.configuration.logger = ::Rails.logger + + backtrace_cleaner = Raven::Rails::BacktraceCleaner.new + + Raven.configuration.backtrace_cleanup_callback = lambda do |backtrace| + backtrace_cleaner.clean(backtrace) + end end config.after_initialize do diff --git a/lib/raven/integrations/rails/backtrace_cleaner.rb b/lib/raven/integrations/rails/backtrace_cleaner.rb new file mode 100644 index 000000000..871ca03df --- /dev/null +++ b/lib/raven/integrations/rails/backtrace_cleaner.rb @@ -0,0 +1,29 @@ +require "active_support/backtrace_cleaner" +require "active_support/core_ext/string/access" + +module Raven + class Rails + class BacktraceCleaner < ActiveSupport::BacktraceCleaner + APP_DIRS_PATTERN = /\A(?:\.\/)?(?:app|config|lib|test|\(\w*\))/.freeze + RENDER_TEMPLATE_PATTERN = /:in `.*_\w+_{2,3}\d+_\d+'/.freeze + + def initialize + super + # we don't want any default silencers because they're too aggressive + remove_silencers! + + @root = "#{Raven.configuration.project_root}/" + add_filter do |line| + line.start_with?(@root) ? line.from(@root.size) : line + end + add_filter do |line| + if line =~ RENDER_TEMPLATE_PATTERN + line.sub(RENDER_TEMPLATE_PATTERN, "") + else + line + end + end + end + end + end +end diff --git a/spec/raven/backtrace_spec.rb b/spec/raven/backtrace_spec.rb index a7f664276..dae416ba1 100644 --- a/spec/raven/backtrace_spec.rb +++ b/spec/raven/backtrace_spec.rb @@ -5,6 +5,19 @@ @backtrace = Raven::Backtrace.parse(Thread.current.backtrace) end + it "calls backtrace_cleanup_callback if it's present in the configuration" do + called = false + callback = proc do |backtrace| + called = true + backtrace + end + config = Raven.configuration + config.backtrace_cleanup_callback = callback + Raven::Backtrace.parse(Thread.current.backtrace, configuration: config) + + expect(called).to eq(true) + end + it "#lines" do expect(@backtrace.lines.first).to be_a(Raven::Backtrace::Line) end diff --git a/spec/raven/configuration_spec.rb b/spec/raven/configuration_spec.rb index fd6a24a36..27fddec6e 100644 --- a/spec/raven/configuration_spec.rb +++ b/spec/raven/configuration_spec.rb @@ -254,6 +254,18 @@ end end + describe "config: backtrace_cleanup_callback" do + it "defaults to nil" do + expect(subject.backtrace_cleanup_callback).to eq(nil) + end + + it "takes a proc and store it" do + subject.backtrace_cleanup_callback = proc {} + + expect(subject.backtrace_cleanup_callback).to be_a(Proc) + end + end + context 'with a should_capture callback configured' do before(:each) do subject.should_capture = ->(exc_or_msg) { exc_or_msg != "dont send me" } diff --git a/spec/raven/integrations/rails_spec.rb b/spec/raven/integrations/rails_spec.rb index 06e0a643c..96aeaff75 100644 --- a/spec/raven/integrations/rails_spec.rb +++ b/spec/raven/integrations/rails_spec.rb @@ -29,11 +29,35 @@ expect(event["exception"]["values"][0]["value"]).to eq("An unhandled exception!") end - it "should properly set the exception's URL" do + it "should capture exceptions in production" do get "/exception" + expect(response.status).to eq(500) + event = JSON.parse!(Raven.client.transport.events.first[1]) + expect(event["exception"]["values"][0]["type"]).to eq("RuntimeError") + expect(event["exception"]["values"][0]["value"]).to eq("An unhandled exception!") + end + + it "filters exception backtrace with with custom BacktraceCleaner" do + get "/view_exception" + + event = JSON.parse!(Raven.client.transport.events.first[1]) + traces = event.dig("exception", "values", 0, "stacktrace", "frames") + expect(traces.dig(-1, "filename")).to eq("inline template") + + # we want to avoid something like "_inline_template__3014794444104730113_10960" + expect(traces.dig(-1, "function")).to be_nil + end + + it "doesn't filters exception backtrace if backtrace_cleanup_callback is overridden" do + Raven.configuration.backtrace_cleanup_callback = nil + + get "/view_exception" + event = JSON.parse!(Raven.client.transport.events.first[1]) - expect(event['request']['url']).to eq("http://www.example.com/exception") + traces = event.dig("exception", "values", 0, "stacktrace", "frames") + expect(traces.dig(-1, "filename")).to eq("inline template") + expect(traces.dig(-1, "function")).not_to be_nil end it "sets transaction to ControllerName#method" do diff --git a/spec/support/test_rails_app/app.rb b/spec/support/test_rails_app/app.rb index 27a89c417..5cf61c8d3 100644 --- a/spec/support/test_rails_app/app.rb +++ b/spec/support/test_rails_app/app.rb @@ -21,6 +21,7 @@ class TestApp < Rails::Application routes.append do get "/exception", :to => "hello#exception" + get "/view_exception", :to => "hello#view_exception" root :to => "hello#world" end @@ -36,6 +37,10 @@ def exception raise "An unhandled exception!" end + def view_exception + render inline: "<%= foo %>" + end + def world render :plain => "Hello World!" end