thoughtbot-hoptoad_notifier 1.1

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.
data/INSTALL ADDED
@@ -0,0 +1,60 @@
1
+ HoptoadNotifier
2
+ ===============
3
+
4
+ This is the notifier plugin for integrating apps with Hoptoad.
5
+
6
+ When an uncaught exception occurs, HoptoadNotifier will POST the relevant data
7
+ to the Hoptoad server specified in your environment.
8
+
9
+
10
+ INSTALLATION
11
+ ------------
12
+
13
+ REMOVE EXCEPTION_NOTIFIER
14
+
15
+ In your ApplicationController, REMOVE this line:
16
+
17
+ include ExceptionNotifiable
18
+
19
+ In your config/environment* files, remove all references to ExceptionNotifier
20
+
21
+ Remove the vendor/plugins/exception_notifier directory.
22
+
23
+ INSTALL HOPTOAD_NOTIFIER
24
+
25
+ From your project's RAILS_ROOT, run:
26
+
27
+ script/plugin install git://github.com/thoughtbot/hoptoad_notifier.git
28
+
29
+ CONFIGURATION
30
+
31
+ You should have something like this in config/initializers/hoptoad.rb.
32
+
33
+ HoptoadNotifier.configure do |config|
34
+ config.api_key = '1234567890abcdef'
35
+ end
36
+
37
+ (Please note that this configuration should be in a global configuration, and
38
+ is *not* enrivonment-specific. Hoptoad is smart enough to know what errors are
39
+ caused by what environments, so your staging errors don't get mixed in with
40
+ your production errors.)
41
+
42
+ Then, to enable hoptoad in your appication, include this code...
43
+
44
+ include HoptoadNotifier::Catcher
45
+
46
+ ...at the top of your ApplicationController, and all exceptions will be logged
47
+ to Hoptoad where they can be aggregated, filtered, sorted, analyzed, massaged,
48
+ and searched.
49
+
50
+ ** NOTE FOR RAILS 1.2.* USERS: **
51
+ You will need to copy the hoptoad_notifier_tasks.rake file into your
52
+ RAILS_ROOT/lib/tasks directory in order for the following to work:
53
+
54
+ You can test that hoptoad is working in your production environment by using
55
+ this rake task (from RAILS_ROOT):
56
+
57
+ rake hoptoad:test
58
+
59
+ If everything is configured properly, that task will send a notice to hoptoad
60
+ which will be visible immediately.
data/README ADDED
@@ -0,0 +1,165 @@
1
+ HoptoadNotifier
2
+ ===============
3
+
4
+ This is the notifier plugin for integrating apps with Hoptoad.
5
+
6
+ When an uncaught exception occurs, HoptoadNotifier will POST the relevant data
7
+ to the Hoptoad server specified in your environment.
8
+
9
+ INSTALLATION
10
+ ------------
11
+
12
+ REMOVE EXCEPTION_NOTIFIER
13
+
14
+ In your ApplicationController, REMOVE this line:
15
+
16
+ include ExceptionNotifiable
17
+
18
+ In your config/environment* files, remove all references to ExceptionNotifier
19
+
20
+ Remove the vendor/plugins/exception_notifier directory.
21
+
22
+ INSTALL HOPTOAD_NOTIFIER
23
+
24
+ From your project's RAILS_ROOT, run:
25
+
26
+ script/plugin install git://github.com/thoughtbot/hoptoad_notifier.git
27
+
28
+ CONFIGURATION
29
+
30
+ You should have something like this in config/initializers/hoptoad.rb.
31
+
32
+ HoptoadNotifier.configure do |config|
33
+ config.api_key = '1234567890abcdef'
34
+ end
35
+
36
+ (Please note that this configuration should be in a global configuration, and
37
+ is *not* enrivonment-specific. Hoptoad is smart enough to know what errors are
38
+ caused by what environments, so your staging errors don't get mixed in with
39
+ your production errors.)
40
+
41
+ Then, to enable hoptoad in your application, include this code...
42
+
43
+ include HoptoadNotifier::Catcher
44
+
45
+ ...in your ApplicationController, and all exceptions will be logged to Hoptoad
46
+ where they can be aggregated, filtered, sorted, analyzed, massaged, and
47
+ searched.
48
+
49
+ ** NOTE FOR RAILS 1.2.* USERS: **
50
+ You will need to copy the hoptoad_notifier_tasks.rake file into your
51
+ RAILS_ROOT/lib/tasks directory in order for the following to work:
52
+
53
+ You can test that hoptoad is working in your production environment by using
54
+ this rake task (from RAILS_ROOT):
55
+
56
+ rake hoptoad:test
57
+
58
+ If everything is configured properly, that task will send a notice to hoptoad
59
+ which will be visible immediately.
60
+
61
+ USAGE
62
+
63
+ For the most part, hoptoad works for itself. Once you've included the notifier
64
+ in your ApplicationController, all errors will be rescued by the
65
+ #rescue_action_in_public provided by the plugin.
66
+
67
+ If you want to log arbitrary things which you've rescued yourself from a
68
+ controller, you can do something like this:
69
+
70
+ ...
71
+ rescue => ex
72
+ notify_hoptoad(ex)
73
+ flash[:failure] = 'Encryptions could not be rerouted, try again.'
74
+ end
75
+ ...
76
+
77
+ The #notify_hoptoad call will send the notice over to hoptoad for later
78
+ analysis.
79
+
80
+ GOING BEYOND EXCEPTIONS
81
+
82
+ You can also pass a hash to notify_hoptoad method and store whatever you want, not just an exception. And you can also use it anywhere, not just in controllers:
83
+
84
+ begin
85
+ params = {
86
+ # params that you pass to a method that can throw an exception
87
+ }
88
+ my_unpredicable_method(params)
89
+ rescue => e
90
+ HoptoadNotifier.notify(
91
+ :error_class => "Special Error",
92
+ :error_message => "Special Error: #{e.message}",
93
+ :request => { :params => params }
94
+ )
95
+ end
96
+
97
+ While in your controllers you use the notify_hoptoad method, anywhere else in your code, use HoptoadNotifier.notify. Hoptoad will get all the information about the error itself. As for a hash, these are the keys you should pass:
98
+
99
+ * :error_class – Use this to group similar errors together. When Hoptoad catches an exception it sends the class name of that exception object.
100
+ * :error_message – This is the title of the error you see in the errors list. For exceptions it is "#{exception.class.name}: #{exception.message}"
101
+ * :request – While there are several ways to send additional data to Hoptoad, passing a Hash with :params key as :request as in the example above is the most common use case. When Hoptoad catches an exception in a controller, the actual HTTP client request is being sent using this key.
102
+
103
+ Hoptoad merges the hash you pass with these default options:
104
+
105
+ def default_notice_options
106
+ {
107
+ :api_key => HoptoadNotifier.api_key,
108
+ :error_message => 'Notification',
109
+ :backtrace => caller,
110
+ :request => {},
111
+ :session => {},
112
+ :environment => ENV.to_hash
113
+ }
114
+ end
115
+
116
+ You can override any of those parameters.
117
+
118
+ FILTERING
119
+
120
+ You can specify a whitelist of errors, that Hoptoad will not report on. Use
121
+ this feature when you are so apathetic to certain errors that you don't want
122
+ them even logged.
123
+
124
+ This filter will only be applied to automatic notifications, not manual
125
+ notifications (when #notify is called directly).
126
+
127
+ Hoptoad ignores the following exceptions by default:
128
+ ActiveRecord::RecordNotFound
129
+ ActionController::RoutingError
130
+ ActionController::InvalidAuthenticityToken
131
+ CGI::Session::CookieStore::TamperedWithCookie
132
+
133
+ To ignore errors in addition to those, specify their names in your Hoptoad
134
+ configuration block.
135
+
136
+ HoptoadNotifier.configure do |config|
137
+ config.api_key = '1234567890abcdef'
138
+ config.ignore << ActiveRecord::IgnoreThisError
139
+ end
140
+
141
+ To ignore *only* certain errors (and override the defaults), use the
142
+ #ignore_only attribute.
143
+
144
+ HoptoadNotifier.configure do |config|
145
+ config.api_key = '1234567890abcdef'
146
+ config.ignore_only = [ActiveRecord::IgnoreThisError]
147
+ end
148
+
149
+ TESTING
150
+
151
+ When you run your tests, you might notice that the hoptoad service is recording
152
+ notices generated using #notify when you don't expect it to. You can
153
+ use code like this in your test_helper.rb to redefine that method so those
154
+ errors are not reported while running tests.
155
+
156
+ module HoptoadNotifier::Catcher
157
+ def notify(thing)
158
+ # do nothing.
159
+ end
160
+ end
161
+
162
+ THANKS
163
+
164
+ Thanks to Eugene Bolshakov for the excellent write-up on GOING BEYOND EXCEPTIONS, which we have included above.
165
+
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the hoptoad_notifier plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the hoptoad_notifier plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'HoptoadNotifier'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,322 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'rubygems'
4
+ require 'active_support'
5
+
6
+ # Plugin for applications to automatically post errors to the Hoptoad of their choice.
7
+ module HoptoadNotifier
8
+
9
+ IGNORE_DEFAULT = ['ActiveRecord::RecordNotFound',
10
+ 'ActionController::RoutingError',
11
+ 'ActionController::InvalidAuthenticityToken',
12
+ 'CGI::Session::CookieStore::TamperedWithCookie']
13
+
14
+ # Some of these don't exist for Rails 1.2.*, so we have to consider that.
15
+ IGNORE_DEFAULT.map!{|e| eval(e) rescue nil }.compact!
16
+ IGNORE_DEFAULT.freeze
17
+
18
+ class << self
19
+ attr_accessor :host, :port, :secure, :api_key, :http_open_timeout, :http_read_timeout,
20
+ :proxy_host, :proxy_port, :proxy_user, :proxy_pass
21
+ attr_reader :backtrace_filters
22
+
23
+ # Takes a block and adds it to the list of backtrace filters. When the filters
24
+ # run, the block will be handed each line of the backtrace and can modify
25
+ # it as necessary. For example, by default a path matching the RAILS_ROOT
26
+ # constant will be transformed into "[RAILS_ROOT]"
27
+ def filter_backtrace &block
28
+ (@backtrace_filters ||= []) << block
29
+ end
30
+
31
+ # The port on which your Hoptoad server runs.
32
+ def port
33
+ @port || (secure ? 443 : 80)
34
+ end
35
+
36
+ # The host to connect to.
37
+ def host
38
+ @host ||= 'hoptoadapp.com'
39
+ end
40
+
41
+ # The HTTP open timeout (defaults to 2 seconds).
42
+ def http_open_timeout
43
+ @http_open_timeout ||= 2
44
+ end
45
+
46
+ # The HTTP read timeout (defaults to 5 seconds).
47
+ def http_read_timeout
48
+ @http_read_timeout ||= 5
49
+ end
50
+
51
+ # Returns the list of errors that are being ignored. The array can be appended to.
52
+ def ignore
53
+ @ignore ||= (HoptoadNotifier::IGNORE_DEFAULT.dup)
54
+ @ignore.flatten!
55
+ @ignore
56
+ end
57
+
58
+ # Sets the list of ignored errors to only what is passed in here. This method
59
+ # can be passed a single error or a list of errors.
60
+ def ignore_only=(names)
61
+ @ignore = [names].flatten
62
+ end
63
+
64
+ # Returns a list of parameters that should be filtered out of what is sent to Hoptoad.
65
+ # By default, all "password" attributes will have their contents replaced.
66
+ def params_filters
67
+ @params_filters ||= %w(password)
68
+ end
69
+
70
+ def environment_filters
71
+ @environment_filters ||= %w()
72
+ end
73
+
74
+ # Call this method to modify defaults in your initializers.
75
+ #
76
+ # HoptoadNotifier.configure do |config|
77
+ # config.api_key = '1234567890abcdef'
78
+ # config.secure = false
79
+ # end
80
+ #
81
+ # NOTE: secure connections are not yet supported.
82
+ def configure
83
+ yield self
84
+ end
85
+
86
+ def protocol #:nodoc:
87
+ secure ? "https" : "http"
88
+ end
89
+
90
+ def url #:nodoc:
91
+ URI.parse("#{protocol}://#{host}:#{port}/notices/")
92
+ end
93
+
94
+ def default_notice_options #:nodoc:
95
+ {
96
+ :api_key => HoptoadNotifier.api_key,
97
+ :error_message => 'Notification',
98
+ :backtrace => caller,
99
+ :request => {},
100
+ :session => {},
101
+ :environment => ENV.to_hash
102
+ }
103
+ end
104
+
105
+ # You can send an exception manually using this method, even when you are not in a
106
+ # controller. You can pass an exception or a hash that contains the attributes that
107
+ # would be sent to Hoptoad:
108
+ # * api_key: The API key for this project. The API key is a unique identifier that Hoptoad
109
+ # uses for identification.
110
+ # * error_message: The error returned by the exception (or the message you want to log).
111
+ # * backtrace: A backtrace, usually obtained with +caller+.
112
+ # * request: The controller's request object.
113
+ # * session: The contents of the user's session.
114
+ # * environment: ENV merged with the contents of the request's environment.
115
+ def notify notice = {}
116
+ Sender.new.notify_hoptoad( notice )
117
+ end
118
+ end
119
+
120
+ filter_backtrace do |line|
121
+ line.gsub(/#{RAILS_ROOT}/, "[RAILS_ROOT]")
122
+ end
123
+
124
+ filter_backtrace do |line|
125
+ line.gsub(/^\.\//, "")
126
+ end
127
+
128
+ filter_backtrace do |line|
129
+ if defined?(Gem)
130
+ Gem.path.inject(line) do |line, path|
131
+ line.gsub(/#{path}/, "[GEM_ROOT]")
132
+ end
133
+ end
134
+ end
135
+
136
+ # Include this module in Controllers in which you want to be notified of errors.
137
+ module Catcher
138
+
139
+ def self.included(base) #:nodoc:
140
+ if base.instance_methods.include? 'rescue_action_in_public' and !base.instance_methods.include? 'rescue_action_in_public_without_hoptoad'
141
+ base.send(:alias_method, :rescue_action_in_public_without_hoptoad, :rescue_action_in_public)
142
+ base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_hoptoad)
143
+ end
144
+ end
145
+
146
+ # Overrides the rescue_action method in ActionController::Base, but does not inhibit
147
+ # any custom processing that is defined with Rails 2's exception helpers.
148
+ def rescue_action_in_public_with_hoptoad exception
149
+ notify_hoptoad(exception) unless ignore?(exception)
150
+ rescue_action_in_public_without_hoptoad(exception)
151
+ end
152
+
153
+ # This method should be used for sending manual notifications while you are still
154
+ # inside the controller. Otherwise it works like HoptoadNotifier.notify.
155
+ def notify_hoptoad hash_or_exception
156
+ if public_environment?
157
+ notice = normalize_notice(hash_or_exception)
158
+ notice = clean_notice(notice)
159
+ send_to_hoptoad(:notice => notice)
160
+ end
161
+ end
162
+
163
+ alias_method :inform_hoptoad, :notify_hoptoad
164
+
165
+ # Returns the default logger or a logger that prints to STDOUT. Necessary for manual
166
+ # notifications outside of controllers.
167
+ def logger
168
+ ActiveRecord::Base.logger
169
+ rescue
170
+ @logger ||= Logger.new(STDERR)
171
+ end
172
+
173
+ private
174
+
175
+ def public_environment? #nodoc:
176
+ defined?(RAILS_ENV) and !['development', 'test'].include?(RAILS_ENV)
177
+ end
178
+
179
+ def ignore?(exception) #:nodoc:
180
+ ignore_these = HoptoadNotifier.ignore.flatten
181
+ ignore_these.include?(exception.class) || ignore_these.include?(exception.class.name)
182
+ end
183
+
184
+ def exception_to_data exception #:nodoc:
185
+ data = {
186
+ :api_key => HoptoadNotifier.api_key,
187
+ :error_class => exception.class.name,
188
+ :error_message => "#{exception.class.name}: #{exception.message}",
189
+ :backtrace => exception.backtrace,
190
+ :environment => ENV.to_hash
191
+ }
192
+
193
+ if self.respond_to? :request
194
+ data[:request] = {
195
+ :params => request.parameters.to_hash,
196
+ :rails_root => File.expand_path(RAILS_ROOT),
197
+ :url => "#{request.protocol}#{request.host}#{request.request_uri}"
198
+ }
199
+ data[:environment].merge!(request.env.to_hash)
200
+ end
201
+
202
+ if self.respond_to? :session
203
+ data[:session] = {
204
+ :key => session.instance_variable_get("@session_id"),
205
+ :data => session.instance_variable_get("@data")
206
+ }
207
+ end
208
+
209
+ data
210
+ end
211
+
212
+ def normalize_notice(notice) #:nodoc:
213
+ case notice
214
+ when Hash
215
+ HoptoadNotifier.default_notice_options.merge(notice)
216
+ when Exception
217
+ HoptoadNotifier.default_notice_options.merge(exception_to_data(notice))
218
+ end
219
+ end
220
+
221
+ def clean_notice(notice) #:nodoc:
222
+ notice[:backtrace] = clean_hoptoad_backtrace(notice[:backtrace])
223
+ if notice[:request].is_a?(Hash) && notice[:request][:params].is_a?(Hash)
224
+ notice[:request][:params] = filter_parameters(notice[:request][:params]) if respond_to?(:filter_parameters)
225
+ notice[:request][:params] = clean_hoptoad_params(notice[:request][:params])
226
+ end
227
+ if notice[:environment].is_a?(Hash)
228
+ notice[:environment] = filter_parameters(notice[:environment]) if respond_to?(:filter_parameters)
229
+ notice[:environment] = clean_hoptoad_environment(notice[:environment])
230
+ end
231
+ clean_non_serializable_data(notice)
232
+ end
233
+
234
+ def send_to_hoptoad data #:nodoc:
235
+ headers = {
236
+ 'Content-type' => 'application/x-yaml',
237
+ 'Accept' => 'text/xml, application/xml'
238
+ }
239
+
240
+ url = HoptoadNotifier.url
241
+
242
+ http = Net::HTTP::Proxy(HoptoadNotifier.proxy_host,
243
+ HoptoadNotifier.proxy_port,
244
+ HoptoadNotifier.proxy_user,
245
+ HoptoadNotifier.proxy_pass).new(url.host, url.port)
246
+
247
+ http.use_ssl = true
248
+ http.read_timeout = HoptoadNotifier.http_read_timeout
249
+ http.open_timeout = HoptoadNotifier.http_open_timeout
250
+ http.use_ssl = !!HoptoadNotifier.secure
251
+
252
+ response = begin
253
+ http.post(url.path, stringify_keys(data).to_yaml, headers)
254
+ rescue TimeoutError => e
255
+ logger.error "Timeout while contacting the Hoptoad server."
256
+ nil
257
+ end
258
+
259
+ case response
260
+ when Net::HTTPSuccess then
261
+ logger.info "Hoptoad Success: #{response.class}"
262
+ else
263
+ logger.error "Hoptoad Failure: #{response.class}\n#{response.body if response.respond_to? :body}"
264
+ end
265
+ end
266
+
267
+ def clean_hoptoad_backtrace backtrace #:nodoc:
268
+ if backtrace.to_a.size == 1
269
+ backtrace = backtrace.to_a.first.split(/\n\s*/)
270
+ end
271
+
272
+ backtrace.to_a.map do |line|
273
+ HoptoadNotifier.backtrace_filters.inject(line) do |line, proc|
274
+ proc.call(line)
275
+ end
276
+ end
277
+ end
278
+
279
+ def clean_hoptoad_params params #:nodoc:
280
+ params.each do |k, v|
281
+ params[k] = "[FILTERED]" if HoptoadNotifier.params_filters.any? do |filter|
282
+ k.to_s.match(/#{filter}/)
283
+ end
284
+ end
285
+ end
286
+
287
+ def clean_hoptoad_environment env #:nodoc:
288
+ env.each do |k, v|
289
+ env[k] = "[FILTERED]" if HoptoadNotifier.environment_filters.any? do |filter|
290
+ k.to_s.match(/#{filter}/)
291
+ end
292
+ end
293
+ end
294
+
295
+ def clean_non_serializable_data(notice) #:nodoc:
296
+ notice.select{|k,v| serializable?(v) }.inject({}) do |h, pair|
297
+ h[pair.first] = pair.last.is_a?(Hash) ? clean_non_serializable_data(pair.last) : pair.last
298
+ h
299
+ end
300
+ end
301
+
302
+ def serializable?(value) #:nodoc:
303
+ !(value.is_a?(Module) || value.kind_of?(IO))
304
+ end
305
+
306
+ def stringify_keys(hash) #:nodoc:
307
+ hash.inject({}) do |h, pair|
308
+ h[pair.first.to_s] = pair.last.is_a?(Hash) ? stringify_keys(pair.last) : pair.last
309
+ h
310
+ end
311
+ end
312
+
313
+ end
314
+
315
+ # A dummy class for sending notifications manually outside of a controller.
316
+ class Sender
317
+ def rescue_action_in_public(exception)
318
+ end
319
+
320
+ include HoptoadNotifier::Catcher
321
+ end
322
+ end
@@ -0,0 +1,55 @@
1
+ namespace :hoptoad do
2
+ desc "Verify your plugin installation by sending a test exception to the hoptoad service"
3
+ task :test => :environment do
4
+ require 'action_controller/test_process'
5
+ require 'application'
6
+
7
+ request = ActionController::TestRequest.new({
8
+ 'action' => 'verify',
9
+ 'controller' => 'hoptoad_verification',
10
+ '_method' => 'GET'
11
+ })
12
+
13
+ response = ActionController::TestResponse.new
14
+
15
+ class HoptoadTestingException < RuntimeError; end
16
+
17
+ unless ApplicationController.ancestors.include? HoptoadNotifier::Catcher
18
+ puts "You have not included HoptoadNotifier::Catcher in your ApplicationController!"
19
+ exit
20
+ end
21
+
22
+ puts 'Setting up the Controller.'
23
+ class ApplicationController
24
+ # This is to bypass any filters that may prevent access to the action.
25
+ prepend_before_filter :test_hoptoad
26
+ def test_hoptoad
27
+ puts "Raising '#{exception_class.name}' to simulate application failure."
28
+ raise exception_class.new, 'Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
29
+ end
30
+
31
+ def rescue_action exception
32
+ rescue_action_in_public exception
33
+ end
34
+
35
+ def public_environment?
36
+ true
37
+ end
38
+
39
+ # Ensure we actually have an action to go to.
40
+ def verify; end
41
+
42
+ def exception_class
43
+ exception_name = ENV['EXCEPTION'] || "HoptoadTestingException"
44
+ Object.const_get(exception_name)
45
+ rescue
46
+ Object.const_set(exception_name, Class.new(Exception))
47
+ end
48
+ end
49
+
50
+ puts 'Processing request.'
51
+ class HoptoadVerificationController < ApplicationController; end
52
+ HoptoadVerificationController.new.process(request, response)
53
+ end
54
+ end
55
+
@@ -0,0 +1,527 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'mocha'
4
+ gem 'thoughtbot-shoulda', ">= 2.0.0"
5
+ require 'shoulda'
6
+ require 'action_controller'
7
+ require 'action_controller/test_process'
8
+ require 'active_record'
9
+ require File.join(File.dirname(__FILE__), "..", "lib", "hoptoad_notifier")
10
+
11
+ RAILS_ROOT = File.join( File.dirname(__FILE__), "rails_root" )
12
+ RAILS_ENV = "test"
13
+
14
+ class HoptoadController < ActionController::Base
15
+ def rescue_action e
16
+ raise e
17
+ end
18
+
19
+ def do_raise
20
+ raise "Hoptoad"
21
+ end
22
+
23
+ def do_not_raise
24
+ render :text => "Success"
25
+ end
26
+
27
+ def do_raise_ignored
28
+ raise ActiveRecord::RecordNotFound.new("404")
29
+ end
30
+
31
+ def do_raise_not_ignored
32
+ raise ActiveRecord::StatementInvalid.new("Statement invalid")
33
+ end
34
+
35
+ def manual_notify
36
+ notify_hoptoad(Exception.new)
37
+ render :text => "Success"
38
+ end
39
+
40
+ def manual_notify_ignored
41
+ notify_hoptoad(ActiveRecord::RecordNotFound.new("404"))
42
+ render :text => "Success"
43
+ end
44
+ end
45
+
46
+ class HoptoadNotifierTest < Test::Unit::TestCase
47
+ def request(action = nil, method = :get)
48
+ @request = ActionController::TestRequest.new({
49
+ "controller" => "hoptoad",
50
+ "action" => action ? action.to_s : "",
51
+ "_method" => method.to_s
52
+ })
53
+ @response = ActionController::TestResponse.new
54
+ @controller.process(@request, @response)
55
+ end
56
+
57
+ context "Hoptoad inclusion" do
58
+ should "be able to occur even outside Rails controllers" do
59
+ assert_nothing_raised do
60
+ class MyHoptoad
61
+ include HoptoadNotifier::Catcher
62
+ end
63
+ end
64
+ my = MyHoptoad.new
65
+ assert my.respond_to?(:notify_hoptoad)
66
+ end
67
+ end
68
+
69
+ context "HoptoadNotifier configuration" do
70
+ setup do
71
+ @controller = HoptoadController.new
72
+ class ::HoptoadController
73
+ include HoptoadNotifier::Catcher
74
+ def rescue_action e
75
+ rescue_action_in_public e
76
+ end
77
+ end
78
+ assert @controller.methods.include?("notify_hoptoad")
79
+ end
80
+
81
+ should "be done with a block" do
82
+ HoptoadNotifier.configure do |config|
83
+ config.host = "host"
84
+ config.port = 3333
85
+ config.secure = true
86
+ config.api_key = "1234567890abcdef"
87
+ config.ignore << [ RuntimeError ]
88
+ config.proxy_host = 'proxyhost1'
89
+ config.proxy_port = '80'
90
+ config.proxy_user = 'user'
91
+ config.proxy_pass = 'secret'
92
+ config.http_open_timeout = 2
93
+ config.http_read_timeout = 5
94
+ end
95
+
96
+ assert_equal "host", HoptoadNotifier.host
97
+ assert_equal 3333, HoptoadNotifier.port
98
+ assert_equal true, HoptoadNotifier.secure
99
+ assert_equal "1234567890abcdef", HoptoadNotifier.api_key
100
+ assert_equal 'proxyhost1', HoptoadNotifier.proxy_host
101
+ assert_equal '80', HoptoadNotifier.proxy_port
102
+ assert_equal 'user', HoptoadNotifier.proxy_user
103
+ assert_equal 'secret', HoptoadNotifier.proxy_pass
104
+ assert_equal 2, HoptoadNotifier.http_open_timeout
105
+ assert_equal 5, HoptoadNotifier.http_read_timeout
106
+ assert_equal (HoptoadNotifier::IGNORE_DEFAULT + [RuntimeError]), HoptoadNotifier.ignore
107
+ end
108
+
109
+ should "set a default host" do
110
+ HoptoadNotifier.instance_variable_set("@host",nil)
111
+ assert_equal "hoptoadapp.com", HoptoadNotifier.host
112
+ end
113
+
114
+ should "add filters to the backtrace_filters" do
115
+ assert_difference "HoptoadNotifier.backtrace_filters.length" do
116
+ HoptoadNotifier.configure do |config|
117
+ config.filter_backtrace do |line|
118
+ line = "1234"
119
+ end
120
+ end
121
+ end
122
+
123
+ assert_equal %w( 1234 1234 ), @controller.send(:clean_hoptoad_backtrace, %w( foo bar ))
124
+ end
125
+
126
+ should "use standard rails logging filters on params and env" do
127
+ ::HoptoadController.class_eval do
128
+ filter_parameter_logging :ghi
129
+ end
130
+
131
+ expected = {"notice" => {"request" => {"params" => {"abc" => "123", "def" => "456", "ghi" => "[FILTERED]"}},
132
+ "environment" => {"abc" => "123", "ghi" => "[FILTERED]"}}}
133
+ notice = {"notice" => {"request" => {"params" => {"abc" => "123", "def" => "456", "ghi" => "789"}},
134
+ "environment" => {"abc" => "123", "ghi" => "789"}}}
135
+ assert @controller.respond_to?(:filter_parameters)
136
+ assert_equal( expected[:notice], @controller.send(:clean_notice, notice)[:notice] )
137
+ end
138
+
139
+ should "add filters to the params filters" do
140
+ assert_difference "HoptoadNotifier.params_filters.length", 2 do
141
+ HoptoadNotifier.configure do |config|
142
+ config.params_filters << "abc"
143
+ config.params_filters << "def"
144
+ end
145
+ end
146
+
147
+ assert HoptoadNotifier.params_filters.include?( "abc" )
148
+ assert HoptoadNotifier.params_filters.include?( "def" )
149
+
150
+ assert_equal( {:abc => "[FILTERED]", :def => "[FILTERED]", :ghi => "789"},
151
+ @controller.send(:clean_hoptoad_params, :abc => "123", :def => "456", :ghi => "789" ) )
152
+ end
153
+
154
+ should "add filters to the environment filters" do
155
+ assert_difference "HoptoadNotifier.environment_filters.length", 2 do
156
+ HoptoadNotifier.configure do |config|
157
+ config.environment_filters << "secret"
158
+ config.environment_filters << "supersecret"
159
+ end
160
+ end
161
+
162
+ assert HoptoadNotifier.environment_filters.include?( "secret" )
163
+ assert HoptoadNotifier.environment_filters.include?( "supersecret" )
164
+
165
+ assert_equal( {:secret => "[FILTERED]", :supersecret => "[FILTERED]", :ghi => "789"},
166
+ @controller.send(:clean_hoptoad_environment, :secret => "123", :supersecret => "456", :ghi => "789" ) )
167
+ end
168
+
169
+ should "have at default ignored exceptions" do
170
+ assert HoptoadNotifier::IGNORE_DEFAULT.any?
171
+ end
172
+ end
173
+
174
+ context "The hoptoad test controller" do
175
+ setup do
176
+ @controller = ::HoptoadController.new
177
+ class ::HoptoadController
178
+ def rescue_action e
179
+ raise e
180
+ end
181
+ end
182
+ end
183
+
184
+ context "with no notifier catcher" do
185
+ should "not prevent raises" do
186
+ assert_raises RuntimeError do
187
+ request("do_raise")
188
+ end
189
+ end
190
+
191
+ should "allow a non-raising action to complete" do
192
+ assert_nothing_raised do
193
+ request("do_not_raise")
194
+ end
195
+ end
196
+ end
197
+
198
+ context "with the notifier installed" do
199
+ setup do
200
+ class ::HoptoadController
201
+ include HoptoadNotifier::Catcher
202
+ def rescue_action e
203
+ rescue_action_in_public e
204
+ end
205
+ end
206
+ HoptoadNotifier.ignore_only = HoptoadNotifier::IGNORE_DEFAULT
207
+ @controller.stubs(:public_environment?).returns(true)
208
+ @controller.stubs(:send_to_hoptoad)
209
+ end
210
+
211
+ should "have inserted its methods into the controller" do
212
+ assert @controller.methods.include?("inform_hoptoad")
213
+ end
214
+
215
+ should "prevent raises and send the error to hoptoad" do
216
+ @controller.expects(:notify_hoptoad)
217
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
218
+ assert_nothing_raised do
219
+ request("do_raise")
220
+ end
221
+ end
222
+
223
+ should "allow a non-raising action to complete" do
224
+ assert_nothing_raised do
225
+ request("do_not_raise")
226
+ end
227
+ end
228
+
229
+ should "allow manual sending of exceptions" do
230
+ @controller.expects(:notify_hoptoad)
231
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
232
+ assert_nothing_raised do
233
+ request("manual_notify")
234
+ end
235
+ end
236
+
237
+ should "disable manual sending of exceptions in a non-public (development or test) environment" do
238
+ @controller.stubs(:public_environment?).returns(false)
239
+ @controller.expects(:send_to_hoptoad).never
240
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
241
+ assert_nothing_raised do
242
+ request("manual_notify")
243
+ end
244
+ end
245
+
246
+ should "send even ignored exceptions if told manually" do
247
+ @controller.expects(:notify_hoptoad)
248
+ @controller.expects(:rescue_action_in_public_without_hoptoad).never
249
+ assert_nothing_raised do
250
+ request("manual_notify_ignored")
251
+ end
252
+ end
253
+
254
+ should "ignore default exceptions" do
255
+ @controller.expects(:notify_hoptoad).never
256
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
257
+ assert_nothing_raised do
258
+ request("do_raise_ignored")
259
+ end
260
+ end
261
+
262
+ should "filter non-serializable data" do
263
+ File.open(__FILE__) do |file|
264
+ assert_equal( {:ghi => "789"},
265
+ @controller.send(:clean_non_serializable_data, :ghi => "789", :class => Class.new, :file => file) )
266
+ end
267
+ end
268
+
269
+ should "apply all params, environment and technical filters" do
270
+ params_hash = {:abc => 123}
271
+ environment_hash = {:def => 456}
272
+ backtrace_data = :backtrace_data
273
+
274
+ raw_notice = {:request => {:params => params_hash},
275
+ :environment => environment_hash,
276
+ :backtrace => backtrace_data}
277
+
278
+ processed_notice = {:backtrace => :backtrace_data,
279
+ :request => {:params => :params_data},
280
+ :environment => :environment_data}
281
+
282
+ @controller.expects(:clean_hoptoad_backtrace).with(backtrace_data).returns(:backtrace_data)
283
+ @controller.expects(:clean_hoptoad_params).with(params_hash).returns(:params_data)
284
+ @controller.expects(:clean_hoptoad_environment).with(environment_hash).returns(:environment_data)
285
+ @controller.expects(:clean_non_serializable_data).with(processed_notice).returns(:serializable_data)
286
+
287
+ assert_equal(:serializable_data, @controller.send(:clean_notice, raw_notice))
288
+ end
289
+
290
+ context "and configured to ignore additional exceptions" do
291
+ setup do
292
+ HoptoadNotifier.ignore << ActiveRecord::StatementInvalid
293
+ end
294
+
295
+ should "still ignore default exceptions" do
296
+ @controller.expects(:notify_hoptoad).never
297
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
298
+ assert_nothing_raised do
299
+ request("do_raise_ignored")
300
+ end
301
+ end
302
+
303
+ should "ignore specified exceptions" do
304
+ @controller.expects(:notify_hoptoad).never
305
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
306
+ assert_nothing_raised do
307
+ request("do_raise_not_ignored")
308
+ end
309
+ end
310
+
311
+ should "not ignore unspecified, non-default exceptions" do
312
+ @controller.expects(:notify_hoptoad)
313
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
314
+ assert_nothing_raised do
315
+ request("do_raise")
316
+ end
317
+ end
318
+ end
319
+
320
+ context "and configured to ignore only certain exceptions" do
321
+ setup do
322
+ HoptoadNotifier.ignore_only = [ActiveRecord::StatementInvalid]
323
+ end
324
+
325
+ should "no longer ignore default exceptions" do
326
+ @controller.expects(:notify_hoptoad)
327
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
328
+ assert_nothing_raised do
329
+ request("do_raise_ignored")
330
+ end
331
+ end
332
+
333
+ should "ignore specified exceptions" do
334
+ @controller.expects(:notify_hoptoad).never
335
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
336
+ assert_nothing_raised do
337
+ request("do_raise_not_ignored")
338
+ end
339
+ end
340
+
341
+ should "not ignore unspecified, non-default exceptions" do
342
+ @controller.expects(:notify_hoptoad)
343
+ @controller.expects(:rescue_action_in_public_without_hoptoad)
344
+ assert_nothing_raised do
345
+ request("do_raise")
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ context "Sending a notice" do
353
+ context "with an exception" do
354
+ setup do
355
+ @sender = HoptoadNotifier::Sender.new
356
+ @backtrace = caller
357
+ @exception = begin
358
+ raise
359
+ rescue => caught_exception
360
+ caught_exception
361
+ end
362
+ @options = {:error_message => "123",
363
+ :backtrace => @backtrace}
364
+ HoptoadNotifier.instance_variable_set("@backtrace_filters", [])
365
+ HoptoadNotifier::Sender.expects(:new).returns(@sender)
366
+ @sender.stubs(:public_environment?).returns(true)
367
+ end
368
+
369
+ context "when using an HTTP Proxy" do
370
+ setup do
371
+ @body = 'body'
372
+ @response = stub(:body => @body)
373
+ @http = stub(:post => @response, :read_timeout= => nil, :open_timeout= => nil, :use_ssl= => nil)
374
+ @sender.stubs(:logger).returns(stub(:error => nil, :info => nil))
375
+ @proxy = stub
376
+ @proxy.stubs(:new).returns(@http)
377
+
378
+ HoptoadNotifier.port = nil
379
+ HoptoadNotifier.host = nil
380
+ HoptoadNotifier.secure = false
381
+
382
+ Net::HTTP.expects(:Proxy).with(
383
+ HoptoadNotifier.proxy_host,
384
+ HoptoadNotifier.proxy_port,
385
+ HoptoadNotifier.proxy_user,
386
+ HoptoadNotifier.proxy_pass
387
+ ).returns(@proxy)
388
+ end
389
+
390
+ context "on notify" do
391
+ setup { HoptoadNotifier.notify(@exception) }
392
+
393
+ before_should "post to Hoptoad" do
394
+ url = "http://hoptoadapp.com:80/notices/"
395
+ uri = URI.parse(url)
396
+ URI.expects(:parse).with(url).returns(uri)
397
+ @http.expects(:post).with(uri.path, anything, anything).returns(@response)
398
+ end
399
+ end
400
+ end
401
+
402
+ context "when stubbing out Net::HTTP" do
403
+ setup do
404
+ @body = 'body'
405
+ @response = stub(:body => @body)
406
+ @http = stub(:post => @response, :read_timeout= => nil, :open_timeout= => nil, :use_ssl= => nil)
407
+ @sender.stubs(:logger).returns(stub(:error => nil, :info => nil))
408
+ Net::HTTP.stubs(:new).returns(@http)
409
+ HoptoadNotifier.port = nil
410
+ HoptoadNotifier.host = nil
411
+ HoptoadNotifier.proxy_host = nil
412
+ end
413
+
414
+ context "on notify" do
415
+ setup { HoptoadNotifier.notify(@exception) }
416
+
417
+ before_should "post to the right url for non-ssl" do
418
+ HoptoadNotifier.secure = false
419
+ url = "http://hoptoadapp.com:80/notices/"
420
+ uri = URI.parse(url)
421
+ URI.expects(:parse).with(url).returns(uri)
422
+ @http.expects(:post).with(uri.path, anything, anything).returns(@response)
423
+ end
424
+
425
+ before_should "post to the right path" do
426
+ @http.expects(:post).with("/notices/", anything, anything).returns(@response)
427
+ end
428
+
429
+ before_should "call send_to_hoptoad" do
430
+ @sender.expects(:send_to_hoptoad)
431
+ end
432
+
433
+ before_should "default the open timeout to 2 seconds" do
434
+ HoptoadNotifier.http_open_timeout = nil
435
+ @http.expects(:open_timeout=).with(2)
436
+ end
437
+
438
+ before_should "default the read timeout to 5 seconds" do
439
+ HoptoadNotifier.http_read_timeout = nil
440
+ @http.expects(:read_timeout=).with(5)
441
+ end
442
+
443
+ before_should "allow override of the open timeout" do
444
+ HoptoadNotifier.http_open_timeout = 4
445
+ @http.expects(:open_timeout=).with(4)
446
+ end
447
+
448
+ before_should "allow override of the read timeout" do
449
+ HoptoadNotifier.http_read_timeout = 10
450
+ @http.expects(:read_timeout=).with(10)
451
+ end
452
+
453
+ before_should "connect to the right port for ssl" do
454
+ HoptoadNotifier.secure = true
455
+ Net::HTTP.expects(:new).with("hoptoadapp.com", 443).returns(@http)
456
+ end
457
+
458
+ before_should "connect to the right port for non-ssl" do
459
+ HoptoadNotifier.secure = false
460
+ Net::HTTP.expects(:new).with("hoptoadapp.com", 80).returns(@http)
461
+ end
462
+
463
+ before_should "use ssl if secure" do
464
+ HoptoadNotifier.secure = true
465
+ HoptoadNotifier.host = 'example.org'
466
+ Net::HTTP.expects(:new).with('example.org', 443).returns(@http)
467
+ end
468
+
469
+ before_should "not use ssl if not secure" do
470
+ HoptoadNotifier.secure = nil
471
+ HoptoadNotifier.host = 'example.org'
472
+ Net::HTTP.expects(:new).with('example.org', 80).returns(@http)
473
+ end
474
+ end
475
+ end
476
+
477
+ should "send as if it were a normally caught exception" do
478
+ @sender.expects(:notify_hoptoad).with(@exception)
479
+ HoptoadNotifier.notify(@exception)
480
+ end
481
+
482
+ should "make sure the exception is munged into a hash" do
483
+ options = HoptoadNotifier.default_notice_options.merge({
484
+ :backtrace => @exception.backtrace,
485
+ :environment => ENV.to_hash,
486
+ :error_class => @exception.class.name,
487
+ :error_message => "#{@exception.class.name}: #{@exception.message}",
488
+ :api_key => HoptoadNotifier.api_key,
489
+ })
490
+ @sender.expects(:send_to_hoptoad).with(:notice => options)
491
+ HoptoadNotifier.notify(@exception)
492
+ end
493
+
494
+ should "parse massive one-line exceptions into multiple lines" do
495
+ @original_backtrace = "one big line\n separated\n by new lines\nand some spaces"
496
+ @expected_backtrace = ["one big line", "separated", "by new lines", "and some spaces"]
497
+ @exception.set_backtrace [@original_backtrace]
498
+
499
+ options = HoptoadNotifier.default_notice_options.merge({
500
+ :backtrace => @expected_backtrace,
501
+ :environment => ENV.to_hash,
502
+ :error_class => @exception.class.name,
503
+ :error_message => "#{@exception.class.name}: #{@exception.message}",
504
+ :api_key => HoptoadNotifier.api_key,
505
+ })
506
+
507
+ @sender.expects(:send_to_hoptoad).with(:notice => options)
508
+ HoptoadNotifier.notify(@exception)
509
+ end
510
+ end
511
+
512
+ context "without an exception" do
513
+ setup do
514
+ @sender = HoptoadNotifier::Sender.new
515
+ @backtrace = caller
516
+ @options = {:error_message => "123",
517
+ :backtrace => @backtrace}
518
+ HoptoadNotifier::Sender.expects(:new).returns(@sender)
519
+ end
520
+
521
+ should "send sensible defaults" do
522
+ @sender.expects(:notify_hoptoad).with(@options)
523
+ HoptoadNotifier.notify(:error_message => "123", :backtrace => @backtrace)
524
+ end
525
+ end
526
+ end
527
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thoughtbot-hoptoad_notifier
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.1"
5
+ platform: ruby
6
+ authors:
7
+ - Thoughtbot
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-31 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Rails plugin that reports exceptions to Hoptoad.
17
+ email: info@thoughtbot.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - INSTALL
26
+ - lib/hoptoad_notifier.rb
27
+ - Rakefile
28
+ - README
29
+ - tasks/hoptoad_notifier_tasks.rake
30
+ has_rdoc: true
31
+ homepage: http://github.com/thoughtbot/hoptoad_notifier
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.2.0
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: Rails plugin that reports exceptions to Hoptoad.
56
+ test_files:
57
+ - test/hoptoad_notifier_test.rb