wrangler

A Rails exception handling gem, including sending emails, rendering error pages...inspired by but distinct from exception_notification. Spun off from some work at discovereads.com

5
2
Ruby

= Wrangler

== NOTE/DISCLAIMER
This gem is almost completely inspired by/ripped off the exception_notification plugin/gem, but had to hack too much to get things to work with delayed_job that I just decided to start from scratch. You’ll see that much has been borrowed however, so I owe a huge debt to the originals (exception_notification [http://github.com/rails/exception_notification] and super_exception_notification [http://github.com/pboling/exception_notification]) to help me recreate the Rails hacking.

If you don’t really care about using delayed_job for your emailing, consider going back to the originals, as they’re likely better maintained… 😉

== Overview:
A gem for handling exceptions in a Rails application/environment.

Some highlights:

  • Allows for rendering error pages and sending email notifications in case of errors.
  • Allows for lots of configuring of which pages to show, whether to email or not.
  • Allows for asynchronous emailing through delayed_job if available, but works fine even if delayed_job not installed (email will be synchronous however)
  • Will honor filter_parameters set in the controller including the Wrangler module (except if the parameters are in the URL for GETs) when logging and emailing application state at the time of the exception
  • Allows email notification on exceptions thrown totally outside Controller context, e.g. in a script run (with rails environment) in a cronjob or delayed_job
  • See TODO for things that are explicitly not included yet, but that could/should be added

== Quickstart
=== Bare Minimum
There are a lot of defaults set, so getting started should be pretty easy. In fact, if you don’t want any email notifications, you basically just need to include the Wrangler module in your controller:

application_controller.rb

class ApplicationController < ActionController::Base
include Wrangler

...

end

=== Enabling email notifications
Email notifications are configured to be sent with the default configuration, but you’ll need to specify from and to addresses for emails to actually be sent. So that brings us to the configuration of Wrangler. Recommended: just create a new initializer file (create a wrangler.rb (or whatever you want to name the file) in RAILS_ROOT/config/initializers/wrangler.rb). In it, add the following:

Wrangler::ExceptionNotifier.configure do |notifier_config|
notifier_config.merge! :from_address => ‘[email protected]’,
:recipient_addresses => [‘[email protected]’]
end

And, if you haven’t already configured ActionMailer to send emails, you’ll need to do that (e.g. setting ActionMailer::Base.smtp_settings), and even after you have, you may want to change some settings in order to send from a different account from the one you may use to email your users (e.g. change the :user_name for smtp_settings):

Wrangler::ExceptionNotifier.smtp_settings.merge! :user_name => ‘[email protected]

(Recommend just putting that in the same wrangler.rb initializer you created above)

For more info on smtp_settings, see ActionMailer [http://am.rubyonrails.org/]

== Configuration
There are two different classes that receive configuration, ExceptionHandler and ExceptionNotifier.

ExceptionHandler stores configurations about what to do about exceptions (whether to handle them, email them, which error templates to render for each exception…).

ExceptionNotifier handles the sending of emails when exceptions occur, so stores configurations about where to send the emails.

You override defaults on each using the same syntax (as seen above) by calling the configure() method on the class and using the config hash that is yielded to set your configurations. See the method documentation for the configure() methods themselves, but here’s the basic idea:

Wrangler::ExceptionHandler.configure do |handler_config|
handler_config[:key1] = value1
handler_config[:key2] = value2
handler_config[:key_for_a_hash].merge! :subkey => value
handler_config[:key_for_an_array] << another_value
end

OR

Wrangler::ExceptionHandler.configure do |handler_config|
handler_config.merge! :key1 => value1,
:key2 => value2,
handler_config[:key_for_a_hash].merge! :subkey => value
handler_config[:key_for_an_array] << another_value
end

(same with Wrangler::ExceptionNotifier, except different classname)

Most configurations are single values (e.g. nums or strings), but some are hashes or arrays. You can either overwrite the hashes/arrays, or selectively delete, or just append. Recommend just appending to the defaults in most cases, but if you know what you’re doing, you can do whatever you like!

Here is the full set of configuration values for both classes, as well as their default values (pasted in from the classes, so you can check the code directly to make sure you’ve got the latest! 😃 ):

####################

ExceptionHandler:

####################

  :app_name => '',
  :handle_local_errors => false,
  :handle_public_errors => true,
  # send email for local reqeusts. ignored if :handle_local_errors false
  :notify_on_local_error => false,
  # send email for public requests. ignored if :handle_public_errors false
  :notify_on_public_error => true,
  # send email for exceptions caught outside of a controller context
  :notify_on_background_error => true,
  # configure regular expressions for specific HTTP headers, which if they
  # match will NOT send notification emails (e.g. for filtering out crazy
  # browsers).
  # array of hashes, each with one key (http header string) and a value
  # (regexp to match in http header string). this allows for more than one
  # regexp to be specified for the same header if that's more convenient.
  # any one pattern match will block notification.
  # note that this looks in all request headers, including query params
  # (e.g. form fields, url query params)
  :block_notify_on_request_headers => [],
  # configure whether to send emails synchronously or asynchronously
  # using delayed_job (these can be true even if delayed job is not
  # installed, in which case, will just send emails synchronously anyway)
  :delayed_job_for_controller_errors => true,
  :delayed_job_for_non_controller_errors => true,
  # mappings from exception classes to http status codes (see above)
  # add/remove from this list as desired in environment configuration
  # note that the keys are String names of the classes, not the class
  # constants themselves
  # (e.g. "StandardError" => "500",    and _NOT_ StandardError => "500")
  :error_class_status_codes => Wrangler::codes_for_exception_classes,
  # explicitly indicate which exceptions to send email notifications for
  :notify_exception_classes => %w(),
  # indicate which http status codes should result in email notification
  :notify_status_codes => %w( 405 500 503 ),
  # where to look for app-specific error page templates (ones you create
  # yourself, for example...there are some defaults in this gem you can
  # use as well...and that are configured already by default)
  :error_template_dir => (defined?(RAILS_ROOT) ? File.join(RAILS_ROOT, 'app', 'views', 'error') : nil),
  # explicit mappings from exception class to arbitrary error page
  # templates, different set for html and js responses (Wrangler determines
  # which to use automatically, so you can have an entry in both
  # hashes for the same error class)
  # note that the keys are String names of the classes, not the class
  # constants themselves
  # (e.g. "StandardError" => "file",    and _NOT_ StandardError => "file")
  :error_class_html_templates => {},
  :error_class_js_templates => {},
  # you can specify options to pass to the render() call when rendering
  # error page for errors. This is applied globally, so plan accordingly
  # (one good option is to pass in :layout => 'my_layout' so your app
  # layout gets applied to the error templates)
  :render_error_options => {},
  # you can specify a fallback failsafe error template to render if
  # no appropriate template is found in the usual places (you shouldn't
  # rely on this, and error messages will be logged if this template is
  # used). note: there's an even more failsafe template included in the
  # gem (absolute_last_resort...) below, but DON'T CHANGE IT!!!
  :default_error_template => '',
  # these filter out any HTTP params that are undesired
  :request_env_to_skip => [ /^rack\./,
                            "action_controller.rescue.request",
                            "action_controller.rescue.response" ],
  # mapping from exception classes to templates (if desired), express
  # in absolute paths. use wildcards like on cmd line (glob-like), NOT
  # regexp-style

  # just DON'T change this! this is the error template of last resort!
  # if you do change this, you really should have a good reason for it and
  # really know what you're doing. really.
  :absolute_last_resort_default_error_template =>
    File.join(WRANGLER_ROOT,'rails','app','views','wrangler','500.html'),
  # allows for inserting additional data/comments/status/environment into
  # the notification emails. possibilities include fetching the release number,
  # patch info, current state of different services....
  # pass a proc instance that accepts a single
  # argument, the request object (if any).
  # (e.g. +Proc.new { |request| puts "request is: #{request}" }+, but don't
  # do that, that would be redundant, silly and not secure).
  # NOTE: +request+ can be nil (if background process, for example), so
  # implement accordingly
  :call_for_supplementary_info => nil

#####################

ExceptionNotifier:

#####################

  # who the emails will be coming from. if nil or missing or empty string,
  # effectively disables email notification
  :from_address => '',
  # array of addresses that the emails will be sent to. if nil or missing
  # or empty array, effectively disables email notification.
  :recipient_addresses => [],
  # what will show up at the beginning of the subject line for each email
  # sent note: will be preceded by "[<app_name (if any)>...", where app_name
  # is the :app_name config value from ExceptionHandler (or explicit
  # proc_name given to notify_on_error() method)
  :subject_prefix => "#{(defined?(Rails) ? Rails.env : RAILS_ENV).capitalize} ERROR",
  # can use this to define app-specific mail views using the same data
  # made available in exception_notification()
  :mailer_template_root => File.join(WRANGLER_ROOT, 'views')

== Search algorithm for error templates (given an exception and a status_code):
When trying to find an appropriate error page template to render, Wrangler goes through several different attempts to locate an appropriate template, beginning with templates you’ve explicitly associated with the exception class or status code that has arisen…and on through to assuming default file naming conventions and finally failsafe default templates.

  1. if there is an explicit mapping from the exception to an error page in :error_class_xxx_templates, use that
  2. if there is a mapping in :error_class_templates for which the exception returns true to an is_a? call, use that
  3. if there is a file/template corresponding to the exception name (underscorified) in one of the following locations, use that:
    config[:error_template_dir]/
    RAILS_ROOT/public/
    WRANGLER_ROOT/rails/app/views/wrangler/
  4. if there is a file/template corresponding to the status code (e.g. named ###.html.erb where ### is the status code) in one of the following locations, use that:
    config[:error_template_dir]/
    RAILS_ROOT/public/
    WRANGLER_ROOT/rails/app/views/wrangler/
  5. if there is a file/template corresponding to a parent class name of the exception (underscorified) one of the following locations, use that:
    config[:error_template_dir]/
    RAILS_ROOT/public/
    WRANGLER_ROOT/rails/app/views/wrangler/
  6. :default_error_template
  7. :absolute_last_resort_default_error_template

== Using outside a Controller:
You can still use Wrangler outside the context of a Controller class. If you’ll be running within the context of an object instance, you can just include Wrangler in the object’s class. If you’ll be running ‘static’ code, you can refer to relevant methods via the Wrangler module. Note that in both cases, you’ll be calling the notify_on_error() method. Also note that the notify_on_error() method will re-raise the exception that occurred in the block, so you may want to begin/rescue/end around the notify_on_error() method call

using in an object instance:

class MyClass
include Wrangler

 def my_error_method; raise "error!"; end
 
 def call_a_method
   notify_on_error { my_error_method }
 rescue => e
   exit
 end

end

using ‘statically’:

Wrangler::notify_on_error { run_some_method_that_might_raise_exceptions }

== Maintaining the Wrangler gem
Should be pretty straightforward. Note that we’re using jeweler, so the .gemspec isn’t included in the git repos; it gets generated dynamically from the settings in Rakefile.

To build:

 cd .../wrangler
 rake gemspec
 rake build

To test:
Now at least, wrangler testing is all manual, which is bad (see TODO). So to test, try at least some of the following cases:

  • enable/disable local exception handling
  • enable/disable local notification
  • enable/disable delayed_job notification
  • raise an exception with a status_code
  • raise an exception without a status_code
  • raise an exception explicitly set to result in notification
  • raise an exception with ancestor class set to result in notification
  • raise an exception with error status code set to result in notification
  • raise an exception with explicit class mapping to error page
  • raise an exception with ancestor class mapping to error page
  • raise an exception with status_code mapping to error page
  • raise an ADDITIONAL exception inside the exception handling code…