super_exception_notifier 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README.rdoc +528 -0
  3. data/VERSION.yml +4 -0
  4. data/init.rb +1 -0
  5. data/lib/exception_notifiable.rb +175 -0
  6. data/lib/exception_notifier.rb +185 -0
  7. data/lib/exception_notifier_helper.rb +60 -0
  8. data/lib/notifiable.rb +91 -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 +79 -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. metadata +119 -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,79 @@
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)
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
+ req = request ? " for request_uri=#{request.request_uri} and env=#{request.env.inspect}" : ""
42
+ 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?
43
+ end
44
+
45
+ def perform_exception_notify_mailing(exception, data, request = nil, the_blamed = nil, verbose = false)
46
+ if ExceptionNotifier.config[:exception_recipients].blank?
47
+ puts "[EMAIL NOTIFICATION] ExceptionNotifier.config[:exception_recipients] is blank, notification cancelled!" if verbose
48
+ else
49
+ class_name = self.respond_to?(:controller_name) ? self.controller_name : self.to_s
50
+ method_name = self.respond_to?(:action_name) ? self.action_name : get_method_name
51
+ ExceptionNotifier.deliver_exception_notification(exception, class_name, method_name,
52
+ request, data, the_blamed)
53
+ puts "[EMAIL NOTIFICATION] Sent" if verbose
54
+ end
55
+ end
56
+
57
+ def should_email_on_exception?(exception, status_cd = nil, verbose = false)
58
+ notification_level_sends_email? && !ExceptionNotifier.config[:exception_recipients].empty? && should_notify_on_exception?(exception, status_cd, verbose)
59
+ end
60
+
61
+ def should_web_hook_on_exception?(exception, status_cd = nil, verbose = false)
62
+ notification_level_sends_web_hooks? && !ExceptionNotifier.config[:web_hooks].empty? && should_notify_on_exception?(exception, status_cd, verbose)
63
+ end
64
+
65
+ # Relies on the base class to define be_silent_for_exception?
66
+ def should_notify_on_exception?(exception, status_cd = nil, verbose = false)
67
+ # don't notify (email or web hooks) on exceptions raised locally
68
+ verbose && ExceptionNotifier.config[:skip_local_notification] && is_local? ?
69
+ "[NOTIFY LOCALLY] NO" :
70
+ nil
71
+ return false if ExceptionNotifier.config[:skip_local_notification] && is_local?
72
+ # don't notify (email or web hooks) exceptions raised that match ExceptionNotifiable.notifiable_silent_exceptions
73
+ return false if self.be_silent_for_exception?(exception)
74
+ return true if ExceptionNotifier.config[:notify_error_classes].include?(exception.class)
75
+ return true if !status_cd.nil? && ExceptionNotifier.config[:notify_error_codes].include?(status_cd)
76
+ return ExceptionNotifier.config[:notify_other_errors]
77
+ end
78
+ end
79
+ 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>