super_exception_notifier 3.0.2 → 3.0.4

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.
data/VERSION.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- :patch: 2
2
+ :patch: 4
3
3
  :major: 3
4
4
  :build:
5
5
  :minor: 0
@@ -0,0 +1,33 @@
1
+ #From rails/exception_notifier 2-3-stable branch - Not yet integrated into SuperExceptionNotifier...
2
+
3
+ #This didn't belong on ExceptionNotification::Notifier and made backtraces worse. To keep original functionality in place
4
+ #'ActionController::Base.send :include, ExceptionNotification::ConsiderLocal' or just include in your controller
5
+ module ExceptionNotification::ConsiderLocal
6
+ module ClassMethods
7
+ def self.included(target)
8
+ require 'ipaddr'
9
+ target.extend(ClassMethods)
10
+ end
11
+
12
+ def consider_local(*args)
13
+ local_addresses.concat(args.flatten.map { |a| IPAddr.new(a) })
14
+ end
15
+
16
+ def local_addresses
17
+ addresses = read_inheritable_attribute(:local_addresses)
18
+ unless addresses
19
+ addresses = [IPAddr.new("127.0.0.1")]
20
+ write_inheritable_attribute(:local_addresses, addresses)
21
+ end
22
+ addresses
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def local_request?
29
+ remote = IPAddr.new(request.remote_ip)
30
+ !self.class.local_addresses.detect { |addr| addr.include?(remote) }.nil?
31
+ end
32
+
33
+ end
@@ -0,0 +1,14 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module ExceptionNotification::CustomExceptionClasses
5
+
6
+ class AccessDenied < StandardError; end
7
+ class ResourceGone < StandardError; end
8
+ class NotImplemented < StandardError; end
9
+ class PageNotFound < StandardError; end
10
+ class InvalidMethod < StandardError; end
11
+ class CorruptData < StandardError; end
12
+ class MethodDisabled < StandardError; end
13
+
14
+ end
@@ -0,0 +1,48 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module ExceptionNotification::CustomExceptionMethods
5
+
6
+ protected
7
+
8
+ #For a while after disabling a route/URL that had been functional we should set it to resource gone to inform people to remove bookmarks.
9
+ def resource_gone
10
+ raise ResourceGone
11
+ end
12
+ #Then for things that have never existed or have not for a long time we call not_implemented
13
+ def not_implemented
14
+ raise NotImplemented
15
+ end
16
+ #Resources that must be requested with a specific HTTP Method (GET, PUT, POST, DELETE, AJAX, etc) but are requested otherwise should:
17
+ def invalid_method
18
+ raise InvalidMethod
19
+ end
20
+ #If your ever at a spot in the code that should never get reached, but corrupt data might get you there anyways then this is for you:
21
+ def corrupt_data
22
+ raise CorruptData
23
+ end
24
+ def page_not_found
25
+ raise PageNotFound
26
+ end
27
+ def record_not_found
28
+ raise ActiveRecord::RecordNotFound
29
+ end
30
+ def method_disabled
31
+ raise MethodDisabled
32
+ end
33
+ #The current user does not have enough privileges to access the requested resource
34
+ def access_denied
35
+ raise AccessDenied
36
+ end
37
+
38
+ def generic_error
39
+ error_stickie("Sorry, an error has occurred.")
40
+ corrupt_data
41
+ end
42
+
43
+ def invalid_page
44
+ error_stickie("Sorry, the page number you requested was not valid.")
45
+ page_not_found
46
+ end
47
+
48
+ end
@@ -0,0 +1,58 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module ExceptionNotification::DeprecatedMethods
5
+ @@namespacing = "Better namespacing to allow for ExceptionNotifiable.rb and ExceptionNotifiable to have similar APIs"
6
+ @@rails2ruby = "An effort to make this a 'Ruby' Gem and not a strictly 'Rails' Gem"
7
+
8
+ def http_error_codes
9
+ deprecation_warning("http_error_codes", "error_class_status_codes")
10
+ error_class_status_codes
11
+ end
12
+
13
+ def http_error_codes=(arg)
14
+ deprecation_warning("http_error_codes", "error_class_status_codes")
15
+ error_class_status_codes=(arg)
16
+ end
17
+
18
+ def rails_error_codes
19
+ deprecation_warning("rails_error_codes", "error_class_status_codes", @@rails2ruby)
20
+ error_class_status_codes
21
+ end
22
+
23
+ def rails_error_codes=(arg)
24
+ deprecation_warning("rails_error_codes=", "error_class_status_codes=", @@rails2ruby)
25
+ error_class_status_codes=(arg)
26
+ end
27
+
28
+ # Now defined in Object class by init.rb & ExceptionNotifiable.rb module,
29
+ # so we need to override them for with the controller settings
30
+ def exception_notifier_verbose
31
+ deprecation_warning("exception_notifier_verbose", "exception_notifiable_verbose", @@namespacing)
32
+ exception_notifiable_verbose
33
+ end
34
+ def silent_exceptions
35
+ deprecation_warning("silent_exceptions", "exception_notifiable_silent_exceptions", @@namespacing)
36
+ exception_notifiable_silent_exceptions
37
+ end
38
+ def notification_level
39
+ deprecation_warning("notification_level", "exception_notifiable_notification_level", @@namespacing)
40
+ exception_notifiable_notification_level
41
+ end
42
+ def exception_notifier_verbose=(arg)
43
+ deprecation_warning("exception_notifier_verbose=", "exception_notifiable_verbose=", @@namespacing)
44
+ exception_notifiable_verbose = arg
45
+ end
46
+ def silent_exceptions=(arg)
47
+ deprecation_warning("silent_exceptions=", "exception_notifiable_silent_exceptions=", @@namespacing)
48
+ exception_notifiable_silent_exceptions = arg
49
+ end
50
+ def notification_level=(arg)
51
+ deprecation_warning("notification_level=", "exception_notifiable_notification_level=", @@namespacing)
52
+ exception_notifiable_notification_level = arg
53
+ end
54
+
55
+ def deprecation_warning(old, new, reason = "")
56
+ puts "[DEPRECATION WARNING] ** Method '#{old}' has been replaced by '#{new}', please update your code.#{' Reason for change: ' + reason + '.' if reason}"
57
+ end
58
+ end
@@ -0,0 +1,205 @@
1
+ require 'ipaddr'
2
+
3
+ module ExceptionNotification::ExceptionNotifiable
4
+ include ExceptionNotification::NotifiableHelper
5
+
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+
9
+ # Adds the following class attributes to the classes that include ExceptionNotifiable
10
+ # HTTP status codes and what their 'English' status message is
11
+ base.cattr_accessor :http_status_codes
12
+ base.http_status_codes = HTTP_STATUS_CODES
13
+ # error_layout:
14
+ # can be defined at controller level to the name of the desired error layout,
15
+ # or set to true to render the controller's own default layout,
16
+ # or set to false to render errors with no layout
17
+ base.cattr_accessor :error_layout
18
+ base.error_layout = nil
19
+ # Rails error classes to rescue and how to rescue them (which error code to use)
20
+ base.cattr_accessor :error_class_status_codes
21
+ base.error_class_status_codes = self.codes_for_error_classes
22
+ # Verbosity of the gem
23
+ base.cattr_accessor :exception_notifiable_verbose
24
+ base.exception_notifiable_verbose = false
25
+ # Do Not Ever send error notification emails for these Error Classes
26
+ base.cattr_accessor :exception_notifiable_silent_exceptions
27
+ base.exception_notifiable_silent_exceptions = SILENT_EXCEPTIONS
28
+ # Notification Level
29
+ base.cattr_accessor :exception_notifiable_notification_level
30
+ base.exception_notifiable_notification_level = [:render, :email, :web_hooks]
31
+ # Since there is no concept of locality from a request here allow user to explicitly define which env's are noisy (send notifications)
32
+ base.cattr_accessor :exception_notifiable_noisy_environments
33
+ base.exception_notifiable_noisy_environments = [:production]
34
+
35
+ base.cattr_accessor :exception_notifiable_pass_through
36
+ base.exception_notifiable_pass_through = false
37
+ end
38
+
39
+ module ClassMethods
40
+ include ExceptionNotification::DeprecatedMethods
41
+
42
+ # specifies ip addresses that should be handled as though local
43
+ def consider_local(*args)
44
+ local_addresses.concat(args.flatten.map { |a| IPAddr.new(a) })
45
+ end
46
+
47
+ def local_addresses
48
+ addresses = read_inheritable_attribute(:local_addresses)
49
+ unless addresses
50
+ addresses = [IPAddr.new("127.0.0.1")]
51
+ write_inheritable_attribute(:local_addresses, addresses)
52
+ end
53
+ addresses
54
+ end
55
+
56
+ # set the exception_data deliverer OR retrieve the exception_data
57
+ def exception_data(deliverer = nil)
58
+ if deliverer
59
+ write_inheritable_attribute(:exception_data, deliverer)
60
+ else
61
+ read_inheritable_attribute(:exception_data)
62
+ end
63
+ end
64
+
65
+ def be_silent_for_exception?(exception)
66
+ self.exception_notifiable_silent_exceptions.respond_to?(:any?) && self.exception_notifiable_silent_exceptions.any? {|klass| klass === exception }
67
+ end
68
+
69
+ end
70
+
71
+ def be_silent_for_exception?(exception)
72
+ self.class.be_silent_for_exception?(exception)
73
+ end
74
+
75
+
76
+ private
77
+
78
+ def environment_is_noisy?
79
+ self.class.exception_notifiable_noisy_environments.include?(Rails.env)
80
+ end
81
+
82
+ def notification_level_sends_email?
83
+ self.class.exception_notifiable_notification_level.include?(:email)
84
+ end
85
+
86
+ def notification_level_sends_web_hooks?
87
+ self.class.exception_notifiable_notification_level.include?(:web_hooks)
88
+ end
89
+
90
+ def notification_level_renders?
91
+ self.class.exception_notifiable_notification_level.include?(:render)
92
+ end
93
+
94
+ # overrides Rails' local_request? method to also check any ip
95
+ # addresses specified through consider_local.
96
+ def local_request?
97
+ remote = IPAddr.new(request.remote_ip)
98
+ !self.class.local_addresses.detect { |addr| addr.include?(remote) }.nil?
99
+ end
100
+
101
+ # When the action being executed has its own local error handling (rescue)
102
+ # Or when the error accurs somewhere without a subsequent render (eg. method calls in console)
103
+ def rescue_with_handler(exception)
104
+ to_return = super
105
+ if to_return
106
+ verbose = self.class.exception_notifiable_verbose
107
+ puts "[RESCUE STYLE] rescue_with_handler" if verbose
108
+ data = get_exception_data
109
+ status_code = status_code_for_exception(exception)
110
+ #We only send email if it has been configured in environment
111
+ send_email = should_email_on_exception?(exception, status_code, verbose)
112
+ #We only send web hooks if they've been configured in environment
113
+ send_web_hooks = should_web_hook_on_exception?(exception, status_code, verbose)
114
+ the_blamed = ExceptionNotification::Notifier.config[:git_repo_path].nil? ? nil : lay_blame(exception)
115
+ rejected_sections = %w(request session)
116
+ # Debugging output
117
+ verbose_output(exception, status_code, "rescued by handler", send_email, send_web_hooks, nil, the_blamed, rejected_sections) if verbose
118
+ # Send the exception notification email
119
+ perform_exception_notify_mailing(exception, data, nil, the_blamed, verbose, rejected_sections) if send_email
120
+ # Send Web Hook requests
121
+ ExceptionNotification::HooksNotifier.deliver_exception_to_web_hooks(ExceptionNotification::Notifier.config, exception, self, request, data, the_blamed) if send_web_hooks
122
+ end
123
+ pass_it_on(exception)
124
+ to_return
125
+ end
126
+
127
+ # When the action being executed is letting SEN handle the exception completely
128
+ def rescue_action_in_public(exception)
129
+ # If the error class is NOT listed in the rails_errror_class hash then we get a generic 500 error:
130
+ # OTW if the error class is listed, but has a blank code or the code is == '200' then we get a custom error layout rendered
131
+ # OTW the error class is listed!
132
+ verbose = self.class.exception_notifiable_verbose
133
+ puts "[RESCUE STYLE] rescue_action_in_public" if verbose
134
+ status_code = status_code_for_exception(exception)
135
+ if status_code == '200'
136
+ notify_and_render_error_template(status_code, request, exception, ExceptionNotification::Notifier.get_view_path_for_class(exception, verbose), verbose)
137
+ else
138
+ notify_and_render_error_template(status_code, request, exception, ExceptionNotification::Notifier.get_view_path_for_status_code(status_code, verbose), verbose)
139
+ end
140
+ end
141
+
142
+ def notify_and_render_error_template(status_cd, request, exception, file_path, verbose = false)
143
+ status = self.class.http_status_codes[status_cd] ? status_cd + " " + self.class.http_status_codes[status_cd] : status_cd
144
+ data = get_exception_data
145
+ #We only send email if it has been configured in environment
146
+ send_email = should_email_on_exception?(exception, status_cd, verbose)
147
+ #We only send web hooks if they've been configured in environment
148
+ send_web_hooks = should_web_hook_on_exception?(exception, status_cd, verbose)
149
+ the_blamed = ExceptionNotification::Notifier.config[:git_repo_path].nil? ? nil : lay_blame(exception)
150
+ rejected_sections = request.nil? ? %w(request session) : []
151
+ # Debugging output
152
+ verbose_output(exception, status_cd, file_path, send_email, send_web_hooks, request, the_blamed, rejected_sections) if verbose
153
+ #TODO: is _rescue_action something from rails 3?
154
+ #if !(self.controller_name == 'application' && self.action_name == '_rescue_action')
155
+ # Send the exception notification email
156
+ perform_exception_notify_mailing(exception, data, request, the_blamed, verbose, rejected_sections) if send_email
157
+ # Send Web Hook requests
158
+ ExceptionNotification::HooksNotifier.deliver_exception_to_web_hooks(ExceptionNotification::Notifier.config, exception, self, request, data, the_blamed) if send_web_hooks
159
+
160
+ # We put the render call after the deliver call to ensure that, if the
161
+ # deliver raises an exception, we don't call render twice.
162
+ # Render the error page to the end user
163
+ render_error_template(file_path, status)
164
+ pass_it_on(exception, request)
165
+ end
166
+
167
+ def pass_it_on(exception, request = nil)
168
+ begin
169
+ request ||= {:params => {}}
170
+ case self.class.exception_notifiable_pass_through
171
+ when :hoptoad then
172
+ HoptoadNotifier.notify(exception, {:request => request})
173
+ puts "[PASS-IT-ON] HOPTOAD NOTIFIED" if verbose
174
+ else
175
+ puts "[PASS-IT-ON] NO" if verbose
176
+ #nothing
177
+ end
178
+ rescue
179
+ #nothing
180
+ end
181
+ end
182
+
183
+ def is_local?
184
+ (consider_all_requests_local || local_request?)
185
+ end
186
+
187
+ def status_code_for_exception(exception)
188
+ self.class.error_class_status_codes[exception.class].nil? ?
189
+ '500' :
190
+ self.class.error_class_status_codes[exception.class].blank? ?
191
+ '200' :
192
+ self.class.error_class_status_codes[exception.class]
193
+ end
194
+
195
+ def render_error_template(file, status)
196
+ respond_to do |type|
197
+ type.html { render :file => file,
198
+ :layout => self.class.error_layout,
199
+ :status => status }
200
+ type.all { render :nothing => true,
201
+ :status => status}
202
+ end
203
+ end
204
+
205
+ end
@@ -0,0 +1,50 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module ExceptionNotification::GitBlame
5
+
6
+ def lay_blame(exception)
7
+ error = {}
8
+ unless(ExceptionNotification::Notifier.config[:git_repo_path].nil?)
9
+ if(exception.class == ActionView::TemplateError)
10
+ blame = blame_output(exception.line_number, "app/views/#{exception.file_name}")
11
+ error[:author] = blame[/^author\s.+$/].gsub(/author\s/,'')
12
+ error[:line] = exception.line_number
13
+ error[:file] = exception.file_name
14
+ else
15
+ exception.backtrace.each do |line|
16
+ file = exception_in_project?(line[/^.+?(?=:)/])
17
+ unless(file.nil?)
18
+ line_number = line[/:\d+:/].gsub(/[^\d]/,'')
19
+ # Use relative path or weird stuff happens
20
+ blame = blame_output(line_number, file.gsub(Regexp.new("#{RAILS_ROOT}/"),''))
21
+ error[:author] = blame[/^author\s.+$/].sub(/author\s/,'')
22
+ error[:line] = line_number
23
+ error[:file] = file
24
+ break
25
+ end
26
+ end
27
+ end
28
+ end
29
+ error
30
+ end
31
+
32
+ def blame_output(line_number, path)
33
+ app_directory = Dir.pwd
34
+ Dir.chdir ExceptionNotification::Notifier.config[:git_repo_path]
35
+ blame = `git blame -p -L #{line_number},#{line_number} #{path}`
36
+ Dir.chdir app_directory
37
+
38
+ blame
39
+ end
40
+
41
+ def exception_in_project?(path) # should be a path like /path/to/broken/thingy.rb
42
+ dir = File.split(path).first rescue ''
43
+ if(File.directory?(dir) and !(path =~ /vendor\/plugins/) and !(path =~ /vendor\/gems/) and path.include?(RAILS_ROOT))
44
+ path
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,64 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module ExceptionNotification::HelpfulHashes
5
+ unless defined?(SILENT_EXCEPTIONS)
6
+ noiseless = []
7
+ noiseless << ActiveRecord::RecordNotFound if defined?(ActiveRecord)
8
+ if defined?(ActionController)
9
+ noiseless << ActionController::UnknownController
10
+ noiseless << ActionController::UnknownAction
11
+ noiseless << ActionController::RoutingError
12
+ noiseless << ActionController::MethodNotAllowed
13
+ end
14
+ SILENT_EXCEPTIONS = noiseless
15
+ end
16
+
17
+ # TODO: use ActionController::StatusCodes
18
+ HTTP_STATUS_CODES = {
19
+ "400" => "Bad Request",
20
+ "403" => "Forbidden",
21
+ "404" => "Not Found",
22
+ "405" => "Method Not Allowed",
23
+ "410" => "Gone",
24
+ "418" => "I'm a teapot",
25
+ "422" => "Unprocessable Entity",
26
+ "423" => "Locked",
27
+ "500" => "Internal Server Error",
28
+ "501" => "Not Implemented",
29
+ "503" => "Service Unavailable"
30
+ } unless defined?(HTTP_STATUS_CODES)
31
+
32
+ def codes_for_error_classes
33
+ #TODO: Format whitespace
34
+ classes = {
35
+ # These are standard errors in rails / ruby
36
+ NameError => "503",
37
+ TypeError => "503",
38
+ RuntimeError => "500",
39
+ ArgumentError => "500",
40
+ # These are custom error names defined in lib/super_exception_notifier/custom_exception_classes
41
+ AccessDenied => "403",
42
+ PageNotFound => "404",
43
+ InvalidMethod => "405",
44
+ ResourceGone => "410",
45
+ CorruptData => "422",
46
+ NoMethodError => "500",
47
+ NotImplemented => "501",
48
+ MethodDisabled => "200"
49
+ }
50
+ # Highly dependent on the verison of rails, so we're very protective about these'
51
+ classes.merge!({ ActionView::TemplateError => "500"}) if defined?(ActionView) && ActionView.const_defined?(:TemplateError)
52
+ classes.merge!({ ActiveRecord::RecordNotFound => "400" }) if defined?(ActiveRecord) && ActiveRecord.const_defined?(:RecordNotFound)
53
+ classes.merge!({ ActiveResource::ResourceNotFound => "404" }) if defined?(ActiveResource) && ActiveResource.const_defined?(:ResourceNotFound)
54
+
55
+ if defined?(ActionController)
56
+ classes.merge!({ ActionController::UnknownController => "404" }) if ActionController.const_defined?(:UnknownController)
57
+ classes.merge!({ ActionController::MissingTemplate => "404" }) if ActionController.const_defined?(:MissingTemplate)
58
+ classes.merge!({ ActionController::MethodNotAllowed => "405" }) if ActionController.const_defined?(:MethodNotAllowed)
59
+ classes.merge!({ ActionController::UnknownAction => "501" }) if ActionController.const_defined?(:UnknownAction)
60
+ classes.merge!({ ActionController::RoutingError => "404" }) if ActionController.const_defined?(:RoutingError)
61
+ classes.merge!({ ActionController::InvalidAuthenticityToken => "405" }) if ActionController.const_defined?(:InvalidAuthenticityToken)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module ExceptionNotification::HooksNotifier
5
+ # Deliver exception data hash to web hooks, if any
6
+ #
7
+ def self.deliver_exception_to_web_hooks(config, exception, controller, request, data={}, the_blamed = nil)
8
+ params = build_web_hook_params(config, exception, controller, request, data, the_blamed)
9
+ # TODO: use threads here
10
+ config[:web_hooks].each do |address|
11
+ post_hook(params, address)
12
+ end
13
+ end
14
+
15
+
16
+ # Parameters hash based on Merb Exceptions example
17
+ #
18
+ def self.build_web_hook_params(config, exception, controller, request, data={}, the_blamed = nil)
19
+ host = (request.env["HTTP_X_FORWARDED_HOST"] || request.env["HTTP_HOST"])
20
+ p = {
21
+ 'environment' => (defined?(Rails) ? Rails.env : RAILS_ENV),
22
+ 'exceptions' => [{
23
+ :class => exception.class.to_s,
24
+ :backtrace => exception.backtrace,
25
+ :message => exception.message
26
+ }],
27
+ 'app_name' => config[:app_name],
28
+ 'version' => config[:version],
29
+ 'blame' => "#{the_blamed}"
30
+ }
31
+ if !request.nil?
32
+ p.merge!({'request_url' => "#{request.protocol}#{host}#{request.request_uri}"})
33
+ p.merge!({'request_action' => request.parameters['action']})
34
+ p.merge!({'request_params' => request.parameters.inspect})
35
+ end
36
+ p.merge!({'request_controller' => controller.class.name}) if !controller.nil?
37
+ p.merge!({'status' => exception.status}) if exception.respond_to?(:status)
38
+ return p
39
+ end
40
+
41
+ def self.post_hook(params, address)
42
+ uri = URI.parse(address)
43
+ uri.path = '/' if uri.path=='' # set a path if one isn't provided to keep Net::HTTP happy
44
+
45
+ headers = { 'Content-Type' => 'text/x-json' }
46
+ data = params.to_json
47
+ Net::HTTP.start(uri.host, uri.port) do |http|
48
+ http.request_post(uri.path, data, headers)
49
+ end
50
+ data
51
+ end
52
+
53
+ end
@@ -0,0 +1,115 @@
1
+ module ExceptionNotification::Notifiable
2
+ include ExceptionNotification::NotifiableHelper
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+
7
+ # Verbosity of the gem
8
+ base.cattr_accessor :notifiable_verbose
9
+ base.notifiable_verbose = false
10
+ # Do Not Ever send error notification emails for these Error Classes
11
+ base.cattr_accessor :notifiable_silent_exceptions
12
+ base.notifiable_silent_exceptions = SILENT_EXCEPTIONS
13
+ # Notification Level
14
+ base.cattr_accessor :notifiable_notification_level
15
+ base.notifiable_notification_level = [:email, :web_hooks]
16
+
17
+ # Since there is no concept of locality from a request here allow user to explicitly define which env's are noisy (send notifications)
18
+ base.cattr_accessor :notifiable_noisy_environments
19
+ base.notifiable_noisy_environments = [:production]
20
+
21
+ base.cattr_accessor :notifiable_pass_through
22
+ base.notifiable_pass_through = false
23
+ end
24
+
25
+ module ClassMethods
26
+ # set the exception_data deliverer OR retrieve the exception_data
27
+ def exception_data(deliverer = nil)
28
+ if deliverer
29
+ write_inheritable_attribute(:exception_data, deliverer)
30
+ else
31
+ read_inheritable_attribute(:exception_data)
32
+ end
33
+ end
34
+
35
+ def be_silent_for_exception?(exception)
36
+ self.notifiable_silent_exceptions.respond_to?(:any?) && self.notifiable_silent_exceptions.any? {|klass| klass === exception }
37
+ end
38
+
39
+ end
40
+
41
+ # Usage:
42
+ # notifiable { Klass.some_method }
43
+ # This will rescue any errors that occur within Klass.some_method
44
+ def notifiable(&block)
45
+ yield
46
+ rescue => exception
47
+ rescue_with_hooks(exception)
48
+ raise
49
+ end
50
+
51
+ def be_silent_for_exception?(exception)
52
+ self.class.be_silent_for_exception?(exception)
53
+ end
54
+
55
+ private
56
+
57
+ def environment_is_noisy?
58
+ !self.notifiable_noisy_environments.include?(Rails.env)
59
+ end
60
+
61
+ def notification_level_sends_email?
62
+ self.class.notifiable_notification_level.include?(:email)
63
+ end
64
+
65
+ def notification_level_sends_web_hooks?
66
+ self.class.notifiable_notification_level.include?(:web_hooks)
67
+ end
68
+
69
+ def rescue_with_hooks(exception)
70
+ verbose = self.class.notifiable_verbose
71
+ puts "[RESCUE STYLE] rescue_with_hooks" if verbose
72
+ data = get_exception_data
73
+ # With ExceptionNotifiable you have an inherent request, and using a status code makes sense.
74
+ # With Notifiable class to wrap around everything that doesn't have a request,
75
+ # the errors you want to be notified of need to be specified either positively or negatively
76
+ # 1. positive eg. set ExceptionNotifier.config[:notify_error_classes] to an array of classes
77
+ # set ExceptionNotifier.config[:notify_other_errors] to false
78
+ # 1. negative eg. set Klass.silent_exceptions to the ones to keep quiet
79
+ # set ExceptionNotifier.config[:notify_other_errors] to true
80
+ status_code = nil
81
+ #We only send email if it has been configured in environment
82
+ send_email = should_email_on_exception?(exception, status_code, verbose)
83
+ #We only send web hooks if they've been configured in environment
84
+ send_web_hooks = should_web_hook_on_exception?(exception, status_code, verbose)
85
+ the_blamed = ExceptionNotification::Notifier.config[:git_repo_path].nil? ? nil : lay_blame(exception)
86
+ rejected_sections = %w(request session)
87
+ verbose_output(exception, status_code, "rescued by handler", send_email, send_web_hooks, nil, the_blamed, rejected_sections) if verbose
88
+ # Send the exception notification email
89
+ perform_exception_notify_mailing(exception, data, nil, the_blamed, verbose, rejected_sections) if send_email
90
+ # Send Web Hook requests
91
+ ExceptionNotification::HooksNotifier.deliver_exception_to_web_hooks(ExceptionNotification::Notifier.config, exception, self, request, data, the_blamed) if send_web_hooks
92
+ pass_it_on(exception)
93
+ end
94
+
95
+ def pass_it_on(exception, request = nil)
96
+ begin
97
+ request ||= {:params => {}}
98
+ case self.class.notifiable_pass_through
99
+ when :hoptoad then
100
+ HoptoadNotifier.notify(exception, {:request => request})
101
+ puts "[PASS-IT-ON] HOPTOAD NOTIFIED" if verbose
102
+ else
103
+ puts "[PASS-IT-ON] NO" if verbose
104
+ #nothing
105
+ end
106
+ rescue
107
+ #nothing
108
+ end
109
+ end
110
+
111
+ def is_local? #like asking is_silent?
112
+ self.notifiable_noisy_environments.include?(Rails.env)
113
+ end
114
+
115
+ end
@@ -0,0 +1,77 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module ExceptionNotification::NotifiableHelper
5
+ include ExceptionNotification::CustomExceptionClasses
6
+ include ExceptionNotification::CustomExceptionMethods
7
+ include ExceptionNotification::HelpfulHashes
8
+ include ExceptionNotification::GitBlame
9
+ include ExceptionNotification::HooksNotifier
10
+
11
+ private
12
+
13
+ def get_method_name
14
+ if /`(.*)'/.match(caller.first)
15
+ return $1
16
+ end
17
+ nil
18
+ end
19
+
20
+ def get_exception_data
21
+ deliverer = self.class.exception_data
22
+ return case deliverer
23
+ when nil then {}
24
+ when Symbol then send(deliverer)
25
+ when Proc then deliverer.call(self)
26
+ end
27
+ end
28
+
29
+ def verbose_output(exception, status_cd, file_path, send_email, send_web_hooks, request = nil, the_blamed = nil, rejected_sections = nil)
30
+ puts "[EXCEPTION] #{exception}"
31
+ puts "[EXCEPTION CLASS] #{exception.class}"
32
+ puts "[EXCEPTION STATUS_CD] #{status_cd}"
33
+ puts "[ERROR LAYOUT] #{self.class.error_layout}" if self.class.respond_to?(:error_layout)
34
+ puts "[ERROR VIEW PATH] #{ExceptionNotification::Notifier.config[:view_path]}" if !ExceptionNotification::Notifier.nil? && !ExceptionNotification::Notifier.config[:view_path].nil?
35
+ puts "[ERROR FILE PATH] #{file_path.inspect}"
36
+ puts "[ERROR EMAIL] #{send_email ? "YES" : "NO"}"
37
+ puts "[ERROR WEB HOOKS] #{send_web_hooks ? "YES" : "NO"}"
38
+ puts "[THE BLAMED] #{the_blamed}"
39
+ puts "[SECTIONS] #{ExceptionNotification::Notifier.sections_for_email(rejected_sections, request)}"
40
+ req = request ? " for request_uri=#{request.request_uri} and env=#{request.env.inspect}" : ""
41
+ logger.error("render_error(#{status_cd}, #{self.class.http_status_codes[status_cd]}) invoked#{req}") if self.class.respond_to?(:http_status_codes) && !logger.nil?
42
+ end
43
+
44
+ def perform_exception_notify_mailing(exception, data, request = nil, the_blamed = nil, verbose = false, rejected_sections = nil)
45
+ if ExceptionNotification::Notifier.config[:exception_recipients].blank?
46
+ puts "[EMAIL NOTIFICATION] ExceptionNotification::Notifier.config[:exception_recipients] is blank, notification cancelled!" if verbose
47
+ else
48
+ class_name = self.respond_to?(:controller_name) ? self.controller_name : self.to_s
49
+ method_name = self.respond_to?(:action_name) ? self.action_name : get_method_name
50
+ ExceptionNotification::Notifier.deliver_exception_notification(exception, class_name, method_name,
51
+ request, data, the_blamed, rejected_sections)
52
+ puts "[EMAIL NOTIFICATION] Sent" if verbose
53
+ end
54
+ end
55
+
56
+ def should_email_on_exception?(exception, status_cd = nil, verbose = false)
57
+ environment_is_noisy? && notification_level_sends_email? && !ExceptionNotification::Notifier.config[:exception_recipients].blank? && should_notify_on_exception?(exception, status_cd, verbose)
58
+ end
59
+
60
+ def should_web_hook_on_exception?(exception, status_cd = nil, verbose = false)
61
+ environment_is_noisy? && notification_level_sends_web_hooks? && !ExceptionNotification::Notifier.config[:web_hooks].blank? && should_notify_on_exception?(exception, status_cd, verbose)
62
+ end
63
+
64
+ # Relies on the base class to define be_silent_for_exception?
65
+ def should_notify_on_exception?(exception, status_cd = nil, verbose = false)
66
+ # don't notify (email or web hooks) on exceptions raised locally
67
+ verbose && ExceptionNotification::Notifier.config[:skip_local_notification] && is_local? ?
68
+ "[NOTIFY LOCALLY] NO" :
69
+ nil
70
+ return false if ExceptionNotification::Notifier.config[:skip_local_notification] && is_local?
71
+ # don't notify (email or web hooks) exceptions raised that match ExceptionNotifiable.notifiable_silent_exceptions
72
+ return false if self.be_silent_for_exception?(exception)
73
+ return true if ExceptionNotification::Notifier.config[:notify_error_classes].include?(exception.class)
74
+ return true if !status_cd.nil? && ExceptionNotification::Notifier.config[:notify_error_codes].include?(status_cd)
75
+ return ExceptionNotification::Notifier.config[:notify_other_errors]
76
+ end
77
+ end
@@ -0,0 +1,15 @@
1
+ class NotifiedTask < Rake::TaskLib
2
+ attr_accessor :name, :block
3
+
4
+ def initialize(name, &block)
5
+ @name = name
6
+ @block = block
7
+ define
8
+ end
9
+
10
+ def define
11
+ task name do |t|
12
+ notifiable { block.call }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,174 @@
1
+ require 'pathname'
2
+
3
+ class ExceptionNotification::Notifier < ActionMailer::Base
4
+
5
+ #andrewroth reported that @@config gets clobbered because rails loads this class twice when installed as a plugin, and adding the ||= fixed it.
6
+ @@config ||= {
7
+ # If left empty web hooks will not be engaged
8
+ :web_hooks => [],
9
+ :app_name => "[MYAPP]",
10
+ :version => "0.0.0",
11
+ :sender_address => "super.exception.notifier@example.com",
12
+ :exception_recipients => [],
13
+ # Customize the subject line
14
+ :subject_prepend => "[#{(defined?(Rails) ? Rails.env : RAILS_ENV).capitalize} ERROR] ",
15
+ :subject_append => nil,
16
+ # Include which sections of the exception email?
17
+ :sections => %w(request session environment backtrace),
18
+ :skip_local_notification => true,
19
+ :view_path => nil,
20
+ #Error Notification will be sent if the HTTP response code for the error matches one of the following error codes
21
+ :notify_error_codes => %W( 405 500 503 ),
22
+ #Error Notification will be sent if the error class matches one of the following error error classes
23
+ :notify_error_classes => %W( ),
24
+ :notify_other_errors => true,
25
+ :git_repo_path => nil,
26
+ :template_root => "#{File.dirname(__FILE__)}/../views"
27
+ }
28
+
29
+ cattr_accessor :config
30
+
31
+ def self.configure_exception_notifier(&block)
32
+ yield @@config
33
+ end
34
+
35
+ self.template_root = config[:template_root]
36
+
37
+ def self.reloadable?() false end
38
+
39
+ # Returns an array of potential filenames to look for
40
+ # eg. For the Exception Class - SuperExceptionNotifier::CustomExceptionClasses::MethodDisabled
41
+ # the filename handles are:
42
+ # super_exception_notifier_custom_exception_classes_method_disabled
43
+ # method_disabled
44
+ def self.exception_to_filenames(exception)
45
+ filenames = []
46
+ e = exception.to_s
47
+ filenames << ExceptionNotification::Notifier.filenamify(e)
48
+
49
+ last_colon = e.rindex(':')
50
+ unless last_colon.nil?
51
+ filenames << ExceptionNotification::Notifier.filenamify(e[(last_colon + 1)..(e.length - 1)])
52
+ end
53
+ filenames
54
+ end
55
+
56
+ def self.sections_for_email(rejected_sections, request)
57
+ rejected_sections = rejected_sections.nil? ? request.nil? ? %w(request session) : [] : rejected_sections
58
+ rejected_sections.empty? ? config[:sections] : config[:sections].reject{|s| rejected_sections.include?(s) }
59
+ end
60
+
61
+ # Converts Stringified Class Names to acceptable filename handles with underscores
62
+ def self.filenamify(str)
63
+ str.delete(':').gsub( /([A-Za-z])([A-Z])/, '\1' << '_' << '\2').downcase
64
+ end
65
+
66
+ # What is the path of the file we will render to the user based on a given status code?
67
+ def self.get_view_path_for_status_code(status_cd, verbose = false)
68
+ file_name = ExceptionNotification::Notifier.get_view_path(status_cd, verbose)
69
+ file_name.nil? ? self.catch_all(verbose) : file_name
70
+ end
71
+
72
+ # def self.get_view_path_for_files(filenames = [])
73
+ # filepaths = filenames.map do |file|
74
+ # ExceptionNotification::Notifier.get_view_path(file)
75
+ # end.compact
76
+ # filepaths.empty? ? "#{File.dirname(__FILE__)}/../rails/app/views/exception_notifiable/500.html" : filepaths.first
77
+ # end
78
+
79
+ # What is the path of the file we will render to the user based on a given exception class?
80
+ def self.get_view_path_for_class(exception, verbose = false)
81
+ return self.catch_all(verbose) if exception.nil?
82
+ #return self.catch_all(verbose) unless exception.is_a?(StandardError) || exception.is_a?(Class) # For some reason exception.is_a?(Class) works in console, but not when running in mongrel (ALWAYS returns false)?!?!?
83
+ filepaths = ExceptionNotification::Notifier.exception_to_filenames(exception).map do |file|
84
+ ExceptionNotification::Notifier.get_view_path(file, verbose)
85
+ end.compact
86
+ filepaths.empty? ? self.catch_all(verbose) : filepaths.first
87
+ end
88
+
89
+ def self.catch_all(verbose = false)
90
+ puts "[CATCH ALL INVOKED] #{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/500.html" if verbose
91
+ "#{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/500.html"
92
+ end
93
+
94
+ # Check the usual suspects
95
+ def self.get_view_path(file_name, verbose = false)
96
+ if File.exist?("#{RAILS_ROOT}/public/#{file_name}.html")
97
+ puts "[FOUND FILE:A] #{RAILS_ROOT}/public/#{file_name}.html" if verbose
98
+ "#{RAILS_ROOT}/public/#{file_name}.html"
99
+ elsif !config[:view_path].nil? && File.exist?("#{RAILS_ROOT}/#{config[:view_path]}/#{file_name}.html.erb")
100
+ puts "[FOUND FILE:B] #{RAILS_ROOT}/#{config[:view_path]}/#{file_name}.html.erb" if verbose
101
+ "#{RAILS_ROOT}/#{config[:view_path]}/#{file_name}.html.erb"
102
+ elsif !config[:view_path].nil? && File.exist?("#{RAILS_ROOT}/#{config[:view_path]}/#{file_name}.html")
103
+ puts "[FOUND FILE:C] #{RAILS_ROOT}/#{config[:view_path]}/#{file_name}.html" if verbose
104
+ "#{RAILS_ROOT}/#{config[:view_path]}/#{file_name}.html"
105
+ elsif File.exist?("#{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/#{file_name}.html.erb")
106
+ puts "[FOUND FILE:D] #{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/#{file_name}.html.erb" if verbose
107
+ "#{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/#{file_name}.html.erb"
108
+ elsif File.exist?("#{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/#{file_name}.html")
109
+ puts "[FOUND FILE:E] #{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/#{file_name}.html" if verbose
110
+ "#{File.dirname(__FILE__)}/../../rails/app/views/exception_notifiable/#{file_name}.html"
111
+ else
112
+ nil
113
+ end
114
+ end
115
+
116
+ def exception_notification(exception, class_name = nil, method_name = nil, request = nil, data = {}, the_blamed = nil, rejected_sections = nil)
117
+ body_hash = error_environment_data_hash(exception, class_name, method_name, request, data, the_blamed, rejected_sections)
118
+ #Prefer to have custom, potentially HTML email templates available
119
+ #content_type "text/plain"
120
+ recipients config[:exception_recipients]
121
+ from config[:sender_address]
122
+
123
+ request.session.inspect unless request.nil? # Ensure session data is loaded (Rails 2.3 lazy-loading)
124
+
125
+ subject "#{config[:subject_prepend]}#{body_hash[:location]} (#{exception.class}) #{exception.message.inspect}#{config[:subject_append]}"
126
+ body body_hash
127
+ end
128
+
129
+ def background_exception_notification(exception, data = {}, the_blamed = nil, rejected_sections = %w(request session))
130
+ exception_notification(exception, nil, nil, nil, data, the_blamed, rejected_sections)
131
+ end
132
+
133
+ def rake_exception_notification(exception, task, data={}, the_blamed = nil, rejected_sections = %w(request session))
134
+ exception_notification(exception, "", "#{task.name}", nil, data, the_blamed, rejected_sections)
135
+ end
136
+
137
+ private
138
+
139
+ def error_environment_data_hash(exception, class_name = nil, method_name = nil, request = nil, data = {}, the_blamed = nil, rejected_sections = nil)
140
+ data.merge!({
141
+ :exception => exception,
142
+ :backtrace => sanitize_backtrace(exception.backtrace),
143
+ :rails_root => rails_root,
144
+ :data => data,
145
+ :the_blamed => the_blamed
146
+ })
147
+
148
+ data.merge!({:class_name => class_name}) if class_name
149
+ data.merge!({:method_name => method_name}) if method_name
150
+ if class_name && method_name
151
+ data.merge!({:location => "#{class_name}##{method_name}"})
152
+ elsif method_name
153
+ data.merge!({:location => "#{method_name}"})
154
+ else
155
+ data.merge!({:location => sanitize_backtrace([exception.backtrace.first]).first})
156
+ end
157
+ if request
158
+ data.merge!({:request => request})
159
+ data.merge!({:host => (request.env['HTTP_X_REAL_IP'] || request.env["HTTP_X_FORWARDED_HOST"] || request.env["HTTP_HOST"])})
160
+ end
161
+ data.merge!({:sections => ExceptionNotification::Notifier.sections_for_email(rejected_sections, request)})
162
+ return data
163
+ end
164
+
165
+ def sanitize_backtrace(trace)
166
+ re = Regexp.new(/^#{Regexp.escape(rails_root)}/)
167
+ trace.map { |line| Pathname.new(line.gsub(re, "[RAILS_ROOT]")).cleanpath.to_s }
168
+ end
169
+
170
+ def rails_root
171
+ @rails_root ||= Pathname.new(RAILS_ROOT).cleanpath.to_s
172
+ end
173
+
174
+ end
@@ -0,0 +1,59 @@
1
+ require 'pp'
2
+
3
+ module ExceptionNotification::NotifierHelper
4
+ VIEW_PATH = "views/exception_notifier" unless defined?(VIEW_PATH)
5
+ APP_PATH = "#{RAILS_ROOT}/app/#{VIEW_PATH}" unless defined?(APP_PATH)
6
+ PARAM_FILTER_REPLACEMENT = "[FILTERED]" unless defined?(PARAM_FILTER_REPLACEMENT)
7
+
8
+ def render_section(section)
9
+ RAILS_DEFAULT_LOGGER.info("rendering section #{section.inspect}")
10
+ summary = render_overridable(section).strip
11
+ unless summary.blank?
12
+ title = render_overridable(:title, :locals => { :title => section }).strip
13
+ "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n"
14
+ end
15
+ end
16
+
17
+ def render_overridable(partial, options={})
18
+ if File.exist?(path = "#{APP_PATH}/_#{partial}.html.erb") ||
19
+ File.exist?(path = "#{File.dirname(__FILE__)}/../#{VIEW_PATH}/_#{partial}.html.erb") ||
20
+ File.exist?(path = "#{APP_PATH}/_#{partial}.rhtml") ||
21
+ File.exist?(path = "#{APP_PATH}/_#{partial}.erb")
22
+ render(options.merge(:file => path, :use_full_path => false))
23
+ else
24
+ ""
25
+ end
26
+ end
27
+
28
+ def inspect_model_object(model, locals={})
29
+ render_overridable(:inspect_model,
30
+ :locals => { :inspect_model => model,
31
+ :show_instance_variables => true,
32
+ :show_attributes => true }.merge(locals))
33
+ end
34
+
35
+ def inspect_value(value)
36
+ len = 512
37
+ result = object_to_yaml(value).gsub(/\n/, "\n ").strip
38
+ result = result[0,len] + "... (#{result.length-len} bytes more)" if result.length > len+20
39
+ result
40
+ end
41
+
42
+ def object_to_yaml(object)
43
+ object.to_yaml.sub(/^---\s*/m, "")
44
+ end
45
+
46
+ def exclude_raw_post_parameters?
47
+ @controller && @controller.respond_to?(:filter_parameters)
48
+ end
49
+
50
+ def filter_sensitive_post_data_parameters(parameters)
51
+ exclude_raw_post_parameters? ? @controller.__send__(:filter_parameters, parameters) : parameters
52
+ end
53
+
54
+ def filter_sensitive_post_data_from_env(env_key, env_value)
55
+ return env_value unless exclude_raw_post_parameters?
56
+ return PARAM_FILTER_REPLACEMENT if (env_key =~ /RAW_POST_DATA/i)
57
+ return @controller.__send__(:filter_parameters, {env_key => env_value}).values[0]
58
+ end
59
+ end
data/rails/init.rb CHANGED
@@ -1,4 +1,3 @@
1
- puts "Begin Loading ExceptionNotification"
2
1
  require 'rake'
3
2
  require 'rake/tasklib'
4
3
  require "action_mailer"
@@ -10,8 +9,6 @@ require "exception_notification" unless defined?(ExceptionNotification)
10
9
  Object.class_eval do
11
10
  include ExceptionNotification::Notifiable
12
11
  end
13
- puts "Object test: #{Object.respond_to?(:notifiable_noisy_environments) ? 'Pass' : 'Fail'}"
14
- puts "Finished Loading ExceptionNotification"
15
12
 
16
13
  #It appears that the view path is auto-added by rails... hmmm.
17
14
  #if ActionController::Base.respond_to?(:append_view_path)
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{super_exception_notifier}
8
- s.version = "3.0.2"
8
+ s.version = "3.0.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Peter Boling", "Scott Windsor", "Ismael Celis", "Jacques Crocker", "Jamis Buck"]
@@ -39,6 +39,19 @@ Gem::Specification.new do |s|
39
39
  "VERSION.yml",
40
40
  "init.rb",
41
41
  "lib/exception_notification.rb",
42
+ "lib/exception_notification/consider_local.rb",
43
+ "lib/exception_notification/custom_exception_classes.rb",
44
+ "lib/exception_notification/custom_exception_methods.rb",
45
+ "lib/exception_notification/deprecated_methods.rb",
46
+ "lib/exception_notification/exception_notifiable.rb",
47
+ "lib/exception_notification/git_blame.rb",
48
+ "lib/exception_notification/helpful_hashes.rb",
49
+ "lib/exception_notification/hooks_notifier.rb",
50
+ "lib/exception_notification/notifiable.rb",
51
+ "lib/exception_notification/notifiable_helper.rb",
52
+ "lib/exception_notification/notified_task.rb",
53
+ "lib/exception_notification/notifier.rb",
54
+ "lib/exception_notification/notifier_helper.rb",
42
55
  "rails/app/views/exception_notifiable/400.html",
43
56
  "rails/app/views/exception_notifiable/403.html",
44
57
  "rails/app/views/exception_notifiable/404.html",
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 3
7
7
  - 0
8
- - 2
9
- version: 3.0.2
8
+ - 4
9
+ version: 3.0.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Peter Boling
@@ -78,6 +78,19 @@ files:
78
78
  - VERSION.yml
79
79
  - init.rb
80
80
  - lib/exception_notification.rb
81
+ - lib/exception_notification/consider_local.rb
82
+ - lib/exception_notification/custom_exception_classes.rb
83
+ - lib/exception_notification/custom_exception_methods.rb
84
+ - lib/exception_notification/deprecated_methods.rb
85
+ - lib/exception_notification/exception_notifiable.rb
86
+ - lib/exception_notification/git_blame.rb
87
+ - lib/exception_notification/helpful_hashes.rb
88
+ - lib/exception_notification/hooks_notifier.rb
89
+ - lib/exception_notification/notifiable.rb
90
+ - lib/exception_notification/notifiable_helper.rb
91
+ - lib/exception_notification/notified_task.rb
92
+ - lib/exception_notification/notifier.rb
93
+ - lib/exception_notification/notifier_helper.rb
81
94
  - rails/app/views/exception_notifiable/400.html
82
95
  - rails/app/views/exception_notifiable/403.html
83
96
  - rails/app/views/exception_notifiable/404.html