exception_notification 3.0.1 → 4.0.0.rc1

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 (55) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +9 -0
  3. data/Appraisals +11 -0
  4. data/CHANGELOG.rdoc +21 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +49 -7
  7. data/README.md +417 -184
  8. data/Rakefile +4 -2
  9. data/examples/sinatra/Gemfile +8 -0
  10. data/examples/sinatra/Gemfile.lock +95 -0
  11. data/examples/sinatra/Procfile +2 -0
  12. data/examples/sinatra/README.md +11 -0
  13. data/examples/sinatra/config.ru +3 -0
  14. data/examples/sinatra/sinatra_app.rb +28 -0
  15. data/exception_notification.gemspec +10 -4
  16. data/gemfiles/rails3_1.gemfile +7 -0
  17. data/gemfiles/rails3_2.gemfile +7 -0
  18. data/gemfiles/rails4_0.gemfile +7 -0
  19. data/lib/exception_notification.rb +10 -0
  20. data/lib/exception_notification/rack.rb +45 -0
  21. data/lib/exception_notification/rails.rb +8 -0
  22. data/lib/exception_notification/resque.rb +24 -0
  23. data/lib/exception_notification/sidekiq.rb +22 -0
  24. data/lib/exception_notifier.rb +89 -61
  25. data/lib/exception_notifier/campfire_notifier.rb +2 -7
  26. data/lib/exception_notifier/email_notifier.rb +181 -0
  27. data/lib/exception_notifier/notifier.rb +9 -178
  28. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +3 -1
  29. data/lib/exception_notifier/views/exception_notifier/_data.html.erb +6 -1
  30. data/lib/exception_notifier/views/exception_notifier/_environment.html.erb +16 -6
  31. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +1 -1
  32. data/lib/exception_notifier/views/exception_notifier/_request.html.erb +24 -5
  33. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +2 -0
  34. data/lib/exception_notifier/views/exception_notifier/_session.html.erb +10 -2
  35. data/lib/exception_notifier/views/exception_notifier/_session.text.erb +1 -1
  36. data/lib/exception_notifier/views/exception_notifier/_title.html.erb +3 -3
  37. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.html.erb +38 -11
  38. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +0 -1
  39. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +39 -21
  40. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +0 -1
  41. data/lib/exception_notifier/webhook_notifier.rb +21 -0
  42. data/lib/generators/exception_notification/install_generator.rb +15 -0
  43. data/lib/generators/exception_notification/templates/exception_notification.rb +47 -0
  44. data/test/dummy/Gemfile +2 -1
  45. data/test/dummy/Gemfile.lock +79 -78
  46. data/test/dummy/config/environment.rb +9 -7
  47. data/test/dummy/test/functional/posts_controller_test.rb +22 -37
  48. data/test/{campfire_test.rb → exception_notifier/campfire_notifier_test.rb} +4 -4
  49. data/test/exception_notifier/email_notifier_test.rb +144 -0
  50. data/test/exception_notifier/webhook_notifier_test.rb +41 -0
  51. data/test/exception_notifier_test.rb +101 -0
  52. data/test/test_helper.rb +4 -1
  53. metadata +136 -18
  54. data/test/background_exception_notification_test.rb +0 -82
  55. data/test/exception_notification_test.rb +0 -73
data/Rakefile CHANGED
@@ -1,12 +1,14 @@
1
- require 'bundler'
1
+ require 'rubygems'
2
+ require 'bundler/setup'
2
3
  Bundler::GemHelper.install_tasks
4
+ require 'appraisal'
3
5
 
4
6
  require 'rake/testtask'
5
7
 
6
8
  task 'setup_dummy_app' do
7
9
  unless File.exists? "test/dummy/db/test.sqlite3"
8
10
  Bundler.with_clean_env do
9
- sh "cd test/dummy; bundle; rake db:migrate; rake db:test:prepare; cd ../../;"
11
+ sh "cd test/dummy; bundle; bundle exec rake db:migrate; bundle exec rake db:test:prepare; cd ../../;"
10
12
  end
11
13
  end
12
14
  end
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "exception_notification", path: "../../"
4
+
5
+ gem "thin", "~> 1.5.1"
6
+ gem "sinatra", "~> 1.3.5"
7
+ gem "foreman"
8
+ gem "mailcatcher"
@@ -0,0 +1,95 @@
1
+ PATH
2
+ remote: ../../
3
+ specs:
4
+ exception_notification (3.0.1)
5
+ actionmailer (>= 3.0.4)
6
+ activesupport (>= 3.0.4)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionmailer (3.2.13)
12
+ actionpack (= 3.2.13)
13
+ mail (~> 2.5.3)
14
+ actionpack (3.2.13)
15
+ activemodel (= 3.2.13)
16
+ activesupport (= 3.2.13)
17
+ builder (~> 3.0.0)
18
+ erubis (~> 2.7.0)
19
+ journey (~> 1.0.4)
20
+ rack (~> 1.4.5)
21
+ rack-cache (~> 1.2)
22
+ rack-test (~> 0.6.1)
23
+ sprockets (~> 2.2.1)
24
+ activemodel (3.2.13)
25
+ activesupport (= 3.2.13)
26
+ builder (~> 3.0.0)
27
+ activesupport (3.2.13)
28
+ i18n (= 0.6.1)
29
+ multi_json (~> 1.0)
30
+ builder (3.0.4)
31
+ daemons (1.1.9)
32
+ erubis (2.7.0)
33
+ eventmachine (1.0.3)
34
+ foreman (0.61.0)
35
+ thor (>= 0.13.6)
36
+ haml (4.0.2)
37
+ tilt
38
+ hike (1.2.1)
39
+ i18n (0.6.1)
40
+ journey (1.0.4)
41
+ mail (2.5.3)
42
+ i18n (>= 0.4.0)
43
+ mime-types (~> 1.16)
44
+ treetop (~> 1.4.8)
45
+ mailcatcher (0.5.11)
46
+ activesupport (~> 3.0)
47
+ eventmachine (~> 1.0.0)
48
+ haml (>= 3.1, < 5)
49
+ mail (~> 2.3)
50
+ sinatra (~> 1.2)
51
+ skinny (~> 0.2.3)
52
+ sqlite3 (~> 1.3)
53
+ thin (~> 1.5.0)
54
+ mime-types (1.22)
55
+ multi_json (1.7.2)
56
+ polyglot (0.3.3)
57
+ rack (1.4.5)
58
+ rack-cache (1.2)
59
+ rack (>= 0.4)
60
+ rack-protection (1.5.0)
61
+ rack
62
+ rack-test (0.6.2)
63
+ rack (>= 1.0)
64
+ sinatra (1.3.6)
65
+ rack (~> 1.4)
66
+ rack-protection (~> 1.3)
67
+ tilt (~> 1.3, >= 1.3.3)
68
+ skinny (0.2.3)
69
+ eventmachine (~> 1.0.0)
70
+ thin (~> 1.5.0)
71
+ sprockets (2.2.2)
72
+ hike (~> 1.2)
73
+ multi_json (~> 1.0)
74
+ rack (~> 1.0)
75
+ tilt (~> 1.1, != 1.3.0)
76
+ sqlite3 (1.3.7)
77
+ thin (1.5.1)
78
+ daemons (>= 1.0.9)
79
+ eventmachine (>= 0.12.6)
80
+ rack (>= 1.0.0)
81
+ thor (0.18.1)
82
+ tilt (1.3.6)
83
+ treetop (1.4.12)
84
+ polyglot
85
+ polyglot (>= 0.3.1)
86
+
87
+ PLATFORMS
88
+ ruby
89
+
90
+ DEPENDENCIES
91
+ exception_notification!
92
+ foreman
93
+ mailcatcher
94
+ sinatra (~> 1.3.5)
95
+ thin (~> 1.5.1)
@@ -0,0 +1,2 @@
1
+ web: bundle exec thin start --port 3000
2
+ mail: bundle exec mailcatcher --foreground --smtp-port 1025 --http-port 1080
@@ -0,0 +1,11 @@
1
+ # Using Exception Notification with Sinatra
2
+
3
+ ## Quick start
4
+
5
+ git clone git@github.com:smartinez87/exception_notification.git
6
+ cd exception_notification/examples/sinatra
7
+ bundle install
8
+ bundle exec foreman start
9
+
10
+
11
+ The last command starts two services, a smtp server and the sinatra app itself. Thus, visit [http://localhost:1080/](http://localhost:1080/) to check the emails sent and, in a separated tab, visit [http://localhost:3000](http://localhost:3000) and cause some errors. For more info, use the [source](https://github.com/smartinez87/exception_notification/blob/master/examples/sinatra/sinatra_app.rb) Luke.
@@ -0,0 +1,3 @@
1
+ require ::File.expand_path('../sinatra_app', __FILE__)
2
+
3
+ run SinatraApp
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'sinatra/base'
4
+ require 'exception_notification'
5
+
6
+ class SinatraApp < Sinatra::Base
7
+ use ExceptionNotification::Rack,
8
+ :email => {
9
+ :email_prefix => "[Example] ",
10
+ :sender_address => %{"notifier" <notifier@example.com>},
11
+ :exception_recipients => %w{exceptions@example.com},
12
+ :smtp_settings => { :address => "localhost", :port => 1025 }
13
+ }
14
+
15
+ get '/' do
16
+ raise StandardError, "ERROR: #{params[:error]}" unless params[:error].blank?
17
+ 'Everything is fine! Now, lets break things clicking <a href="/?error=ops"> here </a>. Dont forget to see the emails at <a href="http://localhost:1080">mailcatcher</a> !'
18
+ end
19
+
20
+ get '/background_notification' do
21
+ begin
22
+ 1/0
23
+ rescue Exception => e
24
+ ExceptionNotifier.notify_exception(e, :data => {:msg => "Cannot divide by zero!"})
25
+ end
26
+ 'Check email at <a href="http://localhost:1080">mailcatcher</a>.'
27
+ end
28
+ end
@@ -1,8 +1,8 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'exception_notification'
3
- s.version = '3.0.1'
3
+ s.version = '4.0.0.rc1'
4
4
  s.authors = ["Jamis Buck", "Josh Peek"]
5
- s.date = %q{2013-02-02}
5
+ s.date = %q{2013-06-20}
6
6
  s.summary = "Exception notification for Rails apps"
7
7
  s.homepage = "http://smartinez87.github.com/exception_notification"
8
8
  s.email = "smartinez87@gmail.com"
@@ -12,9 +12,15 @@ Gem::Specification.new do |s|
12
12
  s.require_path = 'lib'
13
13
 
14
14
  s.add_dependency("actionmailer", ">= 3.0.4")
15
+ s.add_dependency("activesupport", ">= 3.0.4")
15
16
 
16
- s.add_development_dependency "tinder", "~> 1.8"
17
17
  s.add_development_dependency "rails", ">= 3.0.4"
18
- s.add_development_dependency "mocha", ">= 0.11.3"
18
+ s.add_development_dependency "resque", "~> 1.2.0"
19
+ s.add_development_dependency "sidekiq", "~> 2.0"
20
+ s.add_development_dependency "tinder", "~> 1.8"
21
+ s.add_development_dependency "httparty", "~> 0.10.2"
22
+ s.add_development_dependency "mocha", ">= 0.13.0"
19
23
  s.add_development_dependency "sqlite3", ">= 1.3.4"
24
+ s.add_development_dependency "coveralls", "~> 0.6.5"
25
+ s.add_development_dependency "appraisal", ">= 0"
20
26
  end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 3.1.0"
6
+
7
+ gemspec :path=>"../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 3.2.0"
6
+
7
+ gemspec :path=>"../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.0.0.rc2"
6
+
7
+ gemspec :path=>"../"
@@ -1 +1,11 @@
1
1
  require 'exception_notifier'
2
+ require 'exception_notification/rack'
3
+
4
+ module ExceptionNotification
5
+ # Alternative way to setup ExceptionNotification.
6
+ # Run 'rails generate exception_notification:install' to create
7
+ # a fresh initializer with all configuration values.
8
+ def self.configure
9
+ yield ExceptionNotifier
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module ExceptionNotification
2
+ class Rack
3
+ def initialize(app, options = {})
4
+ @app = app
5
+
6
+ ExceptionNotifier.ignored_exceptions = options.delete(:ignore_exceptions) if options.key?(:ignore_exceptions)
7
+
8
+ if options.key?(:ignore_if)
9
+ rack_ignore = options.delete(:ignore_if)
10
+ ExceptionNotifier.ignore_if do |exception, options|
11
+ options.key?(:env) && rack_ignore.call(options[:env], exception)
12
+ end
13
+ end
14
+
15
+ if options.key?(:ignore_crawlers)
16
+ ignore_crawlers = options.delete(:ignore_crawlers)
17
+ ExceptionNotifier.ignore_if do |exception, options|
18
+ options.key?(:env) && from_crawler(options[:env], ignore_crawlers)
19
+ end
20
+ end
21
+
22
+ options.each do |notifier_name, options|
23
+ ExceptionNotifier.register_exception_notifier(notifier_name, options)
24
+ end
25
+ end
26
+
27
+ def call(env)
28
+ @app.call(env)
29
+ rescue Exception => exception
30
+ if ExceptionNotifier.notify_exception(exception, :env => env)
31
+ env['exception_notifier.delivered'] = true
32
+ end
33
+ raise exception
34
+ end
35
+
36
+ private
37
+
38
+ def from_crawler(env, ignored_crawlers)
39
+ agent = env['HTTP_USER_AGENT']
40
+ Array(ignored_crawlers).any? do |crawler|
41
+ agent =~ Regexp.new(crawler)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ module ExceptionNotification
2
+ class Engine < ::Rails::Engine
3
+ config.exception_notification = ExceptionNotifier
4
+ config.exception_notification.logger = Rails.logger
5
+
6
+ config.app_middleware.use ExceptionNotification::Rack
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ require 'resque/failure/base'
2
+
3
+ module ExceptionNotification
4
+ class Resque < Resque::Failure::Base
5
+
6
+ def self.count
7
+ Stat[:failed]
8
+ end
9
+
10
+ def save
11
+ data = {
12
+ :failed_at => Time.now.to_s,
13
+ :queue => queue,
14
+ :worker => worker.to_s,
15
+ :payload => payload,
16
+ :error_class => exception.class.name,
17
+ :error_message => exception.message
18
+ }
19
+
20
+ ExceptionNotifier.notify_exception(exception, :data => { :resque => data })
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ require 'sidekiq'
2
+
3
+ module ExceptionNotification
4
+ class Sidekiq
5
+
6
+ def call(worker, msg, queue)
7
+ begin
8
+ yield
9
+ rescue Exception => exception
10
+ ExceptionNotifier.notify_exception(exception, :data => { :sidekiq => msg })
11
+ raise exception
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+
18
+ ::Sidekiq.configure_server do |config|
19
+ config.server_middleware do |chain|
20
+ chain.add ::ExceptionNotification::Sidekiq
21
+ end
22
+ end
@@ -1,75 +1,103 @@
1
- require 'action_dispatch'
2
- require 'exception_notifier/notifier'
3
- require 'exception_notifier/campfire_notifier'
1
+ require 'logger'
2
+ require 'active_support/core_ext/string/inflections'
4
3
 
5
- class ExceptionNotifier
4
+ module ExceptionNotifier
6
5
 
7
- def self.default_ignore_exceptions
8
- [].tap do |exceptions|
9
- exceptions << 'ActiveRecord::RecordNotFound'
10
- exceptions << 'AbstractController::ActionNotFound'
11
- exceptions << 'ActionController::RoutingError'
6
+ autoload :Notifier, 'exception_notifier/notifier'
7
+ autoload :EmailNotifier, 'exception_notifier/email_notifier'
8
+ autoload :CampfireNotifier, 'exception_notifier/campfire_notifier'
9
+ autoload :WebhookNotifier, 'exception_notifier/webhook_notifier'
10
+
11
+ class UndefinedNotifierError < StandardError; end
12
+
13
+ # Define logger
14
+ mattr_accessor :logger
15
+ @@logger = Logger.new(STDOUT)
16
+
17
+ # Define a set of exceptions to be ignored, ie, dont send notifications when any of them are raised.
18
+ mattr_accessor :ignored_exceptions
19
+ @@ignored_exceptions = %w{ActiveRecord::RecordNotFound AbstractController::ActionNotFound ActionController::RoutingError}
20
+
21
+ class << self
22
+ # Store conditions that decide when exceptions must be ignored or not.
23
+ @@ignores = []
24
+
25
+ # Store notifiers that send notifications when exceptions are raised.
26
+ @@notifiers = {}
27
+
28
+ def notify_exception(exception, options={})
29
+ return false if ignored_exception?(options[:ignore_exceptions], exception)
30
+ return false if ignored?(exception, options)
31
+ selected_notifiers = options.delete(:notifiers) || notifiers
32
+ [*selected_notifiers].each do |notifier|
33
+ fire_notification(notifier, exception, options.dup)
34
+ end
35
+ true
12
36
  end
13
- end
14
37
 
15
- def self.default_ignore_crawlers
16
- []
17
- end
38
+ def register_exception_notifier(name, notifier_or_options)
39
+ if notifier_or_options.respond_to?(:call)
40
+ @@notifiers[name] = notifier_or_options
41
+ elsif notifier_or_options.is_a?(Hash)
42
+ create_and_register_notifier(name, notifier_or_options)
43
+ else
44
+ raise ArgumentError, "Invalid notifier '#{name}' defined as #{notifier_or_options.inspect}"
45
+ end
46
+ end
47
+ alias add_notifier register_exception_notifier
18
48
 
19
- def initialize(app, options = {})
20
- @app, @options = app, options
21
-
22
- Notifier.default_sender_address = @options[:sender_address]
23
- Notifier.default_exception_recipients = @options[:exception_recipients]
24
- Notifier.default_email_prefix = @options[:email_prefix]
25
- Notifier.default_email_format = @options[:email_format]
26
- Notifier.default_sections = @options[:sections]
27
- Notifier.default_background_sections = @options[:background_sections]
28
- Notifier.default_verbose_subject = @options[:verbose_subject]
29
- Notifier.default_normalize_subject = @options[:normalize_subject]
30
- Notifier.default_smtp_settings = @options[:smtp_settings]
31
- Notifier.default_email_headers = @options[:email_headers]
32
-
33
- @campfire = CampfireNotifier.new @options[:campfire]
34
-
35
- @options[:ignore_exceptions] ||= self.class.default_ignore_exceptions
36
- @options[:ignore_crawlers] ||= self.class.default_ignore_crawlers
37
- @options[:ignore_if] ||= lambda { |env, e| false }
38
- end
49
+ def unregister_exception_notifier(name)
50
+ @@notifiers.delete(name)
51
+ end
39
52
 
40
- def call(env)
41
- @app.call(env)
42
- rescue Exception => exception
43
- options = (env['exception_notifier.options'] ||= Notifier.default_options)
44
- options.reverse_merge!(@options)
45
-
46
- unless ignored_exception(options[:ignore_exceptions], exception) ||
47
- from_crawler(options[:ignore_crawlers], env['HTTP_USER_AGENT']) ||
48
- conditionally_ignored(options[:ignore_if], env, exception)
49
- Notifier.exception_notification(env, exception).deliver
50
- @campfire.exception_notification(exception)
51
- env['exception_notifier.delivered'] = true
53
+ def registered_exception_notifier(name)
54
+ @@notifiers[name]
52
55
  end
53
56
 
54
- raise exception
55
- end
57
+ def notifiers
58
+ @@notifiers.keys
59
+ end
56
60
 
57
- private
61
+ # Adds a condition to decide when an exception must be ignored or not.
62
+ #
63
+ # ExceptionNotifier.ignore_if do |exception, options|
64
+ # not Rails.env.production?
65
+ # end
66
+ def ignore_if(&block)
67
+ @@ignores << block
68
+ end
58
69
 
59
- def ignored_exception(ignore_array, exception)
60
- Array.wrap(ignore_array).map(&:to_s).include?(exception.class.name)
61
- end
70
+ def clear_ignore_conditions!
71
+ @@ignores.clear
72
+ end
62
73
 
63
- def from_crawler(ignore_array, agent)
64
- ignore_array.each do |crawler|
65
- return true if (agent =~ Regexp.new(crawler))
66
- end unless ignore_array.blank?
67
- false
68
- end
74
+ private
75
+ def ignored?(exception, options)
76
+ @@ignores.any?{ |condition| condition.call(exception, options) }
77
+ rescue Exception => e
78
+ logger.warn "An error occurred when evaluating an ignore condition. #{e.class}: #{e.message}"
79
+ false
80
+ end
69
81
 
70
- def conditionally_ignored(ignore_proc, env, exception)
71
- ignore_proc.call(env, exception)
72
- rescue Exception => ex
73
- false
82
+ def ignored_exception?(ignore_array, exception)
83
+ (Array(ignored_exceptions) + Array(ignore_array)).map(&:to_s).include?(exception.class.name)
84
+ end
85
+
86
+ def fire_notification(notifier_name, exception, options)
87
+ notifier = registered_exception_notifier(notifier_name)
88
+ notifier.call(exception, options)
89
+ rescue Exception => e
90
+ logger.warn "An error occurred when sending a notification using '#{notifier_name}' notifier. #{e.class}: #{e.message}"
91
+ false
92
+ end
93
+
94
+ def create_and_register_notifier(name, options)
95
+ notifier_classname = "#{name}_notifier".camelize
96
+ notifier_class = ExceptionNotifier.const_get(notifier_classname)
97
+ notifier = notifier_class.new(options)
98
+ register_exception_notifier(name, notifier)
99
+ rescue NameError => e
100
+ raise UndefinedNotifierError, "No notifier named '#{name}' was found. Please, revise your configuration options. Cause: #{e.message}"
101
+ end
74
102
  end
75
103
  end