jordan-brough-hoptoad_notifier 2.3.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 (45) hide show
  1. data/CHANGELOG +161 -0
  2. data/INSTALL +25 -0
  3. data/MIT-LICENSE +22 -0
  4. data/README.rdoc +384 -0
  5. data/Rakefile +217 -0
  6. data/SUPPORTED_RAILS_VERSIONS +9 -0
  7. data/TESTING.rdoc +8 -0
  8. data/generators/hoptoad/hoptoad_generator.rb +63 -0
  9. data/generators/hoptoad/lib/insert_commands.rb +34 -0
  10. data/generators/hoptoad/lib/rake_commands.rb +24 -0
  11. data/generators/hoptoad/templates/capistrano_hook.rb +6 -0
  12. data/generators/hoptoad/templates/hoptoad_notifier_tasks.rake +25 -0
  13. data/generators/hoptoad/templates/initializer.rb +6 -0
  14. data/lib/hoptoad_notifier.rb +148 -0
  15. data/lib/hoptoad_notifier/backtrace.rb +99 -0
  16. data/lib/hoptoad_notifier/capistrano.rb +20 -0
  17. data/lib/hoptoad_notifier/configuration.rb +232 -0
  18. data/lib/hoptoad_notifier/notice.rb +318 -0
  19. data/lib/hoptoad_notifier/rack.rb +40 -0
  20. data/lib/hoptoad_notifier/rails.rb +37 -0
  21. data/lib/hoptoad_notifier/rails/action_controller_catcher.rb +29 -0
  22. data/lib/hoptoad_notifier/rails/controller_methods.rb +63 -0
  23. data/lib/hoptoad_notifier/rails/error_lookup.rb +33 -0
  24. data/lib/hoptoad_notifier/rails3_tasks.rb +90 -0
  25. data/lib/hoptoad_notifier/railtie.rb +23 -0
  26. data/lib/hoptoad_notifier/sender.rb +63 -0
  27. data/lib/hoptoad_notifier/tasks.rb +97 -0
  28. data/lib/hoptoad_notifier/version.rb +3 -0
  29. data/lib/hoptoad_tasks.rb +44 -0
  30. data/lib/rails/generators/hoptoad/hoptoad_generator.rb +69 -0
  31. data/lib/templates/rescue.erb +91 -0
  32. data/rails/init.rb +1 -0
  33. data/script/integration_test.rb +38 -0
  34. data/test/backtrace_test.rb +118 -0
  35. data/test/catcher_test.rb +324 -0
  36. data/test/configuration_test.rb +208 -0
  37. data/test/helper.rb +239 -0
  38. data/test/hoptoad_tasks_test.rb +152 -0
  39. data/test/logger_test.rb +85 -0
  40. data/test/notice_test.rb +443 -0
  41. data/test/notifier_test.rb +222 -0
  42. data/test/rack_test.rb +58 -0
  43. data/test/rails_initializer_test.rb +36 -0
  44. data/test/sender_test.rb +123 -0
  45. metadata +205 -0
@@ -0,0 +1,85 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class LoggerTest < Test::Unit::TestCase
4
+ def stub_http(response, body = nil)
5
+ response.stubs(:body => body) if body
6
+ @http = stub(:post => response,
7
+ :read_timeout= => nil,
8
+ :open_timeout= => nil,
9
+ :use_ssl= => nil)
10
+ Net::HTTP.stubs(:new).returns(@http)
11
+ end
12
+
13
+ def send_notice
14
+ HoptoadNotifier.sender.send_to_hoptoad('data')
15
+ end
16
+
17
+ def stub_verbose_log
18
+ HoptoadNotifier.stubs(:write_verbose_log)
19
+ end
20
+
21
+ def assert_logged(expected)
22
+ assert_received(HoptoadNotifier, :write_verbose_log) do |expect|
23
+ expect.with {|actual| actual =~ expected }
24
+ end
25
+ end
26
+
27
+ def assert_not_logged(expected)
28
+ assert_received(HoptoadNotifier, :write_verbose_log) do |expect|
29
+ expect.with {|actual| actual =~ expected }.never
30
+ end
31
+ end
32
+
33
+ def configure
34
+ HoptoadNotifier.configure { |config| }
35
+ end
36
+
37
+ should "report that notifier is ready when configured" do
38
+ stub_verbose_log
39
+ configure
40
+ assert_logged /Notifier (.*) ready/
41
+ end
42
+
43
+ should "not report that notifier is ready when internally configured" do
44
+ stub_verbose_log
45
+ HoptoadNotifier.configure(true) { |config| }
46
+ assert_not_logged /.*/
47
+ end
48
+
49
+ should "print environment info a successful notification without a body" do
50
+ reset_config
51
+ stub_verbose_log
52
+ stub_http(Net::HTTPSuccess)
53
+ send_notice
54
+ assert_logged /Environment Info:/
55
+ assert_not_logged /Response from Hoptoad:/
56
+ end
57
+
58
+ should "print environment info on a failed notification without a body" do
59
+ reset_config
60
+ stub_verbose_log
61
+ stub_http(Net::HTTPError)
62
+ send_notice
63
+ assert_logged /Environment Info:/
64
+ assert_not_logged /Response from Hoptoad:/
65
+ end
66
+
67
+ should "print environment info and response on a success with a body" do
68
+ reset_config
69
+ stub_verbose_log
70
+ stub_http(Net::HTTPSuccess, 'test')
71
+ send_notice
72
+ assert_logged /Environment Info:/
73
+ assert_logged /Response from Hoptoad:/
74
+ end
75
+
76
+ should "print environment info and response on a failure with a body" do
77
+ reset_config
78
+ stub_verbose_log
79
+ stub_http(Net::HTTPError, 'test')
80
+ send_notice
81
+ assert_logged /Environment Info:/
82
+ assert_logged /Response from Hoptoad:/
83
+ end
84
+
85
+ end
@@ -0,0 +1,443 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class NoticeTest < Test::Unit::TestCase
4
+
5
+ include DefinesConstants
6
+
7
+ def configure
8
+ returning HoptoadNotifier::Configuration.new do |config|
9
+ config.api_key = 'abc123def456'
10
+ end
11
+ end
12
+
13
+ def build_notice(args = {})
14
+ configuration = args.delete(:configuration) || configure
15
+ HoptoadNotifier::Notice.new(configuration.merge(args))
16
+ end
17
+
18
+ def stub_request(attrs = {})
19
+ stub('request', { :parameters => { 'one' => 'two' },
20
+ :protocol => 'http',
21
+ :host => 'some.host',
22
+ :request_uri => '/some/uri',
23
+ :session => { :to_hash => { 'a' => 'b' } },
24
+ :env => { 'three' => 'four' } }.update(attrs))
25
+ end
26
+
27
+ should "set the api key" do
28
+ api_key = 'key'
29
+ notice = build_notice(:api_key => api_key)
30
+ assert_equal api_key, notice.api_key
31
+ end
32
+
33
+ should "accept a project root" do
34
+ project_root = '/path/to/project'
35
+ notice = build_notice(:project_root => project_root)
36
+ assert_equal project_root, notice.project_root
37
+ end
38
+
39
+ should "accept a component" do
40
+ assert_equal 'users_controller', build_notice(:component => 'users_controller').controller
41
+ end
42
+
43
+ should "alias the component as controller" do
44
+ assert_equal 'users_controller', build_notice(:controller => 'users_controller').component
45
+ assert_equal 'users_controller', build_notice(:component => 'users_controller').controller
46
+ end
47
+
48
+ should "accept a action" do
49
+ assert_equal 'index', build_notice(:action => 'index').action
50
+ end
51
+
52
+ should "accept a url" do
53
+ url = 'http://some.host/uri'
54
+ notice = build_notice(:url => url)
55
+ assert_equal url, notice.url
56
+ end
57
+
58
+ should "accept a backtrace from an exception or hash" do
59
+ array = ["user.rb:34:in `crazy'"]
60
+ exception = build_exception
61
+ exception.set_backtrace array
62
+ backtrace = HoptoadNotifier::Backtrace.parse(array)
63
+ notice_from_exception = build_notice(:exception => exception)
64
+
65
+
66
+ assert_equal backtrace,
67
+ notice_from_exception.backtrace,
68
+ "backtrace was not correctly set from an exception"
69
+
70
+ notice_from_hash = build_notice(:backtrace => array)
71
+ assert_equal backtrace,
72
+ notice_from_hash.backtrace,
73
+ "backtrace was not correctly set from a hash"
74
+ end
75
+
76
+ should "pass its backtrace filters for parsing" do
77
+ backtrace_array = ['my/file/backtrace:3']
78
+ exception = build_exception
79
+ exception.set_backtrace(backtrace_array)
80
+ HoptoadNotifier::Backtrace.expects(:parse).with(backtrace_array, {:filters => 'foo'})
81
+
82
+ notice = HoptoadNotifier::Notice.new({:exception => exception, :backtrace_filters => 'foo'})
83
+ end
84
+
85
+ should "set the error class from an exception or hash" do
86
+ assert_accepts_exception_attribute :error_class do |exception|
87
+ exception.class.name
88
+ end
89
+ end
90
+
91
+ should "set the error message from an exception or hash" do
92
+ assert_accepts_exception_attribute :error_message do |exception|
93
+ "#{exception.class.name}: #{exception.message}"
94
+ end
95
+ end
96
+
97
+ should "accept parameters from a request or hash" do
98
+ parameters = { 'one' => 'two' }
99
+ notice_from_hash = build_notice(:parameters => parameters)
100
+ assert_equal notice_from_hash.parameters, parameters
101
+ end
102
+
103
+ should "accept session data from a session[:data] hash" do
104
+ data = { 'one' => 'two' }
105
+ notice = build_notice(:session => { :data => data })
106
+ assert_equal data, notice.session_data
107
+ end
108
+
109
+ should "accept session data from a session_data hash" do
110
+ data = { 'one' => 'two' }
111
+ notice = build_notice(:session_data => data)
112
+ assert_equal data, notice.session_data
113
+ end
114
+
115
+ should "accept an environment name" do
116
+ assert_equal 'development', build_notice(:environment_name => 'development').environment_name
117
+ end
118
+
119
+ should "accept CGI data from a hash" do
120
+ data = { 'string' => 'value' }
121
+ notice = build_notice(:cgi_data => data)
122
+ assert_equal data, notice.cgi_data, "should take CGI data from a hash"
123
+ end
124
+
125
+ should "accept notifier information" do
126
+ params = { :notifier_name => 'a name for a notifier',
127
+ :notifier_version => '1.0.5',
128
+ :notifier_url => 'http://notifiers.r.us/download' }
129
+ notice = build_notice(params)
130
+ assert_equal params[:notifier_name], notice.notifier_name
131
+ assert_equal params[:notifier_version], notice.notifier_version
132
+ assert_equal params[:notifier_url], notice.notifier_url
133
+ end
134
+
135
+ should "set sensible defaults without an exception" do
136
+ backtrace = HoptoadNotifier::Backtrace.parse(build_backtrace_array)
137
+ notice = build_notice(:backtrace => build_backtrace_array)
138
+
139
+ assert_equal 'Notification', notice.error_message
140
+ assert_array_starts_with backtrace.lines, notice.backtrace.lines
141
+ assert_equal({}, notice.parameters)
142
+ assert_equal({}, notice.session_data)
143
+ end
144
+
145
+ should "use the caller as the backtrace for an exception without a backtrace" do
146
+ filters = HoptoadNotifier::Configuration.new.backtrace_filters
147
+ backtrace = HoptoadNotifier::Backtrace.parse(caller, :filters => filters)
148
+ notice = build_notice(:exception => StandardError.new('error'), :backtrace => nil)
149
+
150
+ assert_array_starts_with backtrace.lines, notice.backtrace.lines
151
+ end
152
+
153
+ should "convert unserializable objects to strings" do
154
+ assert_serializes_hash(:parameters)
155
+ assert_serializes_hash(:cgi_data)
156
+ assert_serializes_hash(:session_data)
157
+ end
158
+
159
+ should "filter parameters" do
160
+ assert_filters_hash(:parameters)
161
+ end
162
+
163
+ should "filter cgi data" do
164
+ assert_filters_hash(:cgi_data)
165
+ end
166
+
167
+ context "a Notice turned into XML" do
168
+ setup do
169
+ HoptoadNotifier.configure do |config|
170
+ config.api_key = "1234567890"
171
+ end
172
+
173
+ @exception = build_exception
174
+
175
+ @notice = build_notice({
176
+ :notifier_name => 'a name',
177
+ :notifier_version => '1.2.3',
178
+ :notifier_url => 'http://some.url/path',
179
+ :exception => @exception,
180
+ :controller => "controller",
181
+ :action => "action",
182
+ :url => "http://url.com",
183
+ :parameters => { "paramskey" => "paramsvalue",
184
+ "nestparentkey" => { "nestkey" => "nestvalue" } },
185
+ :session_data => { "sessionkey" => "sessionvalue" },
186
+ :cgi_data => { "cgikey" => "cgivalue" },
187
+ :project_root => "RAILS_ROOT",
188
+ :environment_name => "RAILS_ENV"
189
+ })
190
+
191
+ @xml = @notice.to_xml
192
+
193
+ @document = Nokogiri::XML::Document.parse(@xml)
194
+ end
195
+
196
+ should "validate against the XML schema" do
197
+ assert_valid_notice_document @document
198
+ end
199
+
200
+ should "serialize a Notice to XML when sent #to_xml" do
201
+ assert_valid_node(@document, "//api-key", @notice.api_key)
202
+
203
+ assert_valid_node(@document, "//notifier/name", @notice.notifier_name)
204
+ assert_valid_node(@document, "//notifier/version", @notice.notifier_version)
205
+ assert_valid_node(@document, "//notifier/url", @notice.notifier_url)
206
+
207
+ assert_valid_node(@document, "//error/class", @notice.error_class)
208
+ assert_valid_node(@document, "//error/message", @notice.error_message)
209
+
210
+ assert_valid_node(@document, "//error/backtrace/line/@number", @notice.backtrace.lines.first.number)
211
+ assert_valid_node(@document, "//error/backtrace/line/@file", @notice.backtrace.lines.first.file)
212
+ assert_valid_node(@document, "//error/backtrace/line/@method", @notice.backtrace.lines.first.method)
213
+
214
+ assert_valid_node(@document, "//request/url", @notice.url)
215
+ assert_valid_node(@document, "//request/component", @notice.controller)
216
+ assert_valid_node(@document, "//request/action", @notice.action)
217
+
218
+ assert_valid_node(@document, "//request/params/var/@key", "paramskey")
219
+ assert_valid_node(@document, "//request/params/var", "paramsvalue")
220
+ assert_valid_node(@document, "//request/params/var/@key", "nestparentkey")
221
+ assert_valid_node(@document, "//request/params/var/var/@key", "nestkey")
222
+ assert_valid_node(@document, "//request/params/var/var", "nestvalue")
223
+ assert_valid_node(@document, "//request/session/var/@key", "sessionkey")
224
+ assert_valid_node(@document, "//request/session/var", "sessionvalue")
225
+ assert_valid_node(@document, "//request/cgi-data/var/@key", "cgikey")
226
+ assert_valid_node(@document, "//request/cgi-data/var", "cgivalue")
227
+
228
+ assert_valid_node(@document, "//server-environment/project-root", "RAILS_ROOT")
229
+ assert_valid_node(@document, "//server-environment/environment-name", "RAILS_ENV")
230
+ end
231
+ end
232
+
233
+ should "not send empty request data" do
234
+ notice = build_notice
235
+ assert_nil notice.url
236
+ assert_nil notice.controller
237
+ assert_nil notice.action
238
+
239
+ xml = notice.to_xml
240
+ document = Nokogiri::XML.parse(xml)
241
+ assert_nil document.at('//request/url')
242
+ assert_nil document.at('//request/component')
243
+ assert_nil document.at('//request/action')
244
+
245
+ assert_valid_notice_document document
246
+ end
247
+
248
+ %w(url controller action).each do |var|
249
+ should "send a request if #{var} is present" do
250
+ notice = build_notice(var.to_sym => 'value')
251
+ xml = notice.to_xml
252
+ document = Nokogiri::XML.parse(xml)
253
+ assert_not_nil document.at('//request')
254
+ end
255
+ end
256
+
257
+ %w(parameters cgi_data session_data).each do |var|
258
+ should "send a request if #{var} is present" do
259
+ notice = build_notice(var.to_sym => { 'key' => 'value' })
260
+ xml = notice.to_xml
261
+ document = Nokogiri::XML.parse(xml)
262
+ assert_not_nil document.at('//request')
263
+ end
264
+ end
265
+
266
+ should "not ignore an exception not matching ignore filters" do
267
+ notice = build_notice(:error_class => 'ArgumentError',
268
+ :ignore => ['Argument'],
269
+ :ignore_by_filters => [lambda { |notice| false }])
270
+ assert !notice.ignore?
271
+ end
272
+
273
+ should "ignore an exception with a matching error class" do
274
+ notice = build_notice(:error_class => 'ArgumentError',
275
+ :ignore => [ArgumentError])
276
+ assert notice.ignore?
277
+ end
278
+
279
+ should "ignore an exception with a matching error class name" do
280
+ notice = build_notice(:error_class => 'ArgumentError',
281
+ :ignore => ['ArgumentError'])
282
+ assert notice.ignore?
283
+ end
284
+
285
+ should "ignore an exception with a matching filter" do
286
+ filter = lambda {|notice| notice.error_class == 'ArgumentError' }
287
+ notice = build_notice(:error_class => 'ArgumentError',
288
+ :ignore_by_filters => [filter])
289
+ assert notice.ignore?
290
+ end
291
+
292
+ should "not raise without an ignore list" do
293
+ notice = build_notice(:ignore => nil, :ignore_by_filters => nil)
294
+ assert_nothing_raised do
295
+ notice.ignore?
296
+ end
297
+ end
298
+
299
+ should "ignore RecordNotFound error by default" do
300
+ notice = build_notice(:error_class => 'ActiveRecord::RecordNotFound')
301
+ assert notice.ignore?
302
+ end
303
+
304
+ should "ignore RoutingError error by default" do
305
+ notice = build_notice(:error_class => 'ActionController::RoutingError')
306
+ assert notice.ignore?
307
+ end
308
+
309
+ should "ignore InvalidAuthenticityToken error by default" do
310
+ notice = build_notice(:error_class => 'ActionController::InvalidAuthenticityToken')
311
+ assert notice.ignore?
312
+ end
313
+
314
+ should "ignore TamperedWithCookie error by default" do
315
+ notice = build_notice(:error_class => 'CGI::Session::CookieStore::TamperedWithCookie')
316
+ assert notice.ignore?
317
+ end
318
+
319
+ should "ignore UnknownAction error by default" do
320
+ notice = build_notice(:error_class => 'ActionController::UnknownAction')
321
+ assert notice.ignore?
322
+ end
323
+
324
+ should "act like a hash" do
325
+ notice = build_notice(:error_message => 'some message')
326
+ assert_equal notice.error_message, notice[:error_message]
327
+ end
328
+
329
+ should "return params on notice[:request][:params]" do
330
+ params = { 'one' => 'two' }
331
+ notice = build_notice(:parameters => params)
332
+ assert_equal params, notice[:request][:params]
333
+ end
334
+
335
+ should "ensure #to_hash is called on objects that support it" do
336
+ assert_nothing_raised do
337
+ build_notice(:session => { :object => stub(:to_hash => {}) })
338
+ end
339
+ end
340
+
341
+ should "extract data from a rack environment hash" do
342
+ url = "https://subdomain.happylane.com:100/test/file.rb?var=value&var2=value2"
343
+ parameters = { 'var' => 'value', 'var2' => 'value2' }
344
+ env = Rack::MockRequest.env_for(url)
345
+
346
+ notice = build_notice(:rack_env => env)
347
+
348
+ assert_equal url, notice.url
349
+ assert_equal parameters, notice.parameters
350
+ assert_equal 'GET', notice.cgi_data['REQUEST_METHOD']
351
+ end
352
+
353
+ should "extract data from a rack environment hash with action_dispatch info" do
354
+ params = { 'controller' => 'users', 'action' => 'index', 'id' => '7' }
355
+ env = Rack::MockRequest.env_for('/', { 'action_dispatch.request.parameters' => params })
356
+
357
+ notice = build_notice(:rack_env => env)
358
+
359
+ assert_equal params, notice.parameters
360
+ assert_equal params['controller'], notice.component
361
+ assert_equal params['action'], notice.action
362
+ end
363
+
364
+ should "extract session data from a rack environment" do
365
+ session_data = { 'something' => 'some value' }
366
+ env = Rack::MockRequest.env_for('/', 'rack.session' => session_data)
367
+
368
+ notice = build_notice(:rack_env => env)
369
+
370
+ assert_equal session_data, notice.session_data
371
+ end
372
+
373
+ should "prefer passed session data to rack session data" do
374
+ session_data = { 'something' => 'some value' }
375
+ env = Rack::MockRequest.env_for('/')
376
+
377
+ notice = build_notice(:rack_env => env, :session_data => session_data)
378
+
379
+ assert_equal session_data, notice.session_data
380
+ end
381
+
382
+ def assert_accepts_exception_attribute(attribute, args = {}, &block)
383
+ exception = build_exception
384
+ block ||= lambda { exception.send(attribute) }
385
+ value = block.call(exception)
386
+
387
+ notice_from_exception = build_notice(args.merge(:exception => exception))
388
+
389
+ assert_equal notice_from_exception.send(attribute),
390
+ value,
391
+ "#{attribute} was not correctly set from an exception"
392
+
393
+ notice_from_hash = build_notice(args.merge(attribute => value))
394
+ assert_equal notice_from_hash.send(attribute),
395
+ value,
396
+ "#{attribute} was not correctly set from a hash"
397
+ end
398
+
399
+ def assert_serializes_hash(attribute)
400
+ [File.open(__FILE__), Proc.new { puts "boo!" }, Module.new].each do |object|
401
+ hash = {
402
+ :strange_object => object,
403
+ :sub_hash => {
404
+ :sub_object => object
405
+ },
406
+ :array => [object]
407
+ }
408
+ notice = build_notice(attribute => hash)
409
+ hash = notice.send(attribute)
410
+ assert_equal object.to_s, hash[:strange_object], "objects should be serialized"
411
+ assert_kind_of Hash, hash[:sub_hash], "subhashes should be kept"
412
+ assert_equal object.to_s, hash[:sub_hash][:sub_object], "subhash members should be serialized"
413
+ assert_kind_of Array, hash[:array], "arrays should be kept"
414
+ assert_equal object.to_s, hash[:array].first, "array members should be serialized"
415
+ end
416
+ end
417
+
418
+ def assert_valid_notice_document(document)
419
+ xsd_path = File.join(File.dirname(__FILE__), "hoptoad_2_0.xsd")
420
+ schema = Nokogiri::XML::Schema.new(IO.read(xsd_path))
421
+ errors = schema.validate(document)
422
+ assert errors.empty?, errors.collect{|e| e.message }.join
423
+ end
424
+
425
+ def assert_filters_hash(attribute)
426
+ filters = %w(abc def)
427
+ original = { 'abc' => "123", 'def' => "456", 'ghi' => "789", 'nested' => { 'abc' => '100' } }
428
+ filtered = { 'abc' => "[FILTERED]",
429
+ 'def' => "[FILTERED]",
430
+ 'ghi' => "789",
431
+ 'nested' => { 'abc' => '[FILTERED]' } }
432
+
433
+ notice = build_notice(:params_filters => filters, attribute => original)
434
+
435
+ assert_equal(filtered,
436
+ notice.send(attribute))
437
+ end
438
+
439
+ def build_backtrace_array
440
+ ["app/models/user.rb:13:in `magic'",
441
+ "app/controllers/users_controller.rb:8:in `index'"]
442
+ end
443
+ end