exception_notification_more_info 1.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 (118) hide show
  1. checksums.yaml +7 -0
  2. data/Appraisals +7 -0
  3. data/CHANGELOG.rdoc +141 -0
  4. data/CODE_OF_CONDUCT.md +22 -0
  5. data/CONTRIBUTING.md +42 -0
  6. data/Gemfile +3 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +839 -0
  9. data/Rakefile +23 -0
  10. data/examples/sinatra/Gemfile +8 -0
  11. data/examples/sinatra/Gemfile.lock +95 -0
  12. data/examples/sinatra/Procfile +2 -0
  13. data/examples/sinatra/README.md +11 -0
  14. data/examples/sinatra/config.ru +3 -0
  15. data/examples/sinatra/sinatra_app.rb +32 -0
  16. data/exception_notification_more_info.gemspec +34 -0
  17. data/gemfiles/rails4_0.gemfile +7 -0
  18. data/gemfiles/rails4_1.gemfile +7 -0
  19. data/gemfiles/rails4_2.gemfile +7 -0
  20. data/lib/exception_notification.rb +11 -0
  21. data/lib/exception_notification/rack.rb +59 -0
  22. data/lib/exception_notification/rails.rb +8 -0
  23. data/lib/exception_notification/resque.rb +24 -0
  24. data/lib/exception_notification/sidekiq.rb +31 -0
  25. data/lib/exception_notifier.rb +121 -0
  26. data/lib/exception_notifier/base_notifier.rb +25 -0
  27. data/lib/exception_notifier/campfire_notifier.rb +36 -0
  28. data/lib/exception_notifier/email_notifier.rb +204 -0
  29. data/lib/exception_notifier/hipchat_notifier.rb +45 -0
  30. data/lib/exception_notifier/irc_notifier.rb +51 -0
  31. data/lib/exception_notifier/modules/backtrace_cleaner.rb +13 -0
  32. data/lib/exception_notifier/notifier.rb +16 -0
  33. data/lib/exception_notifier/slack_notifier.rb +73 -0
  34. data/lib/exception_notifier/views/exception_notifier/_backtrace.html.erb +3 -0
  35. data/lib/exception_notifier/views/exception_notifier/_backtrace.text.erb +1 -0
  36. data/lib/exception_notifier/views/exception_notifier/_data.html.erb +6 -0
  37. data/lib/exception_notifier/views/exception_notifier/_data.text.erb +1 -0
  38. data/lib/exception_notifier/views/exception_notifier/_environment.html.erb +10 -0
  39. data/lib/exception_notifier/views/exception_notifier/_environment.text.erb +5 -0
  40. data/lib/exception_notifier/views/exception_notifier/_request.html.erb +36 -0
  41. data/lib/exception_notifier/views/exception_notifier/_request.text.erb +10 -0
  42. data/lib/exception_notifier/views/exception_notifier/_session.html.erb +10 -0
  43. data/lib/exception_notifier/views/exception_notifier/_session.text.erb +2 -0
  44. data/lib/exception_notifier/views/exception_notifier/_title.html.erb +3 -0
  45. data/lib/exception_notifier/views/exception_notifier/_title.text.erb +3 -0
  46. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.html.erb +53 -0
  47. data/lib/exception_notifier/views/exception_notifier/background_exception_notification.text.erb +14 -0
  48. data/lib/exception_notifier/views/exception_notifier/exception_notification.html.erb +54 -0
  49. data/lib/exception_notifier/views/exception_notifier/exception_notification.text.erb +24 -0
  50. data/lib/exception_notifier/webhook_notifier.rb +47 -0
  51. data/lib/generators/exception_notification/install_generator.rb +15 -0
  52. data/lib/generators/exception_notification/templates/exception_notification.rb +53 -0
  53. data/test/dummy/.gitignore +4 -0
  54. data/test/dummy/Gemfile +34 -0
  55. data/test/dummy/Gemfile.lock +137 -0
  56. data/test/dummy/Rakefile +7 -0
  57. data/test/dummy/app/controllers/application_controller.rb +3 -0
  58. data/test/dummy/app/controllers/posts_controller.rb +30 -0
  59. data/test/dummy/app/helpers/application_helper.rb +2 -0
  60. data/test/dummy/app/helpers/posts_helper.rb +2 -0
  61. data/test/dummy/app/models/post.rb +2 -0
  62. data/test/dummy/app/views/exception_notifier/_new_bkg_section.html.erb +1 -0
  63. data/test/dummy/app/views/exception_notifier/_new_bkg_section.text.erb +1 -0
  64. data/test/dummy/app/views/exception_notifier/_new_section.html.erb +1 -0
  65. data/test/dummy/app/views/exception_notifier/_new_section.text.erb +1 -0
  66. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  67. data/test/dummy/app/views/posts/_form.html.erb +0 -0
  68. data/test/dummy/app/views/posts/new.html.erb +0 -0
  69. data/test/dummy/app/views/posts/show.html.erb +0 -0
  70. data/test/dummy/config.ru +4 -0
  71. data/test/dummy/config/application.rb +42 -0
  72. data/test/dummy/config/boot.rb +6 -0
  73. data/test/dummy/config/database.yml +22 -0
  74. data/test/dummy/config/environment.rb +17 -0
  75. data/test/dummy/config/environments/development.rb +25 -0
  76. data/test/dummy/config/environments/production.rb +50 -0
  77. data/test/dummy/config/environments/test.rb +38 -0
  78. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  79. data/test/dummy/config/initializers/inflections.rb +10 -0
  80. data/test/dummy/config/initializers/mime_types.rb +5 -0
  81. data/test/dummy/config/initializers/secret_token.rb +8 -0
  82. data/test/dummy/config/initializers/session_store.rb +8 -0
  83. data/test/dummy/config/locales/en.yml +5 -0
  84. data/test/dummy/config/routes.rb +3 -0
  85. data/test/dummy/db/migrate/20110729022608_create_posts.rb +15 -0
  86. data/test/dummy/db/schema.rb +24 -0
  87. data/test/dummy/db/seeds.rb +7 -0
  88. data/test/dummy/lib/tasks/.gitkeep +0 -0
  89. data/test/dummy/public/404.html +26 -0
  90. data/test/dummy/public/422.html +26 -0
  91. data/test/dummy/public/500.html +26 -0
  92. data/test/dummy/public/favicon.ico +0 -0
  93. data/test/dummy/public/images/rails.png +0 -0
  94. data/test/dummy/public/index.html +239 -0
  95. data/test/dummy/public/javascripts/application.js +2 -0
  96. data/test/dummy/public/javascripts/controls.js +965 -0
  97. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  98. data/test/dummy/public/javascripts/effects.js +1123 -0
  99. data/test/dummy/public/javascripts/prototype.js +6001 -0
  100. data/test/dummy/public/javascripts/rails.js +191 -0
  101. data/test/dummy/public/robots.txt +5 -0
  102. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  103. data/test/dummy/public/stylesheets/scaffold.css +56 -0
  104. data/test/dummy/script/rails +6 -0
  105. data/test/dummy/test/fixtures/posts.yml +11 -0
  106. data/test/dummy/test/functional/posts_controller_test.rb +224 -0
  107. data/test/dummy/test/test_helper.rb +13 -0
  108. data/test/exception_notification/rack_test.rb +20 -0
  109. data/test/exception_notifier/campfire_notifier_test.rb +100 -0
  110. data/test/exception_notifier/email_notifier_test.rb +185 -0
  111. data/test/exception_notifier/hipchat_notifier_test.rb +177 -0
  112. data/test/exception_notifier/irc_notifier_test.rb +121 -0
  113. data/test/exception_notifier/sidekiq_test.rb +27 -0
  114. data/test/exception_notifier/slack_notifier_test.rb +179 -0
  115. data/test/exception_notifier/webhook_notifier_test.rb +68 -0
  116. data/test/exception_notifier_test.rb +103 -0
  117. data/test/test_helper.rb +18 -0
  118. metadata +428 -0
@@ -0,0 +1,25 @@
1
+ module ExceptionNotifier
2
+ class BaseNotifier
3
+ attr_accessor :base_options
4
+
5
+ def initialize(options={})
6
+ @base_options = options
7
+ end
8
+
9
+ def send_notice(exception, options, message, message_opts=nil)
10
+ _pre_callback(exception, options, message, message_opts)
11
+ result = yield(message, message_opts)
12
+ _post_callback(exception, options, message, message_opts)
13
+ result
14
+ end
15
+
16
+ def _pre_callback(exception, options, message, message_opts)
17
+ @base_options[:pre_callback].call(options, self, exception.backtrace, message, message_opts) if @base_options[:pre_callback].respond_to?(:call)
18
+ end
19
+
20
+ def _post_callback(exception, options, message, message_opts)
21
+ @base_options[:post_callback].call(options, self, exception.backtrace, message, message_opts) if @base_options[:post_callback].respond_to?(:call)
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ module ExceptionNotifier
2
+ class CampfireNotifier < BaseNotifier
3
+
4
+ attr_accessor :subdomain
5
+ attr_accessor :token
6
+ attr_accessor :room
7
+
8
+ def initialize(options)
9
+ super
10
+ begin
11
+ subdomain = options.delete(:subdomain)
12
+ room_name = options.delete(:room_name)
13
+ @campfire = Tinder::Campfire.new subdomain, options
14
+ @room = @campfire.find_room_by_name room_name
15
+ rescue
16
+ @campfire = @room = nil
17
+ end
18
+ end
19
+
20
+ def call(exception, options={})
21
+ if active?
22
+ message = "A new exception occurred: '#{exception.message}'"
23
+ message += " on '#{exception.backtrace.first}'" if exception.backtrace
24
+ send_notice(exception, options, message) do |msg, _|
25
+ @room.paste msg
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def active?
33
+ !@room.nil?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,204 @@
1
+ require "active_support/core_ext/hash/reverse_merge"
2
+ require 'action_mailer'
3
+ require 'action_dispatch'
4
+ require 'pp'
5
+
6
+ module ExceptionNotifier
7
+ class EmailNotifier < BaseNotifier
8
+ attr_accessor(:sender_address, :exception_recipients,
9
+ :pre_callback, :post_callback,
10
+ :email_prefix, :email_format, :sections, :background_sections,
11
+ :verbose_subject, :normalize_subject, :delivery_method, :mailer_settings,
12
+ :email_headers, :mailer_parent, :template_path, :deliver_with)
13
+
14
+ module Mailer
15
+ class MissingController
16
+ def method_missing(*args, &block)
17
+ end
18
+ end
19
+
20
+ def self.extended(base)
21
+ base.class_eval do
22
+ self.send(:include, ExceptionNotifier::BacktraceCleaner)
23
+
24
+ # Append application view path to the ExceptionNotifier lookup context.
25
+ self.append_view_path "#{File.dirname(__FILE__)}/views"
26
+
27
+ def exception_notification(env, exception, options={}, default_options={})
28
+ load_custom_views
29
+
30
+ @env = env
31
+ @exception = exception
32
+ @options = options.reverse_merge(env['exception_notifier.options'] || {}).reverse_merge(default_options)
33
+ @kontroller = env['action_controller.instance'] || MissingController.new
34
+ @request = ActionDispatch::Request.new(env)
35
+ @backtrace = exception.backtrace ? clean_backtrace(exception) : []
36
+ @sections = @options[:sections]
37
+ @data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
38
+ @sections = @sections + %w(data) unless @data.empty?
39
+
40
+ compose_email
41
+ end
42
+
43
+ def background_exception_notification(exception, options={}, default_options={})
44
+ load_custom_views
45
+
46
+ @exception = exception
47
+ @options = options.reverse_merge(default_options)
48
+ @backtrace = exception.backtrace || []
49
+ @sections = @options[:background_sections]
50
+ @data = options[:data] || {}
51
+
52
+ compose_email
53
+ end
54
+
55
+ private
56
+
57
+ def compose_subject
58
+ subject = "#{@options[:email_prefix]}"
59
+ subject << "#{@kontroller.controller_name}##{@kontroller.action_name}" if @kontroller
60
+ subject << " (#{@exception.class})"
61
+ subject << " #{@exception.message.inspect}" if @options[:verbose_subject]
62
+ subject = EmailNotifier.normalize_digits(subject) if @options[:normalize_subject]
63
+ subject.length > 120 ? subject[0...120] + "..." : subject
64
+ end
65
+
66
+ def set_data_variables
67
+ @data.each do |name, value|
68
+ instance_variable_set("@#{name}", value)
69
+ end
70
+ end
71
+
72
+ helper_method :inspect_object
73
+
74
+ def inspect_object(object)
75
+ case object
76
+ when Hash, Array
77
+ object.inspect
78
+ else
79
+ object.to_s
80
+ end
81
+ end
82
+
83
+ helper_method :safe_encode
84
+
85
+ def safe_encode(value)
86
+ value.encode("utf-8", invalid: :replace, undef: :replace, replace: "_")
87
+ end
88
+
89
+ def html_mail?
90
+ @options[:email_format] == :html
91
+ end
92
+
93
+ def compose_email
94
+ set_data_variables
95
+ subject = compose_subject
96
+ name = @env.nil? ? 'background_exception_notification' : 'exception_notification'
97
+
98
+ headers = {
99
+ :delivery_method => @options[:delivery_method],
100
+ :to => @options[:exception_recipients],
101
+ :from => @options[:sender_address],
102
+ :subject => subject,
103
+ :template_name => name
104
+ }.merge(@options[:email_headers])
105
+
106
+ mail = mail(headers) do |format|
107
+ format.text
108
+ format.html if html_mail?
109
+ end
110
+
111
+ mail.delivery_method.settings.merge!(@options[:mailer_settings]) if @options[:mailer_settings]
112
+
113
+ mail
114
+ end
115
+
116
+ def load_custom_views
117
+ if defined?(Rails) && Rails.respond_to?(:root)
118
+ self.prepend_view_path Rails.root.nil? ? "app/views" : "#{Rails.root}/app/views"
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def initialize(options)
126
+ super
127
+ delivery_method = (options[:delivery_method] || :smtp)
128
+ mailer_settings_key = "#{delivery_method}_settings".to_sym
129
+ options[:mailer_settings] = options.delete(mailer_settings_key)
130
+
131
+ options.reverse_merge(EmailNotifier.default_options).select{|k,v|[
132
+ :sender_address, :exception_recipients,
133
+ :pre_callback, :post_callback,
134
+ :email_prefix, :email_format, :sections, :background_sections,
135
+ :verbose_subject, :normalize_subject, :delivery_method, :mailer_settings,
136
+ :email_headers, :mailer_parent, :template_path, :deliver_with].include?(k)}.each{|k,v| send("#{k}=", v)}
137
+ end
138
+
139
+ def options
140
+ @options ||= {}.tap do |opts|
141
+ self.instance_variables.each { |var| opts[var[1..-1].to_sym] = self.instance_variable_get(var) }
142
+ end
143
+ end
144
+
145
+ def mailer
146
+ @mailer ||= Class.new(mailer_parent.constantize).tap do |mailer|
147
+ mailer.extend(EmailNotifier::Mailer)
148
+ mailer.mailer_name = template_path
149
+ end
150
+ end
151
+
152
+ def call(exception, options={})
153
+ message = create_email(exception, options)
154
+
155
+ # FIXME: use `if Gem::Version.new(ActionMailer::VERSION::STRING) < Gem::Version.new('4.1')`
156
+ if deliver_with == :default
157
+ if message.respond_to?(:deliver_now)
158
+ deliver_with = :deliver_now
159
+ else
160
+ deliver_with = :deliver
161
+ end
162
+ end
163
+
164
+ message.send(deliver_with)
165
+ end
166
+
167
+ def create_email(exception, options={})
168
+ env = options[:env]
169
+ default_options = self.options
170
+ if env.nil?
171
+ send_notice(exception, options, nil, default_options) do |_, default_opts|
172
+ mailer.background_exception_notification(exception, options, default_opts)
173
+ end
174
+ else
175
+ send_notice(exception, options, nil, default_options) do |_, default_opts|
176
+ mailer.exception_notification(env, exception, options, default_opts)
177
+ end
178
+ end
179
+ end
180
+
181
+ def self.default_options
182
+ {
183
+ :sender_address => %("Exception Notifier" <exception.notifier@example.com>),
184
+ :exception_recipients => [],
185
+ :email_prefix => "[ERROR] ",
186
+ :email_format => :text,
187
+ :sections => %w(request session environment backtrace),
188
+ :background_sections => %w(backtrace data),
189
+ :verbose_subject => true,
190
+ :normalize_subject => false,
191
+ :delivery_method => nil,
192
+ :mailer_settings => nil,
193
+ :email_headers => {},
194
+ :mailer_parent => 'ActionMailer::Base',
195
+ :template_path => 'exception_notifier',
196
+ :deliver_with => :default
197
+ }
198
+ end
199
+
200
+ def self.normalize_digits(string)
201
+ string.gsub(/[0-9]+/, 'N')
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,45 @@
1
+ module ExceptionNotifier
2
+ class HipchatNotifier < BaseNotifier
3
+
4
+ attr_accessor :from
5
+ attr_accessor :room
6
+ attr_accessor :message_options
7
+
8
+ def initialize(options)
9
+ super
10
+ begin
11
+ api_token = options.delete(:api_token)
12
+ room_name = options.delete(:room_name)
13
+ opts = {
14
+ :api_version => options.delete(:api_version) || 'v1'
15
+ }
16
+ @from = options.delete(:from) || 'Exception'
17
+ @room = HipChat::Client.new(api_token, opts)[room_name]
18
+ @message_template = options.delete(:message_template) || ->(exception) {
19
+ msg = "A new exception occurred: '#{Rack::Utils.escape_html(exception.message)}'"
20
+ msg += " on '#{exception.backtrace.first}'" if exception.backtrace
21
+ msg
22
+ }
23
+ @message_options = options
24
+ @message_options[:color] ||= 'red'
25
+ rescue
26
+ @room = nil
27
+ end
28
+ end
29
+
30
+ def call(exception, options={})
31
+ return if !active?
32
+
33
+ message = @message_template.call(exception)
34
+ send_notice(exception, options, message, @message_options) do |msg, message_opts|
35
+ @room.send(@from, msg, message_opts)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def active?
42
+ !@room.nil?
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ module ExceptionNotifier
2
+ class IrcNotifier < BaseNotifier
3
+ def initialize(options)
4
+ super
5
+ @config = OpenStruct.new
6
+ parse_options(options)
7
+ end
8
+
9
+ def call(exception, options={})
10
+ message = "'#{exception.message}'"
11
+ message += " on '#{exception.backtrace.first}'" if exception.backtrace
12
+ if active?
13
+ send_notice(exception, options, message) do |msg, _|
14
+ send_message([*@config.prefix, *msg].join(' '))
15
+ end
16
+ end
17
+ end
18
+
19
+ def send_message(message)
20
+ CarrierPigeon.send @config.irc.merge({message: message})
21
+ end
22
+
23
+ private
24
+ def parse_options(options)
25
+ nick = options.fetch(:nick, 'ExceptionNotifierBot')
26
+ password = options[:password] ? ":#{options[:password]}" : nil
27
+ domain = options.fetch(:domain, nil)
28
+ port = options[:port] ? ":#{options[:port]}" : nil
29
+ channel = options.fetch(:channel, '#log')
30
+ notice = options.fetch(:notice, false)
31
+ ssl = options.fetch(:ssl, false)
32
+ join = options.fetch(:join, false)
33
+ uri = "irc://#{nick}#{password}@#{domain}#{port}/#{channel}"
34
+ prefix = options.fetch(:prefix, nil)
35
+ recipients = options[:recipients] ? options[:recipients].join(', ') + ':' : nil
36
+
37
+ @config.prefix = [*prefix, *recipients].join(' ')
38
+ @config.irc = { uri: uri, ssl: ssl, notice: notice, join: join }
39
+ end
40
+
41
+ def active?
42
+ valid_uri? @config.irc[:uri]
43
+ end
44
+
45
+ def valid_uri?(uri)
46
+ !!URI.parse(uri)
47
+ rescue URI::InvalidURIError
48
+ false
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,13 @@
1
+ module ExceptionNotifier
2
+ module BacktraceCleaner
3
+
4
+ def clean_backtrace(exception)
5
+ if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
6
+ Rails.backtrace_cleaner.send(:filter, exception.backtrace)
7
+ else
8
+ exception.backtrace
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ require 'active_support/deprecation'
2
+
3
+ module ExceptionNotifier
4
+ class Notifier
5
+
6
+ def self.exception_notification(env, exception, options={})
7
+ ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options.merge(:env => env))."
8
+ ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options.merge(:env => env))
9
+ end
10
+
11
+ def self.background_exception_notification(exception, options={})
12
+ ActiveSupport::Deprecation.warn "Please use ExceptionNotifier.notify_exception(exception, options)."
13
+ ExceptionNotifier.registered_exception_notifier(:email).create_email(exception, options)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ module ExceptionNotifier
2
+ class SlackNotifier < BaseNotifier
3
+ include ExceptionNotifier::BacktraceCleaner
4
+
5
+ attr_accessor :notifier
6
+
7
+ def initialize(options)
8
+ super
9
+ begin
10
+ @ignore_data_if = options[:ignore_data_if]
11
+
12
+ webhook_url = options.fetch(:webhook_url)
13
+ @message_opts = options.fetch(:additional_parameters, {})
14
+ @notifier = Slack::Notifier.new webhook_url, options
15
+ rescue
16
+ @notifier = nil
17
+ end
18
+ end
19
+
20
+ def call(exception, options={})
21
+ env = options[:env] || {}
22
+ title = "#{env['REQUEST_METHOD']} <#{env['REQUEST_URI']}>"
23
+ data = (env['exception_notifier.exception_data'] || {}).merge(options[:data] || {})
24
+ text = "*An exception occurred while doing*: `#{title}`\n"
25
+
26
+ clean_message = exception.message.gsub("`", "'")
27
+ fields = [ { title: 'Exception', value: clean_message} ]
28
+
29
+ fields.push({ title: 'Hostname', value: Socket.gethostname })
30
+
31
+ if exception.backtrace
32
+ formatted_backtrace = "```#{exception.backtrace.join("\n")}```"
33
+ fields.push({ title: 'Backtrace', value: formatted_backtrace })
34
+ end
35
+
36
+ unless data.empty?
37
+ deep_reject(data, @ignore_data_if) if @ignore_data_if.is_a?(Proc)
38
+ data_string = data.map{|k,v| "#{k}: #{v}"}.join("\n")
39
+ fields.push({ title: 'Data', value: "```#{data_string}```" })
40
+ end
41
+
42
+ fields.push({title: 'Request Params'}, value: options[:env]['action_dispatch.request.parameters'].to_s)
43
+ fields.push({title: 'User Agent'}, value: options[:env]['HTTP_USER_AGENT'])
44
+ fields.push({title: 'Remote IP'}, value: options[:env]['REMOTE_ADDR'])
45
+ attchs = [color: 'danger', text: text, fields: fields, mrkdwn_in: %w(text fields)]
46
+
47
+ if valid?
48
+ send_notice(exception, options, clean_message, @message_opts.merge(attachments: attchs)) do |msg, message_opts|
49
+ @notifier.ping '', message_opts
50
+ end
51
+ end
52
+ end
53
+
54
+ protected
55
+
56
+ def valid?
57
+ !@notifier.nil?
58
+ end
59
+
60
+ def deep_reject(hash, block)
61
+ hash.each do |k, v|
62
+ if v.is_a?(Hash)
63
+ deep_reject(v, block)
64
+ end
65
+
66
+ if block.call(k, v)
67
+ hash.delete(k)
68
+ end
69
+ end
70
+ end
71
+
72
+ end
73
+ end