exceptify 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +16 -0
  3. data/CODE_OF_CONDUCT.md +22 -0
  4. data/CONTRIBUTING.md +33 -0
  5. data/MIT-LICENSE +23 -0
  6. data/README.md +534 -0
  7. data/RELEASING.md +51 -0
  8. data/Rakefile +25 -0
  9. data/docs/notifiers/custom.md +42 -0
  10. data/docs/notifiers/datadog.md +51 -0
  11. data/docs/notifiers/email.md +195 -0
  12. data/docs/notifiers/slack.md +154 -0
  13. data/docs/notifiers/sns.md +37 -0
  14. data/docs/notifiers/teams.md +54 -0
  15. data/docs/notifiers/webhook.md +60 -0
  16. data/exceptify.gemspec +48 -0
  17. data/lib/exceptify/base_notifier.rb +30 -0
  18. data/lib/exceptify/configuration.rb +184 -0
  19. data/lib/exceptify/datadog_notifier.rb +160 -0
  20. data/lib/exceptify/dispatcher.rb +49 -0
  21. data/lib/exceptify/email_notifier.rb +208 -0
  22. data/lib/exceptify/modules/backtrace_cleaner.rb +13 -0
  23. data/lib/exceptify/modules/error_grouping.rb +170 -0
  24. data/lib/exceptify/modules/formatter.rb +119 -0
  25. data/lib/exceptify/notification.rb +71 -0
  26. data/lib/exceptify/notifier.rb +19 -0
  27. data/lib/exceptify/notifier_registry.rb +55 -0
  28. data/lib/exceptify/rack.rb +88 -0
  29. data/lib/exceptify/rails/runner_tie.rb +57 -0
  30. data/lib/exceptify/rails.rb +29 -0
  31. data/lib/exceptify/rake.rb +59 -0
  32. data/lib/exceptify/request_context.rb +35 -0
  33. data/lib/exceptify/resque.rb +25 -0
  34. data/lib/exceptify/sidekiq.rb +15 -0
  35. data/lib/exceptify/slack_notifier.rb +141 -0
  36. data/lib/exceptify/sns_notifier.rb +98 -0
  37. data/lib/exceptify/solid_queue.rb +68 -0
  38. data/lib/exceptify/teams_notifier.rb +209 -0
  39. data/lib/exceptify/version.rb +5 -0
  40. data/lib/exceptify/views/exceptify/_backtrace.html.erb +3 -0
  41. data/lib/exceptify/views/exceptify/_backtrace.text.erb +1 -0
  42. data/lib/exceptify/views/exceptify/_data.html.erb +6 -0
  43. data/lib/exceptify/views/exceptify/_data.text.erb +1 -0
  44. data/lib/exceptify/views/exceptify/_environment.html.erb +10 -0
  45. data/lib/exceptify/views/exceptify/_environment.text.erb +5 -0
  46. data/lib/exceptify/views/exceptify/_request.html.erb +36 -0
  47. data/lib/exceptify/views/exceptify/_request.text.erb +10 -0
  48. data/lib/exceptify/views/exceptify/_session.html.erb +10 -0
  49. data/lib/exceptify/views/exceptify/_session.text.erb +2 -0
  50. data/lib/exceptify/views/exceptify/_title.html.erb +3 -0
  51. data/lib/exceptify/views/exceptify/_title.text.erb +3 -0
  52. data/lib/exceptify/views/exceptify/background_exceptify.html.erb +53 -0
  53. data/lib/exceptify/views/exceptify/background_exceptify.text.erb +14 -0
  54. data/lib/exceptify/views/exceptify/exceptify.html.erb +52 -0
  55. data/lib/exceptify/views/exceptify/exceptify.text.erb +24 -0
  56. data/lib/exceptify/webhook_notifier.rb +63 -0
  57. data/lib/exceptify.rb +177 -0
  58. data/lib/generators/exceptify/install_generator.rb +24 -0
  59. data/lib/generators/exceptify/templates/exceptify.rb.erb +44 -0
  60. metadata +364 -0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exceptify
4
+ class BaseNotifier
5
+ attr_accessor :base_options
6
+
7
+ def initialize(options = {})
8
+ @base_options = options
9
+ end
10
+
11
+ def send_notice(exception, options, message, message_opts = nil)
12
+ _pre_callback(exception, options, message, message_opts)
13
+ result = yield(message, message_opts)
14
+ _post_callback(exception, options, message, message_opts)
15
+ result
16
+ end
17
+
18
+ def _pre_callback(exception, options, message, message_opts)
19
+ return unless @base_options[:pre_callback].respond_to?(:call)
20
+
21
+ @base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts)
22
+ end
23
+
24
+ def _post_callback(exception, options, message, message_opts)
25
+ return unless @base_options[:post_callback].respond_to?(:call)
26
+
27
+ @base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "active_support/core_ext/numeric/time"
5
+ require "active_support/cache"
6
+ require "exceptify/notifier_registry"
7
+ require "exceptify/modules/error_grouping"
8
+
9
+ module Exceptify
10
+ class Configuration
11
+ attr_accessor :logger,
12
+ :ignored_exceptions,
13
+ :testing_mode,
14
+ :error_grouping,
15
+ :error_grouping_period,
16
+ :notification_trigger,
17
+ :error_grouping_cache,
18
+ :fallback_cache_store
19
+
20
+ attr_reader :notifier_registry
21
+
22
+ DEFAULT_IGNORED_EXCEPTIONS = %w[
23
+ ActiveRecord::RecordNotFound Mongoid::Errors::DocumentNotFound AbstractController::ActionNotFound
24
+ ActionController::RoutingError ActionController::UnknownFormat ActionController::UrlGenerationError
25
+ ActionDispatch::Http::MimeNegotiation::InvalidType Rack::Utils::InvalidParameterError
26
+ ].freeze
27
+
28
+ def initialize(
29
+ logger: Logger.new($stdout),
30
+ ignored_exceptions: DEFAULT_IGNORED_EXCEPTIONS,
31
+ notifier_registry: NotifierRegistry.new,
32
+ error_grouping_cache: nil,
33
+ fallback_cache_store: ActiveSupport::Cache::MemoryStore.new
34
+ )
35
+ @logger = logger
36
+ @ignored_exceptions = ignored_exceptions.dup
37
+ @testing_mode = false
38
+ @error_grouping = false
39
+ @error_grouping_period = 5.minutes
40
+ @notification_trigger = nil
41
+ @error_grouping_cache = error_grouping_cache
42
+ @fallback_cache_store = fallback_cache_store
43
+ @notifier_registry = notifier_registry
44
+ @ignores = []
45
+ @by_notifier_ignores = {}
46
+ end
47
+
48
+ def copy
49
+ self.class.new(
50
+ logger: logger,
51
+ ignored_exceptions: ignored_exceptions,
52
+ notifier_registry: notifier_registry.copy,
53
+ error_grouping_cache: error_grouping_cache,
54
+ fallback_cache_store: fallback_cache_store
55
+ ).tap do |configuration|
56
+ configuration.testing_mode = testing_mode
57
+ configuration.error_grouping = error_grouping
58
+ configuration.error_grouping_period = error_grouping_period
59
+ configuration.notification_trigger = notification_trigger
60
+ ignores.each { |condition| configuration.ignore_if(&condition) }
61
+ by_notifier_ignores.each { |notifier, condition| configuration.ignore_notifier_if(notifier, &condition) }
62
+ end
63
+ end
64
+
65
+ def reset!
66
+ notifier_registry.clear
67
+ clear_ignore_conditions!
68
+ self.error_grouping = false
69
+ self.notification_trigger = nil
70
+ self.error_grouping_cache = nil
71
+ fallback_cache_store.clear if fallback_cache_store.respond_to?(:clear)
72
+ end
73
+
74
+ def testing_mode!
75
+ self.testing_mode = true
76
+ end
77
+
78
+ def register_notifier(name, notifier_or_options)
79
+ notifier_registry.register(name, notifier_or_options)
80
+ end
81
+ alias_method :add_notifier, :register_notifier
82
+
83
+ def unregister_notifier(name)
84
+ notifier_registry.unregister(name)
85
+ end
86
+
87
+ def registered_notifier(name)
88
+ notifier_registry.fetch(name)
89
+ end
90
+
91
+ def notifiers
92
+ notifier_registry.names
93
+ end
94
+
95
+ def ignore_if(&block)
96
+ ignores << block
97
+ end
98
+
99
+ def ignore_notifier_if(notifier, &block)
100
+ by_notifier_ignores[notifier] = block
101
+ end
102
+
103
+ def ignore_crawlers(crawlers)
104
+ ignore_if do |_exception, opts|
105
+ opts.key?(:env) && from_crawler(opts[:env], crawlers)
106
+ end
107
+ end
108
+
109
+ def clear_ignore_conditions!
110
+ ignores.clear
111
+ by_notifier_ignores.clear
112
+ end
113
+
114
+ def ignored?(exception, options)
115
+ ignores.any? { |condition| condition.call(exception, options) }
116
+ rescue Exception => e # standard:disable Lint/RescueException
117
+ raise e if testing_mode
118
+
119
+ logger.warn(
120
+ "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
121
+ )
122
+ false
123
+ end
124
+
125
+ def notifier_ignored?(exception, options, notifier:)
126
+ return false unless by_notifier_ignores.key?(notifier)
127
+
128
+ by_notifier_ignores[notifier].call(exception, options)
129
+ rescue Exception => e # standard:disable Lint/RescueException
130
+ raise e if testing_mode
131
+
132
+ logger.warn(<<~"MESSAGE")
133
+ An error occurred when evaluating a by-notifier ignore condition. #{e.class}: #{e.message}
134
+ #{e.backtrace.join("\n")}
135
+ MESSAGE
136
+ false
137
+ end
138
+
139
+ def ignored_exception?(ignore_array, exception)
140
+ all_ignored_exceptions = (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s)
141
+ exception_ancestors = exception.singleton_class.ancestors.map(&:to_s)
142
+ !(all_ignored_exceptions & exception_ancestors).empty?
143
+ end
144
+
145
+ def error_count(error_key)
146
+ error_grouping_service.error_count(error_key)
147
+ end
148
+
149
+ def save_error_count(error_key, count)
150
+ error_grouping_service.save_error_count(error_key, count)
151
+ end
152
+
153
+ def group_error!(exception, options)
154
+ error_grouping_service.group_error!(exception, options)
155
+ end
156
+
157
+ def send_notification?(exception, count)
158
+ error_grouping_service.send_notification?(exception, count)
159
+ end
160
+
161
+ protected
162
+
163
+ attr_reader :ignores, :by_notifier_ignores
164
+
165
+ private
166
+
167
+ def error_grouping_service
168
+ ErrorGrouping::Service.new(
169
+ cache: error_grouping_cache,
170
+ fallback_cache_store: fallback_cache_store,
171
+ period: error_grouping_period,
172
+ notification_trigger: notification_trigger,
173
+ logger: logger
174
+ )
175
+ end
176
+
177
+ def from_crawler(env, ignored_crawlers)
178
+ agent = env["HTTP_USER_AGENT"]
179
+ Array(ignored_crawlers).any? do |crawler|
180
+ agent =~ Regexp.new(crawler)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+
5
+ module Exceptify
6
+ class DatadogNotifier < BaseNotifier
7
+ attr_reader :client,
8
+ :default_options
9
+
10
+ def initialize(options)
11
+ super
12
+ @client = options[:client]
13
+ raise ArgumentError, "You must provide 'client' option" unless @client
14
+
15
+ @default_options = options
16
+ end
17
+
18
+ def call(exception, options = {})
19
+ client.emit_event(
20
+ datadog_event(exception, options)
21
+ )
22
+ end
23
+
24
+ def datadog_event(exception, options = {})
25
+ DatadogExceptionEvent.new(
26
+ exception,
27
+ options.reverse_merge(default_options)
28
+ ).event
29
+ end
30
+
31
+ class DatadogExceptionEvent
32
+ include Exceptify::BacktraceCleaner
33
+
34
+ MAX_TITLE_LENGTH = 120
35
+ MAX_VALUE_LENGTH = 300
36
+ MAX_BACKTRACE_SIZE = 3
37
+ ALERT_TYPE = "error"
38
+
39
+ attr_reader :exception,
40
+ :notification,
41
+ :options
42
+
43
+ def initialize(exception, options)
44
+ @notification = Notification.new(exception, options, backtrace_cleaner: self)
45
+ @exception = notification.exception
46
+ @options = options
47
+ end
48
+
49
+ def request
50
+ @request ||= notification.request_context.request
51
+ end
52
+
53
+ def controller
54
+ @controller ||= notification.controller
55
+ end
56
+
57
+ def backtrace
58
+ @backtrace ||= notification.backtrace
59
+ end
60
+
61
+ def tags
62
+ options[:tags] || []
63
+ end
64
+
65
+ def title_prefix
66
+ options[:title_prefix] || ""
67
+ end
68
+
69
+ def event
70
+ title = formatted_title
71
+ body = formatted_body
72
+
73
+ Dogapi::Event.new(
74
+ body,
75
+ msg_title: title,
76
+ alert_type: ALERT_TYPE,
77
+ tags: tags,
78
+ aggregation_key: [title]
79
+ )
80
+ end
81
+
82
+ def formatted_title
83
+ title =
84
+ "#{title_prefix}#{controller_subtitle} (#{exception.class}) #{exception.message.inspect}"
85
+
86
+ truncate(title, MAX_TITLE_LENGTH)
87
+ end
88
+
89
+ def formatted_body
90
+ text = []
91
+
92
+ text << "%%%"
93
+ text << formatted_request if request
94
+ text << formatted_session if request
95
+ text << formatted_backtrace
96
+ text << "%%%"
97
+
98
+ text.join("\n")
99
+ end
100
+
101
+ def formatted_key_value(key, value)
102
+ "**#{key}:** #{value}"
103
+ end
104
+
105
+ def formatted_request
106
+ text = []
107
+ text << "### **Request**"
108
+ text << formatted_key_value("URL", request.url)
109
+ text << formatted_key_value("HTTP Method", request.request_method)
110
+ text << formatted_key_value("IP Address", request.remote_ip)
111
+ text << formatted_key_value("Parameters", request.filtered_parameters.inspect)
112
+ text << formatted_key_value("Timestamp", notification.timestamp)
113
+ text << formatted_key_value("Server", notification.hostname)
114
+ text << formatted_key_value("Rails root", Rails.root) if defined?(Rails) && Rails.respond_to?(:root)
115
+ text << formatted_key_value("Process", Process.pid)
116
+ text << "___"
117
+ text.join("\n")
118
+ end
119
+
120
+ def formatted_session
121
+ text = []
122
+ text << "### **Session**"
123
+ text << formatted_key_value("Data", request.session.to_hash)
124
+ text << "___"
125
+ text.join("\n")
126
+ end
127
+
128
+ def formatted_backtrace
129
+ size = [backtrace.size, MAX_BACKTRACE_SIZE].min
130
+
131
+ text = []
132
+ text << "### **Backtrace**"
133
+ text << "````"
134
+ size.times { |i| text << backtrace[i] }
135
+ text << "````"
136
+ text << "___"
137
+ text.join("\n")
138
+ end
139
+
140
+ def truncate(string, max)
141
+ (string.length > max) ? "#{string[0...max]}..." : string
142
+ end
143
+
144
+ def inspect_object(object)
145
+ case object
146
+ when Hash, Array
147
+ truncate(object.inspect, MAX_VALUE_LENGTH)
148
+ else
149
+ object.to_s
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def controller_subtitle
156
+ "#{controller.controller_name} #{controller.action_name}" if controller
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exceptify
4
+ class Dispatcher
5
+ attr_reader :configuration
6
+
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ end
10
+
11
+ def notify_exception(exception, options = {}, &block)
12
+ options = options.dup
13
+
14
+ return false if configuration.ignored_exception?(options[:ignore_exceptions], exception)
15
+ return false if configuration.ignored?(exception, options)
16
+
17
+ if configuration.error_grouping
18
+ errors_count = configuration.group_error!(exception, options)
19
+ return false unless configuration.send_notification?(exception, errors_count)
20
+ end
21
+
22
+ notification_fired = false
23
+ selected_notifiers = options.delete(:notifiers) || configuration.notifiers
24
+ [*selected_notifiers].each do |notifier|
25
+ unless configuration.notifier_ignored?(exception, options, notifier: notifier)
26
+ fire_notification(notifier, exception, options.dup, &block)
27
+ notification_fired = true
28
+ end
29
+ end
30
+
31
+ notification_fired
32
+ end
33
+
34
+ private
35
+
36
+ def fire_notification(notifier_name, exception, options, &block)
37
+ notifier = configuration.registered_notifier(notifier_name)
38
+ notifier.call(exception, options, &block)
39
+ rescue Exception => e # standard:disable Lint/RescueException
40
+ raise e if configuration.testing_mode
41
+
42
+ configuration.logger.warn(
43
+ "An error occurred when sending a notification using '#{notifier_name}' notifier." \
44
+ "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
45
+ )
46
+ false
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/time"
4
+ require "action_mailer"
5
+ require "action_dispatch"
6
+ require "pp"
7
+
8
+ module Exceptify
9
+ class EmailNotifier < BaseNotifier
10
+ DEFAULT_OPTIONS = {
11
+ sender_address: %("Exceptify" <notifier@example.com>),
12
+ exception_recipients: [],
13
+ email_prefix: "[ERROR] ",
14
+ email_format: :text,
15
+ sections: %w[request session environment backtrace],
16
+ background_sections: %w[backtrace data],
17
+ verbose_subject: true,
18
+ normalize_subject: false,
19
+ include_controller_and_action_names_in_subject: true,
20
+ delivery_method: nil,
21
+ mailer_settings: nil,
22
+ email_headers: {},
23
+ mailer_parent: "ActionMailer::Base",
24
+ template_path: "exceptify",
25
+ deliver_with: nil
26
+ }.freeze
27
+
28
+ module Mailer
29
+ class MissingController
30
+ def method_missing(*args, &block)
31
+ end
32
+
33
+ def respond_to_missing?(*args)
34
+ end
35
+ end
36
+
37
+ def self.extended(base)
38
+ base.class_eval do
39
+ send(:include, Exceptify::BacktraceCleaner)
40
+
41
+ # Append application view path to the Exceptify lookup context.
42
+ append_view_path "#{File.dirname(__FILE__)}/views"
43
+
44
+ def exceptify(env, exception, options = {}, default_options = {})
45
+ load_custom_views
46
+
47
+ @env = env
48
+ @exception = exception
49
+
50
+ env_options = env["exceptify.options"] || {}
51
+ @options = default_options.merge(env_options).merge(options)
52
+
53
+ @kontroller = env["action_controller.instance"] || MissingController.new
54
+ @request = ActionDispatch::Request.new(env)
55
+ @backtrace = exception.backtrace ? clean_backtrace(exception) : []
56
+ @timestamp = Time.current
57
+ @sections = @options[:sections]
58
+ @data = (env["exceptify.exception_data"] || {}).merge(options[:data] || {})
59
+ @sections += %w[data] unless @data.empty?
60
+
61
+ compose_email
62
+ end
63
+
64
+ def background_exceptify(exception, options = {}, default_options = {})
65
+ load_custom_views
66
+
67
+ @exception = exception
68
+ @options = default_options.merge(options).symbolize_keys
69
+ @backtrace = exception.backtrace || []
70
+ @timestamp = Time.current
71
+ @sections = @options[:background_sections]
72
+ @data = options[:data] || {}
73
+ @env = @kontroller = nil
74
+
75
+ compose_email
76
+ end
77
+
78
+ private
79
+
80
+ def compose_subject
81
+ subject = @options[:email_prefix].to_s.dup
82
+ subject << "(#{@options[:accumulated_errors_count]} times)" if @options[:accumulated_errors_count].to_i > 1
83
+ subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" if include_controller?
84
+ subject << " (#{@exception.class})"
85
+ subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
86
+ subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
87
+ (subject.length > 120) ? subject[0...120] + "..." : subject
88
+ end
89
+
90
+ def include_controller?
91
+ @kontroller && @options[:include_controller_and_action_names_in_subject]
92
+ end
93
+
94
+ def set_data_variables
95
+ @data.each do |name, value|
96
+ instance_variable_set(:"@#{name}", value)
97
+ end
98
+ end
99
+
100
+ helper_method :inspect_object
101
+
102
+ def truncate(string, max)
103
+ (string.length > max) ? "#{string[0...max]}..." : string
104
+ end
105
+
106
+ def inspect_object(object)
107
+ case object
108
+ when Hash, Array
109
+ truncate(object.inspect, 300)
110
+ else
111
+ object.to_s
112
+ end
113
+ end
114
+
115
+ helper_method :safe_encode
116
+
117
+ def safe_encode(value)
118
+ value.encode("utf-8", invalid: :replace, undef: :replace, replace: "_")
119
+ end
120
+
121
+ def html_mail?
122
+ @options[:email_format] == :html
123
+ end
124
+
125
+ def compose_email
126
+ set_data_variables
127
+ subject = compose_subject
128
+ name = @env.nil? ? "background_exceptify" : "exceptify"
129
+ exception_recipients = maybe_call(@options[:exception_recipients])
130
+
131
+ headers = {
132
+ delivery_method: @options[:delivery_method],
133
+ to: exception_recipients,
134
+ from: @options[:sender_address],
135
+ subject: subject,
136
+ template_name: name
137
+ }.merge(@options[:email_headers])
138
+
139
+ mail = mail(headers) do |format|
140
+ format.text
141
+ format.html if html_mail?
142
+ end
143
+
144
+ mail.delivery_method.settings.merge!(@options[:mailer_settings]) if @options[:mailer_settings]
145
+
146
+ mail
147
+ end
148
+
149
+ def load_custom_views
150
+ return unless defined?(Rails) && Rails.respond_to?(:root)
151
+
152
+ prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views"
153
+ end
154
+
155
+ def maybe_call(maybe_proc)
156
+ maybe_proc.respond_to?(:call) ? maybe_proc.call : maybe_proc
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def initialize(options)
163
+ super
164
+
165
+ delivery_method = options[:delivery_method] || :smtp
166
+ mailer_settings_key = :"#{delivery_method}_settings"
167
+ options[:mailer_settings] = options.delete(mailer_settings_key)
168
+
169
+ @base_options = DEFAULT_OPTIONS.merge(options)
170
+ end
171
+
172
+ def call(exception, options = {})
173
+ message = create_email(exception, options)
174
+
175
+ message.send(base_options[:deliver_with] || default_deliver_with(message))
176
+ end
177
+
178
+ def create_email(exception, options = {})
179
+ env = options[:env]
180
+
181
+ send_notice(exception, options, nil, base_options) do |_, default_opts|
182
+ if env.nil?
183
+ mailer.background_exceptify(exception, options, default_opts)
184
+ else
185
+ mailer.exceptify(env, exception, options, default_opts)
186
+ end
187
+ end
188
+ end
189
+
190
+ def self.normalize_digits(string)
191
+ string.gsub(/[0-9]+/, "N")
192
+ end
193
+
194
+ private
195
+
196
+ def mailer
197
+ @mailer ||= Class.new(base_options[:mailer_parent].constantize).tap do |mailer|
198
+ mailer.extend(EmailNotifier::Mailer)
199
+ mailer.mailer_name = base_options[:template_path]
200
+ end
201
+ end
202
+
203
+ def default_deliver_with(message)
204
+ # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')`
205
+ message.respond_to?(:deliver_now) ? :deliver_now : :deliver
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exceptify
4
+ module BacktraceCleaner
5
+ def clean_backtrace(exception)
6
+ if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
7
+ Rails.backtrace_cleaner.send(:filter, exception.backtrace)
8
+ else
9
+ exception.backtrace
10
+ end
11
+ end
12
+ end
13
+ end