bmpercy-exception_notification 2.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README.rdoc +539 -0
  3. data/VERSION.yml +5 -0
  4. data/init.rb +1 -0
  5. data/lib/exception_notifiable.rb +177 -0
  6. data/lib/exception_notifier.rb +176 -0
  7. data/lib/exception_notifier_helper.rb +60 -0
  8. data/lib/notifiable.rb +92 -0
  9. data/lib/super_exception_notifier/custom_exception_classes.rb +16 -0
  10. data/lib/super_exception_notifier/custom_exception_methods.rb +50 -0
  11. data/lib/super_exception_notifier/deprecated_methods.rb +60 -0
  12. data/lib/super_exception_notifier/git_blame.rb +52 -0
  13. data/lib/super_exception_notifier/helpful_hashes.rb +66 -0
  14. data/lib/super_exception_notifier/hooks_notifier.rb +55 -0
  15. data/lib/super_exception_notifier/notifiable_helper.rb +80 -0
  16. data/rails/app/views/exception_notifiable/400.html +5 -0
  17. data/rails/app/views/exception_notifiable/403.html +6 -0
  18. data/rails/app/views/exception_notifiable/404.html +6 -0
  19. data/rails/app/views/exception_notifiable/405.html +6 -0
  20. data/rails/app/views/exception_notifiable/410.html +7 -0
  21. data/rails/app/views/exception_notifiable/418.html +6 -0
  22. data/rails/app/views/exception_notifiable/422.html +5 -0
  23. data/rails/app/views/exception_notifiable/423.html +6 -0
  24. data/rails/app/views/exception_notifiable/501.html +8 -0
  25. data/rails/app/views/exception_notifiable/503.html +6 -0
  26. data/rails/app/views/exception_notifiable/method_disabled.html.erb +6 -0
  27. data/rails/init.rb +25 -0
  28. data/tasks/notified_task.rake +15 -0
  29. data/test/exception_notifiable_test.rb +34 -0
  30. data/test/exception_notifier_helper_test.rb +76 -0
  31. data/test/exception_notifier_test.rb +41 -0
  32. data/test/exception_notify_functional_test.rb +139 -0
  33. data/test/mocks/controllers.rb +82 -0
  34. data/test/notifiable_test.rb +79 -0
  35. data/test/test_helper.rb +32 -0
  36. data/views/exception_notifier/_backtrace.html.erb +1 -0
  37. data/views/exception_notifier/_environment.html.erb +14 -0
  38. data/views/exception_notifier/_inspect_model.html.erb +16 -0
  39. data/views/exception_notifier/_request.html.erb +8 -0
  40. data/views/exception_notifier/_session.html.erb +6 -0
  41. data/views/exception_notifier/_title.html.erb +3 -0
  42. data/views/exception_notifier/background_exception_notification.text.plain.erb +10 -0
  43. data/views/exception_notifier/exception_notification.text.plain.erb +15 -0
  44. data/views/exception_notifier/rake_exception_notification.text.plain.erb +6 -0
  45. metadata +132 -0
@@ -0,0 +1,16 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module SuperExceptionNotifier
5
+ module CustomExceptionClasses
6
+
7
+ class AccessDenied < StandardError; end
8
+ class ResourceGone < StandardError; end
9
+ class NotImplemented < StandardError; end
10
+ class PageNotFound < StandardError; end
11
+ class InvalidMethod < StandardError; end
12
+ class CorruptData < StandardError; end
13
+ class MethodDisabled < StandardError; end
14
+
15
+ end
16
+ 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 SuperExceptionNotifier
5
+ module CustomExceptionMethods
6
+
7
+ protected
8
+
9
+ #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.
10
+ def resource_gone
11
+ raise ResourceGone
12
+ end
13
+ #Then for things that have never existed or have not for a long time we call not_implemented
14
+ def not_implemented
15
+ raise NotImplemented
16
+ end
17
+ #Resources that must be requested with a specific HTTP Method (GET, PUT, POST, DELETE, AJAX, etc) but are requested otherwise should:
18
+ def invalid_method
19
+ raise InvalidMethod
20
+ end
21
+ #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:
22
+ def corrupt_data
23
+ raise CorruptData
24
+ end
25
+ def page_not_found
26
+ raise PageNotFound
27
+ end
28
+ def record_not_found
29
+ raise ActiveRecord::RecordNotFound
30
+ end
31
+ def method_disabled
32
+ raise MethodDisabled
33
+ end
34
+ #The current user does not have enough privileges to access the requested resource
35
+ def access_denied
36
+ raise AccessDenied
37
+ end
38
+
39
+ def generic_error
40
+ error_stickie("Sorry, an error has occurred.")
41
+ corrupt_data
42
+ end
43
+
44
+ def invalid_page
45
+ error_stickie("Sorry, the page number you requested was not valid.")
46
+ page_not_found
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,60 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module SuperExceptionNotifier
5
+ module DeprecatedMethods
6
+ @@namespacing = "Better namespacing to allow for Notifiable and ExceptionNotifiable to have similar APIs"
7
+ @@rails2ruby = "An effort to make this a 'Ruby' Gem and not a stricly 'Rails' Gem"
8
+
9
+ def http_error_codes
10
+ deprecation_warning("http_error_codes", "error_class_status_codes")
11
+ error_class_status_codes
12
+ end
13
+
14
+ def http_error_codes=(arg)
15
+ deprecation_warning("http_error_codes", "error_class_status_codes")
16
+ error_class_status_codes=(arg)
17
+ end
18
+
19
+ def rails_error_codes
20
+ deprecation_warning("rails_error_codes", "error_class_status_codes", @@rails2ruby)
21
+ error_class_status_codes
22
+ end
23
+
24
+ def rails_error_codes=(arg)
25
+ deprecation_warning("rails_error_codes=", "error_class_status_codes=", @@rails2ruby)
26
+ error_class_status_codes=(arg)
27
+ end
28
+
29
+ # Now defined in Object class by init.rb & Notifiable module,
30
+ # so we need to override them for with the controller settings
31
+ def exception_notifier_verbose
32
+ deprecation_warning("exception_notifier_verbose", "exception_notifiable_verbose", @@namespacing)
33
+ exception_notifiable_verbose
34
+ end
35
+ def silent_exceptions
36
+ deprecation_warning("silent_exceptions", "exception_notifiable_silent_exceptions", @@namespacing)
37
+ exception_notifiable_silent_exceptions
38
+ end
39
+ def notification_level
40
+ deprecation_warning("notification_level", "exception_notifiable_notification_level", @@namespacing)
41
+ exception_notifiable_notification_level
42
+ end
43
+ def exception_notifier_verbose=(arg)
44
+ deprecation_warning("exception_notifier_verbose=", "exception_notifiable_verbose=", @@namespacing)
45
+ exception_notifiable_verbose = arg
46
+ end
47
+ def silent_exceptions=(arg)
48
+ deprecation_warning("silent_exceptions=", "exception_notifiable_silent_exceptions=", @@namespacing)
49
+ exception_notifiable_silent_exceptions = arg
50
+ end
51
+ def notification_level=(arg)
52
+ deprecation_warning("notification_level=", "exception_notifiable_notification_level=", @@namespacing)
53
+ exception_notifiable_notification_level = arg
54
+ end
55
+
56
+ def deprecation_warning(old, new, reason = "")
57
+ puts "[DEPRECATION WARNING] ** Method '#{old}' has been replaced by '#{new}', please update your code.#{' Reason for change: ' + reason + '.' if reason}"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,52 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module SuperExceptionNotifier
5
+ module GitBlame
6
+
7
+ def lay_blame(exception)
8
+ error = {}
9
+ unless(ExceptionNotifier.config[:git_repo_path].nil?)
10
+ if(exception.class == ActionView::TemplateError)
11
+ blame = blame_output(exception.line_number, "app/views/#{exception.file_name}")
12
+ error[:author] = blame[/^author\s.+$/].gsub(/author\s/,'')
13
+ error[:line] = exception.line_number
14
+ error[:file] = exception.file_name
15
+ else
16
+ exception.backtrace.each do |line|
17
+ file = exception_in_project?(line[/^.+?(?=:)/])
18
+ unless(file.nil?)
19
+ line_number = line[/:\d+:/].gsub(/[^\d]/,'')
20
+ # Use relative path or weird stuff happens
21
+ blame = blame_output(line_number, file.gsub(Regexp.new("#{RAILS_ROOT}/"),''))
22
+ error[:author] = blame[/^author\s.+$/].sub(/author\s/,'')
23
+ error[:line] = line_number
24
+ error[:file] = file
25
+ break
26
+ end
27
+ end
28
+ end
29
+ end
30
+ error
31
+ end
32
+
33
+ def blame_output(line_number, path)
34
+ app_directory = Dir.pwd
35
+ Dir.chdir ExceptionNotifier.config[:git_repo_path]
36
+ blame = `git blame -p -L #{line_number},#{line_number} #{path}`
37
+ Dir.chdir app_directory
38
+
39
+ blame
40
+ end
41
+
42
+ def exception_in_project?(path) # should be a path like /path/to/broken/thingy.rb
43
+ dir = File.split(path).first rescue ''
44
+ if(File.directory?(dir) and !(path =~ /vendor\/plugins/) and !(path =~ /vendor\/gems/) and path.include?(RAILS_ROOT))
45
+ path
46
+ else
47
+ nil
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module SuperExceptionNotifier
5
+ module HelpfulHashes
6
+ unless defined?(SILENT_EXCEPTIONS)
7
+ noiseless = []
8
+ noiseless << ActiveRecord::RecordNotFound if defined?(ActiveRecord)
9
+ if defined?(ActionController)
10
+ noiseless << ActionController::UnknownController
11
+ noiseless << ActionController::UnknownAction
12
+ noiseless << ActionController::RoutingError
13
+ noiseless << ActionController::MethodNotAllowed
14
+ end
15
+ SILENT_EXCEPTIONS = noiseless
16
+ end
17
+
18
+ # TODO: use ActionController::StatusCodes
19
+ HTTP_STATUS_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_STATUS_CODES)
32
+
33
+ def codes_for_error_classes
34
+ #TODO: Format whitespace
35
+ classes = {
36
+ # These are standard errors in rails / ruby
37
+ NameError => "503",
38
+ TypeError => "503",
39
+ RuntimeError => "500",
40
+ ArgumentError => "500",
41
+ # These are custom error names defined in lib/super_exception_notifier/custom_exception_classes
42
+ AccessDenied => "403",
43
+ PageNotFound => "404",
44
+ InvalidMethod => "405",
45
+ ResourceGone => "410",
46
+ CorruptData => "422",
47
+ NoMethodError => "500",
48
+ NotImplemented => "501",
49
+ MethodDisabled => "200"
50
+ }
51
+ # Highly dependent on the verison of rails, so we're very protective about these'
52
+ classes.merge!({ ActionView::TemplateError => "500"}) if defined?(ActionView) && ActionView.const_defined?(:TemplateError)
53
+ classes.merge!({ ActiveRecord::RecordNotFound => "400" }) if defined?(ActiveRecord) && ActiveRecord.const_defined?(:RecordNotFound)
54
+ classes.merge!({ ActiveResource::ResourceNotFound => "404" }) if defined?(ActiveResource) && ActiveResource.const_defined?(:ResourceNotFound)
55
+
56
+ if defined?(ActionController)
57
+ classes.merge!({ ActionController::UnknownController => "404" }) if ActionController.const_defined?(:UnknownController)
58
+ classes.merge!({ ActionController::MissingTemplate => "404" }) if ActionController.const_defined?(:MissingTemplate)
59
+ classes.merge!({ ActionController::MethodNotAllowed => "405" }) if ActionController.const_defined?(:MethodNotAllowed)
60
+ classes.merge!({ ActionController::UnknownAction => "501" }) if ActionController.const_defined?(:UnknownAction)
61
+ classes.merge!({ ActionController::RoutingError => "404" }) if ActionController.const_defined?(:RoutingError)
62
+ classes.merge!({ ActionController::InvalidAuthenticityToken => "405" }) if ActionController.const_defined?(:InvalidAuthenticityToken)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module SuperExceptionNotifier
5
+ module HooksNotifier
6
+ # Deliver exception data hash to web hooks, if any
7
+ #
8
+ def self.deliver_exception_to_web_hooks(config, exception, controller, request, data={}, the_blamed = nil)
9
+ params = build_web_hook_params(config, exception, controller, request, data, the_blamed)
10
+ # TODO: use threads here
11
+ config[:web_hooks].each do |address|
12
+ post_hook(params, address)
13
+ end
14
+ end
15
+
16
+
17
+ # Parameters hash based on Merb Exceptions example
18
+ #
19
+ def self.build_web_hook_params(config, exception, controller, request, data={}, the_blamed = nil)
20
+ host = (request.env["HTTP_X_FORWARDED_HOST"] || request.env["HTTP_HOST"])
21
+ p = {
22
+ 'environment' => (defined?(Rails) ? Rails.env : RAILS_ENV),
23
+ 'exceptions' => [{
24
+ :class => exception.class.to_s,
25
+ :backtrace => exception.backtrace,
26
+ :message => exception.message
27
+ }],
28
+ 'app_name' => config[:app_name],
29
+ 'version' => config[:version],
30
+ 'blame' => "#{the_blamed}"
31
+ }
32
+ if !request.nil?
33
+ p.merge!({'request_url' => "#{request.protocol}#{host}#{request.request_uri}"})
34
+ p.merge!({'request_action' => request.parameters['action']})
35
+ p.merge!({'request_params' => request.parameters.inspect})
36
+ end
37
+ p.merge!({'request_controller' => controller.class.name}) if !controller.nil?
38
+ p.merge!({'status' => exception.status}) if exception.respond_to?(:status)
39
+ return p
40
+ end
41
+
42
+ def self.post_hook(params, address)
43
+ uri = URI.parse(address)
44
+ uri.path = '/' if uri.path=='' # set a path if one isn't provided to keep Net::HTTP happy
45
+
46
+ headers = { 'Content-Type' => 'text/x-json' }
47
+ data = params.to_json
48
+ Net::HTTP.start(uri.host, uri.port) do |http|
49
+ http.request_post(uri.path, data, headers)
50
+ end
51
+ data
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,80 @@
1
+ #Copyright (c) 2008-2009 Peter H. Boling of 9thBit LLC
2
+ #Released under the MIT license
3
+
4
+ module SuperExceptionNotifier
5
+ module NotifiableHelper
6
+ include CustomExceptionClasses
7
+ include CustomExceptionMethods
8
+ include HelpfulHashes
9
+ include GitBlame
10
+ include HooksNotifier
11
+
12
+ private
13
+
14
+ def get_method_name
15
+ if /`(.*)'/.match(caller.first)
16
+ return $1
17
+ end
18
+ nil
19
+ end
20
+
21
+ def get_exception_data
22
+ deliverer = self.class.exception_data
23
+ return case deliverer
24
+ when nil then {}
25
+ when Symbol then send(deliverer)
26
+ when Proc then deliverer.call(self)
27
+ end
28
+ end
29
+
30
+ def verbose_output(exception, status_cd, file_path, send_email, send_web_hooks, request = nil, the_blamed = nil, rejected_sections = nil)
31
+ puts "[EXCEPTION] #{exception}"
32
+ puts "[EXCEPTION CLASS] #{exception.class}"
33
+ puts "[EXCEPTION STATUS_CD] #{status_cd}"
34
+ puts "[ERROR LAYOUT] #{self.class.error_layout}" if self.class.respond_to?(:error_layout)
35
+ puts "[ERROR VIEW PATH] #{ExceptionNotifier.config[:view_path]}" if !ExceptionNotifier.nil? && !ExceptionNotifier.config[:view_path].nil?
36
+ puts "[ERROR FILE PATH] #{file_path.inspect}"
37
+ puts "[ERROR EMAIL] #{send_email ? "YES" : "NO"}"
38
+ puts "[ERROR WEB HOOKS] #{send_web_hooks ? "YES" : "NO"}"
39
+ puts "[COMPAT MODE] #{ExceptionNotifierHelper::COMPAT_MODE ? "YES" : "NO"}"
40
+ puts "[THE BLAMED] #{the_blamed}"
41
+ puts "[SECTIONS] #{ExceptionNotifier.sections_for_email(rejected_sections, request)}"
42
+ req = request ? " for request_uri=#{request.request_uri} and env=#{request.env.inspect}" : ""
43
+ 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?
44
+ end
45
+
46
+ def perform_exception_notify_mailing(exception, data, request = nil, the_blamed = nil, verbose = false, rejected_sections = nil)
47
+ if ExceptionNotifier.config[:exception_recipients].blank?
48
+ puts "[EMAIL NOTIFICATION] ExceptionNotifier.config[:exception_recipients] is blank, notification cancelled!" if verbose
49
+ else
50
+ class_name = self.respond_to?(:controller_name) ? self.controller_name : self.to_s
51
+ method_name = self.respond_to?(:action_name) ? self.action_name : get_method_name
52
+ ExceptionNotifier.deliver_exception_notification(exception, class_name, method_name,
53
+ request, data, the_blamed, rejected_sections)
54
+ puts "[EMAIL NOTIFICATION] Sent" if verbose
55
+ end
56
+ end
57
+
58
+ def should_email_on_exception?(exception, status_cd = nil, verbose = false)
59
+ notification_level_sends_email? && !ExceptionNotifier.config[:exception_recipients].empty? && should_notify_on_exception?(exception, status_cd, verbose)
60
+ end
61
+
62
+ def should_web_hook_on_exception?(exception, status_cd = nil, verbose = false)
63
+ notification_level_sends_web_hooks? && !ExceptionNotifier.config[:web_hooks].empty? && should_notify_on_exception?(exception, status_cd, verbose)
64
+ end
65
+
66
+ # Relies on the base class to define be_silent_for_exception?
67
+ def should_notify_on_exception?(exception, status_cd = nil, verbose = false)
68
+ # don't notify (email or web hooks) on exceptions raised locally
69
+ verbose && ExceptionNotifier.config[:skip_local_notification] && is_local? ?
70
+ "[NOTIFY LOCALLY] NO" :
71
+ nil
72
+ return false if ExceptionNotifier.config[:skip_local_notification] && is_local?
73
+ # don't notify (email or web hooks) exceptions raised that match ExceptionNotifiable.notifiable_silent_exceptions
74
+ return false if self.be_silent_for_exception?(exception)
75
+ return true if ExceptionNotifier.config[:notify_error_classes].include?(exception.class)
76
+ return true if !status_cd.nil? && ExceptionNotifier.config[:notify_error_codes].include?(status_cd)
77
+ return ExceptionNotifier.config[:notify_other_errors]
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/400.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Unable to Process Requested Data</h1>
4
+ <p>Reloading the page will probably not help.</p>
5
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/403.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Access Restricted</h1>
4
+
5
+ <p>If you need access to this resource please contact support or an administrator.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/404.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The page you were looking for doesn't exist.</h1>
4
+
5
+ <p>You may have mistyped the address or the page may have moved.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/404.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Invalid Method.</h1>
4
+
5
+ <p>This resource only accepts certain HTTP methods.</p>
6
+ </div>
@@ -0,0 +1,7 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/410.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Requested resource is no longer available</h1>
4
+
5
+ <p>Please remove bookmarks to this resource.</p>
6
+ <p>If you arrived here via a link from somewhere else please inform them of the broken link.</p>
7
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/418.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>418 I�m a teapot</h1>
4
+
5
+ <p>I MAY be short and stout. Defined by the April Fools specification RFC 2324. See Hyper Text Coffee Pot Control Protocol for more information.</p>
6
+ </div>
@@ -0,0 +1,5 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/422.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Unprocessable Request</h1>
4
+ <p>The request was well-formed but was unable to be followed due to semantic errors.</p>
5
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/super_exception_notifier/rails/app/views/exception_notifiable/423.html. See Readme for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The change you wanted was rejected because the resource is locked</h1>
4
+ <p>It might become available again soon, if you try again!</p>
5
+ <p>Maybe you tried to change something you didn't have access to.</p>
6
+ </div>