super_exception_notifier 3.0.2 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
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