2

I am using the new Sentry (not Raven) for Ruby for my Rails 6.1 application:

gem "sentry-ruby"
gem "sentry-rails"

I am able to see the transactions when users triggered ActionController::RoutingError, but I want these to appear as errors in Sentry. I do not see ActionController::RoutingError as an 'ignored' or 'excluded' error:

> Sentry::Configuration::IGNORE_DEFAULT
 => ["Mongoid::Errors::DocumentNotFound", "Rack::QueryParser::InvalidParameterError", "Rack::QueryParser::ParameterTypeError", "Sinatra::NotFound"] 

I tried clearing excluded_exceptions in sentry.rb initializer file, but this had no effect:

Sentry.init do |config|
  ...
  config.excluded_exceptions = []
end

How can I configure Sentry so that these are sent as errors? I was also thinking that I can modify the middleware (ActionDispatch?, ActionPack?) and add Sentry.capture_message where appropriate, but I am not sure how to do that.

I do not want a "catch-all" route that redirects to an ErrorController or ApplicationController:

match '*path', to: "application#handle_route_error", via: :all

RailinginDFW
  • 1,523
  • 12
  • 19
  • Looks like there is no any option except "catch-all" way. See https://stackoverflow.com/questions/37173519/rescue-from-actioncontrollerroutingerror-doesnt-work for details. – Vasily Kolesnikov Aug 19 '23 at 13:57
  • Are you sure that your Rails app doesn't swallow the exception? E.g. maybe there's a `rescue_from ActionController::RoutingError` inside a base controller, and it renders a 404 page instead of letting the exception propagate. – nitsas Aug 19 '23 at 23:27
  • @nitsas Unfortunately we can't use `rescue_from` to handle RoutingError, and this has been the case since Rails 3. That's when routing was moved to the middleware. – RailinginDFW Aug 20 '23 at 01:04

2 Answers2

2

The problem is that this error is handled by default in Rails and rescued with "Not Found" response

Of course you can handle such errors ("Not Found", "Unprocessable Entity", "Internal Server Error", etc.) manually using router and response with specific action of something like ErrorsController. You can send message to Sentry from there

But since you don't want such decision, you can monkeypatch middleware or even exactly ActionController::RoutingError. Find source of this error and add initializer like this

# config/initializers/handle_routing_error.rb

module ActionController
  class RoutingError < ActionControllerError
    attr_reader :failures
    def initialize(message, failures = [])
      Sentry.capture_message(message) # or Sentry.capture_error(self)

      super(message)
      @failures = failures
    end
  end
end

Usually monkeypatch is some hack and not nice solution, but it will work

mechnicov
  • 12,025
  • 4
  • 33
  • 56
  • thanks for the insight. I agree that monkey-patching is not preferable. My issue is that a "catch-all" route seems, for a lack of better description, too blunt of an instrument. I will accept your answer in the next day or so. Thank you. – RailinginDFW Aug 20 '23 at 01:10
2

I don't use sentry, just replace p with whatever command for sentry. But I can make up for it with 3 solutions:

Rescue

For production only, this is required: config.consider_all_requests_local = false.

# config/initializers/report_routing_error.rb

class ReportRoutingErrorMiddleware
  def initialize app
    @app = app
  end

  def call(env)
    @app.call(env)
  rescue ActionController::RoutingError => e
    p ["NOT FOUND", e]
    raise
  end
end

# DebugExceptions raises RoutingError when consider_all_requests_local is false
# insert_before - to catch the error
Rails.application.config.middleware.insert_before ActionDispatch::DebugExceptions, ReportRoutingErrorMiddleware

#=> ["NOT FOUND", #<ActionController::RoutingError: No route matches [GET] "/asdf">]

404

In development, DebugExceptions middleware technically raises the error but it also rescues it and renders the full error page. Until response gets to that middleware it is just a 404 response from the router: https://github.com/rails/rails/blob/v7.0.7/actionpack/lib/action_dispatch/routing/route_set.rb#L37

# config/initializers/report_routing_error.rb

class ReportRoutingErrorMiddleware
  def initialize app
    @app = app
  end

  def call(env)
    status, headers, body = response = @app.call(env)
    if status == 404
      # no error, so get what you need from env
      p ["NOT FOUND", env["REQUEST_METHOD"], env["PATH_INFO"]]
    end
    response
  end
end

# insert_after - to get response first, because it doesn't go past DebugExceptions
Rails.application.config.middleware.insert_after ActionDispatch::DebugExceptions, ReportRoutingErrorMiddleware

#=> ["NOT FOUND", "GET", "/asdf"]

Intercept

Apparently, they've figured it out a long time ago register_interceptor:

# config/initializers/report_routing_error.rb

ActionDispatch::DebugExceptions.register_interceptor do |req, exception|
  if exception.is_a? ActionController::RoutingError
    p ["NOT FOUND", req, exception]
  end
end

#=> ["NOT FOUND", #<ActionDispatch::Request GET "http://0.0.0.0:3000/asdf" for 127.0.0.1>, #<ActionController::RoutingError: No route matches [GET] "/asdf">]

Introduce ActionDispatch::DebugExceptions.register_interceptor, a way to hook into DebugExceptions and process the exception, before being rendered.

https://github.com/rails/rails/pull/23868

Alex
  • 16,409
  • 6
  • 40
  • 56