exception_notification 3.0.1 → 4.0.0.rc1

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