pboling-super_exception_notifier 1.6.5

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 (38) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README.rdoc +419 -0
  3. data/VERSION.yml +4 -0
  4. data/exception_notification.gemspec +71 -0
  5. data/init.rb +1 -0
  6. data/lib/exception_notifiable.rb +278 -0
  7. data/lib/exception_notifier.rb +108 -0
  8. data/lib/exception_notifier_helper.rb +58 -0
  9. data/lib/hooks_notifier.rb +53 -0
  10. data/lib/notifiable.rb +8 -0
  11. data/lib/super_exception_notifier/custom_exception_classes.rb +16 -0
  12. data/lib/super_exception_notifier/custom_exception_methods.rb +50 -0
  13. data/rails/app/views/exception_notifiable/400.html +5 -0
  14. data/rails/app/views/exception_notifiable/403.html +6 -0
  15. data/rails/app/views/exception_notifiable/404.html +6 -0
  16. data/rails/app/views/exception_notifiable/405.html +6 -0
  17. data/rails/app/views/exception_notifiable/410.html +7 -0
  18. data/rails/app/views/exception_notifiable/418.html +6 -0
  19. data/rails/app/views/exception_notifiable/422.html +5 -0
  20. data/rails/app/views/exception_notifiable/423.html +6 -0
  21. data/rails/app/views/exception_notifiable/501.html +8 -0
  22. data/rails/app/views/exception_notifiable/503.html +6 -0
  23. data/rails/init.rb +18 -0
  24. data/test/exception_notifier_helper_test.rb +76 -0
  25. data/test/exception_notify_functional_test.rb +102 -0
  26. data/test/mocks/404.html +1 -0
  27. data/test/mocks/500.html +1 -0
  28. data/test/mocks/controllers.rb +46 -0
  29. data/test/test_helper.rb +28 -0
  30. data/views/exception_notifier/_backtrace.html.erb +1 -0
  31. data/views/exception_notifier/_environment.html.erb +14 -0
  32. data/views/exception_notifier/_inspect_model.html.erb +16 -0
  33. data/views/exception_notifier/_request.html.erb +8 -0
  34. data/views/exception_notifier/_session.html.erb +7 -0
  35. data/views/exception_notifier/_title.html.erb +3 -0
  36. data/views/exception_notifier/background_exception_notification.text.plain.erb +6 -0
  37. data/views/exception_notifier/exception_notification.text.plain.erb +10 -0
  38. metadata +100 -0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,278 @@
1
+ require 'ipaddr'
2
+
3
+ module ExceptionNotifiable
4
+ include SuperExceptionNotifier::CustomExceptionClasses
5
+ include SuperExceptionNotifier::CustomExceptionMethods
6
+ include HooksNotifier
7
+
8
+ unless defined?(SILENT_EXCEPTIONS)
9
+ SILENT_EXCEPTIONS = []
10
+ SILENT_EXCEPTIONS << ActiveRecord::RecordNotFound if defined?(ActiveRecord)
11
+ SILENT_EXCEPTIONS << [
12
+ ActionController::UnknownController,
13
+ ActionController::UnknownAction,
14
+ ActionController::RoutingError,
15
+ ActionController::MethodNotAllowed
16
+ ] if defined?(ActionController)
17
+ end
18
+
19
+ HTTP_ERROR_CODES = {
20
+ "400" => "Bad Request",
21
+ "403" => "Forbidden",
22
+ "404" => "Not Found",
23
+ "405" => "Method Not Allowed",
24
+ "410" => "Gone",
25
+ "418" => "I�m a teapot",
26
+ "422" => "Unprocessable Entity",
27
+ "423" => "Locked",
28
+ "500" => "Internal Server Error",
29
+ "501" => "Not Implemented",
30
+ "503" => "Service Unavailable"
31
+ } unless defined?(HTTP_ERROR_CODES)
32
+
33
+ def self.codes_for_rails_error_classes
34
+ classes = {
35
+ # These are standard errors in rails / ruby
36
+ NameError => "503",
37
+ TypeError => "503",
38
+ RuntimeError => "500",
39
+ # These are custom error names defined in lib/super_exception_notifier/custom_exception_classes
40
+ AccessDenied => "403",
41
+ PageNotFound => "404",
42
+ InvalidMethod => "405",
43
+ ResourceGone => "410",
44
+ CorruptData => "422",
45
+ NoMethodError => "500",
46
+ NotImplemented => "501",
47
+ MethodDisabled => "200"
48
+ }
49
+ # Highly dependent on the verison of rails, so we're very protective about these'
50
+ classes.merge!({ ActionView::TemplateError => "500"}) if defined?(ActionView) && ActionView.const_defined?(:TemplateError)
51
+ classes.merge!({ ActiveRecord::RecordNotFound => "400" }) if defined?(ActiveRecord) && ActiveRecord.const_defined?(:RecordNotFound)
52
+ classes.merge!({ ActiveResource::ResourceNotFound => "404" }) if defined?(ActiveResource) && ActiveResource.const_defined?(:ResourceNotFound)
53
+
54
+ if defined?(ActionController)
55
+ classes.merge!({ ActionController::UnknownController => "404" }) if ActionController.const_defined?(:UnknownController)
56
+ classes.merge!({ ActionController::MissingTemplate => "404" }) if ActionController.const_defined?(:MissingTemplate)
57
+ classes.merge!({ ActionController::MethodNotAllowed => "405" }) if ActionController.const_defined?(:MethodNotAllowed)
58
+ classes.merge!({ ActionController::UnknownAction => "501" }) if ActionController.const_defined?(:UnknownAction)
59
+ classes.merge!({ ActionController::RoutingError => "404" }) if ActionController.const_defined?(:RoutingError)
60
+ classes.merge!({ ActionController::InvalidAuthenticityToken => "405" }) if ActionController.const_defined?(:InvalidAuthenticityToken)
61
+ end
62
+ end
63
+
64
+ def self.included(base)
65
+ base.extend ClassMethods
66
+
67
+ # Adds the following class attributes to the classes that include ExceptionNotifiable
68
+ # HTTP status codes and what their 'English' status message is
69
+ # Rails error classes to rescue and how to rescue them
70
+ # error_layout:
71
+ # can be defined at controller level to the name of the layout,
72
+ # or set to true to render the controller's own default layout,
73
+ # or set to false to render errors with no layout
74
+ base.cattr_accessor :http_error_codes
75
+ base.http_error_codes = HTTP_ERROR_CODES
76
+ base.cattr_accessor :error_layout
77
+ base.error_layout = nil
78
+ base.cattr_accessor :rails_error_classes
79
+ base.rails_error_classes = self.codes_for_rails_error_classes
80
+ base.cattr_accessor :exception_notifier_verbose
81
+ base.exception_notifier_verbose = false
82
+ base.cattr_accessor :silent_exceptions
83
+ base.silent_exceptions = SILENT_EXCEPTIONS
84
+ end
85
+
86
+ module ClassMethods
87
+ # specifies ip addresses that should be handled as though local
88
+ def consider_local(*args)
89
+ local_addresses.concat(args.flatten.map { |a| IPAddr.new(a) })
90
+ end
91
+
92
+ def local_addresses
93
+ addresses = read_inheritable_attribute(:local_addresses)
94
+ unless addresses
95
+ addresses = [IPAddr.new("127.0.0.1")]
96
+ write_inheritable_attribute(:local_addresses, addresses)
97
+ end
98
+ addresses
99
+ end
100
+
101
+ # set the exception_data deliverer OR retrieve the exception_data
102
+ def exception_data(deliverer = nil)
103
+ if deliverer
104
+ write_inheritable_attribute(:exception_data, deliverer)
105
+ else
106
+ read_inheritable_attribute(:exception_data)
107
+ end
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ # overrides Rails' local_request? method to also check any ip
114
+ # addresses specified through consider_local.
115
+ def local_request?
116
+ remote = IPAddr.new(request.remote_ip)
117
+ !self.class.local_addresses.detect { |addr| addr.include?(remote) }.nil?
118
+ end
119
+
120
+ # When the action being executed has its own local error handling (rescue)
121
+ def rescue_with_handler(exception)
122
+ to_return = super
123
+ if to_return
124
+ data = get_exception_data
125
+ send_email = should_notify_on_exception?(exception)
126
+ send_web_hooks = !ExceptionNotifier.config[:web_hooks].empty?
127
+ the_blamed = ExceptionNotifier.config[:git_repo_path].nil? ? nil : lay_blame(exception)
128
+ verbose_output(exception, "N/A", "rescued by handler", send_email, send_web_hooks, nil, the_blamed) if self.class.exception_notifier_verbose
129
+ # Send the exception notificaiton email
130
+ perform_exception_notify_mailing(exception, data, nil, the_blamed) if send_email
131
+ # Send Web Hook requests
132
+ HooksNotifier.deliver_exception_to_web_hooks(ExceptionNotifier.config, exception, self, request, data, the_blamed) if send_web_hooks
133
+ end
134
+ to_return
135
+ end
136
+
137
+ # When the action being executed is letting SEN handle the exception completely
138
+ def rescue_action_in_public(exception)
139
+ # If the error class is NOT listed in the rails_errror_class hash then we get a generic 500 error:
140
+ # 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
141
+ # OTW the error class is listed!
142
+ status_code = status_code_for_exception(exception)
143
+ if status_code == '200'
144
+ notify_and_render_error_template(status_code, request, exception, exception_to_filename(exception))
145
+ else
146
+ notify_and_render_error_template(status_code, request, exception)
147
+ end
148
+ end
149
+
150
+ def notify_and_render_error_template(status_cd, request, exception, file_path = nil)
151
+ status = self.class.http_error_codes[status_cd] ? status_cd + " " + self.class.http_error_codes[status_cd] : status_cd
152
+ file = file_path ? ExceptionNotifier.get_view_path(file_path) : ExceptionNotifier.get_view_path(status_cd)
153
+ data = get_exception_data
154
+ send_email = should_notify_on_exception?(exception, status_cd)
155
+ send_web_hooks = !ExceptionNotifier.config[:web_hooks].empty?
156
+ the_blamed = ExceptionNotifier.config[:git_repo_path].nil? ? nil : lay_blame(exception)
157
+
158
+ # Debugging output
159
+ verbose_output(exception, status_cd, file, send_email, send_web_hooks, request, the_blamed) if self.class.exception_notifier_verbose
160
+ # Send the email before rendering to avert possible errors on render preventing the email from being sent.
161
+ perform_exception_notify_mailing(exception, data, request, the_blamed) if send_email
162
+ # Send Web Hook requests
163
+ HooksNotifier.deliver_exception_to_web_hooks(ExceptionNotifier.config, exception, self, request, data, the_blamed) if send_web_hooks
164
+ # Render the error page to the end user
165
+ render_error_template(file, status)
166
+ end
167
+
168
+ def get_exception_data
169
+ deliverer = self.class.exception_data
170
+ return case deliverer
171
+ when nil then {}
172
+ when Symbol then send(deliverer)
173
+ when Proc then deliverer.call(self)
174
+ end
175
+ end
176
+
177
+ def render_error_template(file, status)
178
+ respond_to do |type|
179
+ type.html { render :file => file,
180
+ :layout => self.class.error_layout,
181
+ :status => status }
182
+ type.all { render :nothing => true,
183
+ :status => status}
184
+ end
185
+ end
186
+
187
+ def verbose_output(exception, status_cd, file, send_email, send_web_hooks, request = nil, the_blamed = nil)
188
+ puts "[EXCEPTION] #{exception}"
189
+ puts "[EXCEPTION CLASS] #{exception.class}"
190
+ puts "[EXCEPTION STATUS_CD] #{status_cd}"
191
+ puts "[ERROR LAYOUT] #{self.class.error_layout}" if self.class.error_layout
192
+ puts "[ERROR VIEW PATH] #{ExceptionNotifier.config[:view_path]}" if !ExceptionNotifier.nil? && !ExceptionNotifier.config[:view_path].nil?
193
+ puts "[ERROR RENDER] #{file}"
194
+ puts "[ERROR EMAIL] #{send_email ? "YES" : "NO"}"
195
+ puts "[ERROR WEB HOOKS] #{send_web_hooks ? "YES" : "NO"}"
196
+ puts "[COMPAT MODE] #{ExceptionNotifierHelper::COMPAT_MODE ? "Yes" : "No"}"
197
+ puts "[THE BLAMED] #{the_blamed}"
198
+ req = request ? " for request_uri=#{request.request_uri} and env=#{request.env.inspect}" : ""
199
+ logger.error("render_error(#{status_cd}, #{self.class.http_error_codes[status_cd]}) invoked#{req}") if !logger.nil?
200
+ end
201
+
202
+ def perform_exception_notify_mailing(exception, data, request = nil, the_blamed = nil)
203
+ if !ExceptionNotifier.config[:exception_recipients].blank?
204
+ # Send email with ActionMailer
205
+ ExceptionNotifier.deliver_exception_notification(exception, self,
206
+ request, data, the_blamed)
207
+ end
208
+ end
209
+
210
+ def should_notify_on_exception?(exception, status_cd = nil)
211
+ # First honor the custom settings from environment
212
+ return false if ExceptionNotifier.config[:render_only]
213
+ # don't mail exceptions raised locally
214
+ return false if ExceptionNotifier.config[:skip_local_notification] && is_local?
215
+ # don't mail exceptions raised that match ExceptionNotifiable.silent_exceptions
216
+ return false if self.class.silent_exceptions.any? {|klass| klass === exception }
217
+ return true if ExceptionNotifier.config[:send_email_error_classes].include?(exception)
218
+ return true if !status_cd.nil? && ExceptionNotifier.config[:send_email_error_codes].include?(status_cd)
219
+ return ExceptionNotifier.config[:send_email_other_errors]
220
+ end
221
+
222
+ def is_local?
223
+ (consider_all_requests_local || local_request?)
224
+ end
225
+
226
+ def status_code_for_exception(exception)
227
+ self.class.rails_error_classes[exception.class].nil? ? '500' : self.class.rails_error_classes[exception.class].blank? ? '200' : self.class.rails_error_classes[exception.class]
228
+ end
229
+
230
+ def exception_to_filename(exception)
231
+ exception.to_s.delete(':').gsub( /([A-Za-z])([A-Z])/, '\1' << '_' << '\2' ).downcase
232
+ end
233
+
234
+ def lay_blame(exception)
235
+ error = {}
236
+ unless(ExceptionNotifier.config[:git_repo_path].nil?)
237
+ if(exception.class == ActionView::TemplateError)
238
+ blame = blame_output(exception.line_number, "app/views/#{exception.file_name}")
239
+ error[:author] = blame[/^author\s.+$/].gsub(/author\s/,'')
240
+ error[:line] = exception.line_number
241
+ error[:file] = exception.file_name
242
+ else
243
+ exception.backtrace.each do |line|
244
+ file = exception_in_project?(line[/^.+?(?=:)/])
245
+ unless(file.nil?)
246
+ line_number = line[/:\d+:/].gsub(/[^\d]/,'')
247
+ # Use relative path or weird stuff happens
248
+ blame = blame_output(line_number, file.gsub(Regexp.new("#{RAILS_ROOT}/"),''))
249
+ error[:author] = blame[/^author\s.+$/].sub(/author\s/,'')
250
+ error[:line] = line_number
251
+ error[:file] = file
252
+ break
253
+ end
254
+ end
255
+ end
256
+ end
257
+ error
258
+ end
259
+
260
+ def blame_output(line_number, path)
261
+ app_directory = Dir.pwd
262
+ Dir.chdir ExceptionNotifier.config[:git_repo_path]
263
+ blame = `git blame -p -L #{line_number},#{line_number} #{path}`
264
+ Dir.chdir app_directory
265
+
266
+ blame
267
+ end
268
+
269
+ def exception_in_project?(path) # should be a path like /path/to/broken/thingy.rb
270
+ dir = File.split(path).first rescue ''
271
+ if(File.directory?(dir) and !(path =~ /vendor\/plugins/) and path.include?(RAILS_ROOT))
272
+ path
273
+ else
274
+ nil
275
+ end
276
+ end
277
+
278
+ end
@@ -0,0 +1,108 @@
1
+ require 'pathname'
2
+
3
+ class ExceptionNotifier < ActionMailer::Base
4
+
5
+ @@config = {
6
+ # If left empty web hooks will not be engaged
7
+ :web_hooks => [],
8
+ :app_name => "[MYAPP]",
9
+ :version => "0.0.0",
10
+ :sender_address => %("#{(defined?(Rails) ? Rails.env : RAILS_ENV).capitalize} Error" <super.exception.notifier@example.com>),
11
+ :exception_recipients => [],
12
+ # Customize the subject line
13
+ :subject_prepend => "[#{(defined?(Rails) ? Rails.env : RAILS_ENV).capitalize} ERROR] ",
14
+ :subject_append => nil,
15
+ # Include which sections of the exception email?
16
+ :sections => %w(request session environment backtrace),
17
+ # Only use this gem to render, never email
18
+ :render_only => false,
19
+ :skip_local_notification => true,
20
+ :view_path => nil,
21
+ #Error Notification will be sent if the HTTP response code for the error matches one of the following error codes
22
+ :send_email_error_codes => %W( 405 500 503 ),
23
+ #Error Notification will be sent if the error class matches one of the following error error classes
24
+ :send_email_error_classes => %W( ),
25
+ :send_email_other_errors => true,
26
+ :git_repo_path => nil,
27
+ :template_root => "#{File.dirname(__FILE__)}/../views"
28
+ }
29
+
30
+ cattr_accessor :config
31
+
32
+ def self.configure_exception_notifier(&block)
33
+ yield @@config
34
+ end
35
+
36
+ self.template_root = config[:template_root]
37
+
38
+ def self.reloadable?() false end
39
+
40
+ # What is the path of the file we will render to the user?
41
+ def self.get_view_path(status_cd)
42
+ if File.exist?("#{RAILS_ROOT}/public/#{status_cd}.html")
43
+ "#{RAILS_ROOT}/public/#{status_cd}.html"
44
+ elsif !config[:view_path].nil? && File.exist?("#{RAILS_ROOT}/#{config[:view_path]}/#{status_cd}.html")
45
+ "#{RAILS_ROOT}/#{config[:view_path]}/#{status_cd}.html"
46
+ elsif File.exist?("#{File.dirname(__FILE__)}/../rails/app/views/exception_notifiable/#{status_cd}.html")
47
+ "#{File.dirname(__FILE__)}/../rails/app/views/exception_notifiable/#{status_cd}.html"
48
+ else
49
+ "#{File.dirname(__FILE__)}/../rails/app/views/exception_notifiable/500.html"
50
+ end
51
+ end
52
+
53
+ def exception_notification(exception, controller = nil, request = nil, data={}, the_blamed=nil)
54
+ body_hash = error_environment_data_hash(exception, controller, request, data, the_blamed)
55
+ #Prefer to have custom, potentially HTML email templates available
56
+ #content_type "text/plain"
57
+ recipients config[:exception_recipients]
58
+ from config[:sender_address]
59
+
60
+ request.session.inspect unless request.nil? # Ensure session data is loaded (Rails 2.3 lazy-loading)
61
+
62
+ subject "#{config[:subject_prepend]}#{body_hash[:location]} (#{exception.class}) #{exception.message.inspect}#{config[:subject_append]}"
63
+ body body_hash
64
+ end
65
+
66
+ def background_exception_notification(exception, data = {}, the_blamed = nil)
67
+ exception_notification(exception, nil, nil, data, the_blamed)
68
+ end
69
+
70
+ private
71
+
72
+ def error_environment_data_hash(exception, controller = nil, request = nil, data={}, the_blamed=nil)
73
+ data.merge!({
74
+ :exception => exception,
75
+ :backtrace => sanitize_backtrace(exception.backtrace),
76
+ :rails_root => rails_root,
77
+ :data => data,
78
+ :the_blamed => the_blamed
79
+ })
80
+
81
+ if controller && request
82
+ data.merge!({
83
+ :location => "#{controller.controller_name}##{controller.action_name}",
84
+ :controller => controller,
85
+ :request => request,
86
+ :host => (request.env['HTTP_X_REAL_IP'] || request.env["HTTP_X_FORWARDED_HOST"] || request.env["HTTP_HOST"]),
87
+ :sections => config[:sections]
88
+ })
89
+ else
90
+ # TODO: with refactoring, the environment section could show useful ENV data even without a request
91
+ data.merge!({
92
+ :location => sanitize_backtrace([exception.backtrace.first]).first,
93
+ :sections => config[:sections] - %w(request session environment)
94
+ })
95
+ end
96
+ return data
97
+ end
98
+
99
+ def sanitize_backtrace(trace)
100
+ re = Regexp.new(/^#{Regexp.escape(rails_root)}/)
101
+ trace.map { |line| Pathname.new(line.gsub(re, "[RAILS_ROOT]")).cleanpath.to_s }
102
+ end
103
+
104
+ def rails_root
105
+ @rails_root ||= Pathname.new(RAILS_ROOT).cleanpath.to_s
106
+ end
107
+
108
+ end
@@ -0,0 +1,58 @@
1
+ require 'pp'
2
+
3
+ module ExceptionNotifierHelper
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
+ COMPAT_MODE = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION < '2' : false unless defined?(COMPAT_MODE)
8
+
9
+ def render_section(section)
10
+ RAILS_DEFAULT_LOGGER.info("rendering section #{section.inspect}")
11
+ summary = render_overridable(section).strip
12
+ unless summary.blank?
13
+ title = render_overridable(:title, :locals => { :title => section }).strip
14
+ "#{title}\n\n#{summary.gsub(/^/, " ")}\n\n"
15
+ end
16
+ end
17
+
18
+ def render_overridable(partial, options={})
19
+ if File.exist?(path = "#{APP_PATH}/_#{partial}.html.erb") ||
20
+ File.exist?(path = "#{File.dirname(__FILE__)}/../#{VIEW_PATH}/_#{partial}.html.erb") ||
21
+ File.exist?(path = "#{APP_PATH}/_#{partial}.rhtml") ||
22
+ File.exist?(path = "#{File.dirname(__FILE__)}/../#{VIEW_PATH}/_#{partial}.rhtml")
23
+ render(options.merge(:file => path, :use_full_path => false))
24
+ end
25
+ end
26
+
27
+ def inspect_model_object(model, locals={})
28
+ render_overridable(:inspect_model,
29
+ :locals => { :inspect_model => model,
30
+ :show_instance_variables => true,
31
+ :show_attributes => true }.merge(locals))
32
+ end
33
+
34
+ def inspect_value(value)
35
+ len = 512
36
+ result = object_to_yaml(value).gsub(/\n/, "\n ").strip
37
+ result = result[0,len] + "... (#{result.length-len} bytes more)" if result.length > len+20
38
+ result
39
+ end
40
+
41
+ def object_to_yaml(object)
42
+ object.to_yaml.sub(/^---\s*/m, "")
43
+ end
44
+
45
+ def exclude_raw_post_parameters?
46
+ @controller && @controller.respond_to?(:filter_parameters)
47
+ end
48
+
49
+ def filter_sensitive_post_data_parameters(parameters)
50
+ exclude_raw_post_parameters? ? COMPAT_MODE ? @controller.filter_parameters(parameters) : @controller.__send__(:filter_parameters, parameters) : parameters
51
+ end
52
+
53
+ def filter_sensitive_post_data_from_env(env_key, env_value)
54
+ return env_value unless exclude_raw_post_parameters?
55
+ return PARAM_FILTER_REPLACEMENT if (env_key =~ /RAW_POST_DATA/i)
56
+ return COMPAT_MODE ? @controller.filter_parameters({env_key => env_value}).values[0] : @controller.__send__(:filter_parameters, {env_key => env_value}).values[0]
57
+ end
58
+ end