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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
|
# frozen_string_literal: true
require "active_support/core_ext/time"
require "action_mailer"
require "action_dispatch"
require "pp"
module ExceptionNotifier
class EmailNotifier < BaseNotifier
DEFAULT_OPTIONS = {
sender_address: %("Exception Notifier" <exception.notifier@example.com>),
exception_recipients: [],
email_prefix: "[ERROR] ",
email_format: :text,
sections: %w[request session environment backtrace],
background_sections: %w[backtrace data],
verbose_subject: true,
normalize_subject: false,
include_controller_and_action_names_in_subject: true,
delivery_method: nil,
mailer_settings: nil,
email_headers: {},
mailer_parent: "ActionMailer::Base",
template_path: "exception_notifier",
deliver_with: nil
}.freeze
module Mailer
class MissingController
def method_missing(*args, &block)
end
def respond_to_missing?(*args)
end
end
def self.extended(base)
base.class_eval do
send(:include, ExceptionNotifier::BacktraceCleaner)
# Append application view path to the ExceptionNotifier lookup context.
append_view_path "#{File.dirname(__FILE__)}/views"
def exception_notification(env, exception, options = {}, default_options = {})
load_custom_views
@env = env
@exception = exception
env_options = env["exception_notifier.options"] || {}
@options = default_options.merge(env_options).merge(options)
@kontroller = env["action_controller.instance"] || MissingController.new
@request = ActionDispatch::Request.new(env)
@backtrace = exception.backtrace ? clean_backtrace(exception) : []
@timestamp = Time.current
@sections = @options[:sections]
@data = (env["exception_notifier.exception_data"] || {}).merge(options[:data] || {})
@sections += %w[data] unless @data.empty?
compose_email
end
def background_exception_notification(exception, options = {}, default_options = {})
load_custom_views
@exception = exception
@options = default_options.merge(options).symbolize_keys
@backtrace = exception.backtrace || []
@timestamp = Time.current
@sections = @options[:background_sections]
@data = options[:data] || {}
@env = @kontroller = nil
compose_email
end
private
def compose_subject
subject = @options[:email_prefix].to_s.dup
subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1
subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" if include_controller?
subject << " (#{@exception.class})"
subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
(subject.length > 120) ? subject[0...120] + "..." : subject
end
def include_controller?
@kontroller && @options[:include_controller_and_action_names_in_subject]
end
def set_data_variables
@data.each do |name, value|
instance_variable_set(:"@#{name}", value)
end
end
helper_method :inspect_object
def truncate(string, max)
(string.length > max) ? "#{string[0...max]}..." : string
end
def inspect_object(object)
case object
when Hash, Array
truncate(object.inspect, 300)
else
object.to_s
end
end
helper_method :safe_encode
def safe_encode(value)
value.encode("utf-8", invalid: :replace, undef: :replace, replace: "_")
end
def html_mail?
@options[:email_format] == :html
end
def compose_email
set_data_variables
subject = compose_subject
name = @env.nil? ? "background_exception_notification" : "exception_notification"
exception_recipients = maybe_call(@options[:exception_recipients])
headers = {
delivery_method: @options[:delivery_method],
to: exception_recipients,
from: @options[:sender_address],
subject: subject,
template_name: name
}.merge(@options[:email_headers])
mail = mail(headers) do |format|
format.text
format.html if html_mail?
end
mail.delivery_method.settings.merge!(@options[:mailer_settings]) if @options[:mailer_settings]
mail
end
def load_custom_views
return unless defined?(Rails) && Rails.respond_to?(:root)
prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views"
end
def maybe_call(maybe_proc)
maybe_proc.respond_to?(:call) ? maybe_proc.call : maybe_proc
end
end
end
end
def initialize(options)
super
delivery_method = options[:delivery_method] || :smtp
mailer_settings_key = :"#{delivery_method}_settings"
options[:mailer_settings] = options.delete(mailer_settings_key)
@base_options = DEFAULT_OPTIONS.merge(options)
end
def call(exception, options = {})
message = create_email(exception, options)
message.send(base_options[:deliver_with] || default_deliver_with(message))
end
def create_email(exception, options = {})
env = options[:env]
send_notice(exception, options, nil, base_options) do |_, default_opts|
if env.nil?
mailer.background_exception_notification(exception, options, default_opts)
else
mailer.exception_notification(env, exception, options, default_opts)
end
end
end
def self.normalize_digits(string)
string.gsub(/[0-9]+/, "N")
end
private
def mailer
@mailer ||= Class.new(base_options[:mailer_parent].constantize).tap do |mailer|
mailer.extend(EmailNotifier::Mailer)
mailer.mailer_name = base_options[:template_path]
end
end
def default_deliver_with(message)
# FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')`
message.respond_to?(:deliver_now) ? :deliver_now : :deliver
end
end
end
|