projectlocker_pulse 0.2.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.
Files changed (82) hide show
  1. data/CHANGELOG +26 -0
  2. data/Gemfile +3 -0
  3. data/Guardfile +6 -0
  4. data/INSTALL +20 -0
  5. data/MIT-LICENSE +23 -0
  6. data/README.md +439 -0
  7. data/README_FOR_HEROKU_ADDON.md +89 -0
  8. data/Rakefile +223 -0
  9. data/SUPPORTED_RAILS_VERSIONS +38 -0
  10. data/TESTING.md +41 -0
  11. data/features/metal.feature +18 -0
  12. data/features/rack.feature +60 -0
  13. data/features/rails.feature +272 -0
  14. data/features/rails_with_js_notifier.feature +97 -0
  15. data/features/rake.feature +27 -0
  16. data/features/sinatra.feature +29 -0
  17. data/features/step_definitions/file_steps.rb +10 -0
  18. data/features/step_definitions/metal_steps.rb +23 -0
  19. data/features/step_definitions/rack_steps.rb +23 -0
  20. data/features/step_definitions/rails_application_steps.rb +478 -0
  21. data/features/step_definitions/rake_steps.rb +17 -0
  22. data/features/support/env.rb +18 -0
  23. data/features/support/matchers.rb +35 -0
  24. data/features/support/projectlocker_pulse_shim.rb.template +16 -0
  25. data/features/support/rails.rb +201 -0
  26. data/features/support/rake/Rakefile +68 -0
  27. data/features/support/terminal.rb +107 -0
  28. data/features/user_informer.feature +63 -0
  29. data/generators/pulse/lib/insert_commands.rb +34 -0
  30. data/generators/pulse/lib/rake_commands.rb +24 -0
  31. data/generators/pulse/pulse_generator.rb +94 -0
  32. data/generators/pulse/templates/capistrano_hook.rb +6 -0
  33. data/generators/pulse/templates/initializer.rb +6 -0
  34. data/generators/pulse/templates/pulse_tasks.rake +25 -0
  35. data/install.rb +1 -0
  36. data/lib/projectlocker_pulse.rb +159 -0
  37. data/lib/pulse/backtrace.rb +108 -0
  38. data/lib/pulse/capistrano.rb +43 -0
  39. data/lib/pulse/configuration.rb +305 -0
  40. data/lib/pulse/notice.rb +390 -0
  41. data/lib/pulse/rack.rb +54 -0
  42. data/lib/pulse/rails/action_controller_catcher.rb +30 -0
  43. data/lib/pulse/rails/controller_methods.rb +85 -0
  44. data/lib/pulse/rails/error_lookup.rb +33 -0
  45. data/lib/pulse/rails/javascript_notifier.rb +47 -0
  46. data/lib/pulse/rails/middleware/exceptions_catcher.rb +33 -0
  47. data/lib/pulse/rails.rb +40 -0
  48. data/lib/pulse/rails3_tasks.rb +99 -0
  49. data/lib/pulse/railtie.rb +49 -0
  50. data/lib/pulse/rake_handler.rb +65 -0
  51. data/lib/pulse/sender.rb +128 -0
  52. data/lib/pulse/shared_tasks.rb +47 -0
  53. data/lib/pulse/tasks.rb +83 -0
  54. data/lib/pulse/user_informer.rb +27 -0
  55. data/lib/pulse/utils/blank.rb +53 -0
  56. data/lib/pulse/version.rb +3 -0
  57. data/lib/pulse_tasks.rb +64 -0
  58. data/lib/rails/generators/pulse/pulse_generator.rb +100 -0
  59. data/lib/templates/javascript_notifier.erb +15 -0
  60. data/lib/templates/rescue.erb +91 -0
  61. data/pulse.gemspec +39 -0
  62. data/rails/init.rb +1 -0
  63. data/resources/README.md +34 -0
  64. data/resources/ca-bundle.crt +3376 -0
  65. data/script/integration_test.rb +38 -0
  66. data/test/backtrace_test.rb +162 -0
  67. data/test/capistrano_test.rb +34 -0
  68. data/test/catcher_test.rb +333 -0
  69. data/test/configuration_test.rb +236 -0
  70. data/test/helper.rb +263 -0
  71. data/test/javascript_notifier_test.rb +51 -0
  72. data/test/logger_test.rb +79 -0
  73. data/test/notice_test.rb +490 -0
  74. data/test/notifier_test.rb +276 -0
  75. data/test/projectlocker_pulse_tasks_test.rb +170 -0
  76. data/test/pulse.xsd +88 -0
  77. data/test/rack_test.rb +58 -0
  78. data/test/rails_initializer_test.rb +36 -0
  79. data/test/recursion_test.rb +10 -0
  80. data/test/sender_test.rb +288 -0
  81. data/test/user_informer_test.rb +29 -0
  82. metadata +432 -0
@@ -0,0 +1,490 @@
1
+ require File.expand_path '../helper', __FILE__
2
+
3
+ class NoticeTest < Test::Unit::TestCase
4
+
5
+ include DefinesConstants
6
+
7
+ def configure
8
+ Pulse::Configuration.new.tap do |config|
9
+ config.api_key = 'abc123def456'
10
+ end
11
+ end
12
+
13
+ def build_notice(args = {})
14
+ configuration = args.delete(:configuration) || configure
15
+ Pulse::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
+ def assert_accepts_exception_attribute(attribute, args = {}, &block)
28
+ exception = build_exception
29
+ block ||= lambda { exception.send(attribute) }
30
+ value = block.call(exception)
31
+
32
+ notice_from_exception = build_notice(args.merge(:exception => exception))
33
+
34
+ assert_equal notice_from_exception.send(attribute),
35
+ value,
36
+ "#{attribute} was not correctly set from an exception"
37
+
38
+ notice_from_hash = build_notice(args.merge(attribute => value))
39
+ assert_equal notice_from_hash.send(attribute),
40
+ value,
41
+ "#{attribute} was not correctly set from a hash"
42
+ end
43
+
44
+ def assert_serializes_hash(attribute)
45
+ [File.open(__FILE__), Proc.new { puts "boo!" }, Module.new].each do |object|
46
+ hash = {
47
+ :strange_object => object,
48
+ :sub_hash => {
49
+ :sub_object => object
50
+ },
51
+ :array => [object]
52
+ }
53
+ notice = build_notice(attribute => hash)
54
+ hash = notice.send(attribute)
55
+ assert_equal object.to_s, hash[:strange_object], "objects should be serialized"
56
+ assert_kind_of Hash, hash[:sub_hash], "subhashes should be kept"
57
+ assert_equal object.to_s, hash[:sub_hash][:sub_object], "subhash members should be serialized"
58
+ assert_kind_of Array, hash[:array], "arrays should be kept"
59
+ assert_equal object.to_s, hash[:array].first, "array members should be serialized"
60
+ end
61
+ end
62
+
63
+ def assert_valid_notice_document(document)
64
+ xsd_path = File.join(File.dirname(__FILE__), "pulse.xsd")
65
+ schema = Nokogiri::XML::Schema.new(IO.read(xsd_path))
66
+ errors = schema.validate(document)
67
+ assert errors.empty?, errors.collect{|e| e.message }.join
68
+ end
69
+
70
+ def assert_filters_hash(attribute)
71
+ filters = ["abc", :def]
72
+ original = { 'abc' => "123", 'def' => "456", 'ghi' => "789", 'nested' => { 'abc' => '100' },
73
+ 'something_with_abc' => 'match the entire string'}
74
+ filtered = { 'abc' => "[FILTERED]",
75
+ 'def' => "[FILTERED]",
76
+ 'something_with_abc' => "match the entire string",
77
+ 'ghi' => "789",
78
+ 'nested' => { 'abc' => '[FILTERED]' } }
79
+
80
+ notice = build_notice(:params_filters => filters, attribute => original)
81
+
82
+ assert_equal(filtered,
83
+ notice.send(attribute))
84
+ end
85
+
86
+ def build_backtrace_array
87
+ ["app/models/user.rb:13:in `magic'",
88
+ "app/controllers/users_controller.rb:8:in `index'"]
89
+ end
90
+
91
+ def hostname
92
+ `hostname`.chomp
93
+ end
94
+
95
+ def user
96
+ Struct.new(:email,:id,:name).
97
+ new("darth@vader.com",1,"Anakin Skywalker")
98
+ end
99
+
100
+ should "set the api key" do
101
+ api_key = 'key'
102
+ notice = build_notice(:api_key => api_key)
103
+ assert_equal api_key, notice.api_key
104
+ end
105
+
106
+ should "accept a project root" do
107
+ project_root = '/path/to/project'
108
+ notice = build_notice(:project_root => project_root)
109
+ assert_equal project_root, notice.project_root
110
+ end
111
+
112
+ should "accept a component" do
113
+ assert_equal 'users_controller', build_notice(:component => 'users_controller').controller
114
+ end
115
+
116
+ should "alias the component as controller" do
117
+ assert_equal 'users_controller', build_notice(:controller => 'users_controller').component
118
+ assert_equal 'users_controller', build_notice(:component => 'users_controller').controller
119
+ end
120
+
121
+ should "accept a action" do
122
+ assert_equal 'index', build_notice(:action => 'index').action
123
+ end
124
+
125
+ should "accept a url" do
126
+ url = 'http://some.host/uri'
127
+ notice = build_notice(:url => url)
128
+ assert_equal url, notice.url
129
+ end
130
+
131
+ should "set the host name" do
132
+ notice = build_notice
133
+ assert_equal hostname, notice.hostname
134
+ end
135
+
136
+ should "accept a backtrace from an exception or hash" do
137
+ array = ["user.rb:34:in `crazy'"]
138
+ exception = build_exception
139
+ exception.set_backtrace array
140
+ backtrace = Pulse::Backtrace.parse(array)
141
+ notice_from_exception = build_notice(:exception => exception)
142
+
143
+
144
+ assert_equal backtrace,
145
+ notice_from_exception.backtrace,
146
+ "backtrace was not correctly set from an exception"
147
+
148
+ notice_from_hash = build_notice(:backtrace => array)
149
+ assert_equal backtrace,
150
+ notice_from_hash.backtrace,
151
+ "backtrace was not correctly set from a hash"
152
+ end
153
+
154
+ should "accept user" do
155
+ assert_equal user.id, build_notice(:user => user).user.id
156
+ assert_equal user.email, build_notice(:user => user).user.email
157
+ assert_equal user.name, build_notice(:user => user).user.name
158
+ end
159
+
160
+ should "pass its backtrace filters for parsing" do
161
+ backtrace_array = ['my/file/backtrace:3']
162
+ exception = build_exception
163
+ exception.set_backtrace(backtrace_array)
164
+ Pulse::Backtrace.expects(:parse).with(backtrace_array, {:filters => 'foo'})
165
+
166
+ notice = Pulse::Notice.new({:exception => exception, :backtrace_filters => 'foo'})
167
+ end
168
+
169
+ should "set the error class from an exception or hash" do
170
+ assert_accepts_exception_attribute :error_class do |exception|
171
+ exception.class.name
172
+ end
173
+ end
174
+
175
+ should "set the error message from an exception or hash" do
176
+ assert_accepts_exception_attribute :error_message do |exception|
177
+ "#{exception.class.name}: #{exception.message}"
178
+ end
179
+ end
180
+
181
+ should "accept parameters from a request or hash" do
182
+ parameters = { 'one' => 'two' }
183
+ notice_from_hash = build_notice(:parameters => parameters)
184
+ assert_equal notice_from_hash.parameters, parameters
185
+ end
186
+
187
+ should "accept session data from a session[:data] hash" do
188
+ data = { 'one' => 'two' }
189
+ notice = build_notice(:session => { :data => data })
190
+ assert_equal data, notice.session_data
191
+ end
192
+
193
+ should "accept session data from a session_data hash" do
194
+ data = { 'one' => 'two' }
195
+ notice = build_notice(:session_data => data)
196
+ assert_equal data, notice.session_data
197
+ end
198
+
199
+ should "accept an environment name" do
200
+ assert_equal 'development', build_notice(:environment_name => 'development').environment_name
201
+ end
202
+
203
+ should "accept CGI data from a hash" do
204
+ data = { 'string' => 'value' }
205
+ notice = build_notice(:cgi_data => data)
206
+ assert_equal data, notice.cgi_data, "should take CGI data from a hash"
207
+ end
208
+
209
+ should "accept notifier information" do
210
+ params = { :notifier_name => 'a name for a notifier',
211
+ :notifier_version => '1.0.5',
212
+ :notifier_url => 'http://notifiers.r.us/download' }
213
+ notice = build_notice(params)
214
+ assert_equal params[:notifier_name], notice.notifier_name
215
+ assert_equal params[:notifier_version], notice.notifier_version
216
+ assert_equal params[:notifier_url], notice.notifier_url
217
+ end
218
+
219
+ should "set sensible defaults without an exception" do
220
+ backtrace = Pulse::Backtrace.parse(build_backtrace_array)
221
+ notice = build_notice(:backtrace => build_backtrace_array)
222
+
223
+ assert_equal 'Notification', notice.error_message
224
+ assert_array_starts_with backtrace.lines, notice.backtrace.lines
225
+ assert_equal({}, notice.parameters)
226
+ assert_equal({}, notice.session_data)
227
+ end
228
+
229
+ should "use the caller as the backtrace for an exception without a backtrace" do
230
+ filters = Pulse::Configuration.new.backtrace_filters
231
+ backtrace = Pulse::Backtrace.parse(caller, :filters => filters)
232
+ notice = build_notice(:exception => StandardError.new('error'), :backtrace => nil)
233
+
234
+ assert_array_starts_with backtrace.lines, notice.backtrace.lines
235
+ end
236
+
237
+ should "convert unserializable objects to strings" do
238
+ assert_serializes_hash(:parameters)
239
+ assert_serializes_hash(:cgi_data)
240
+ assert_serializes_hash(:session_data)
241
+ end
242
+
243
+ should "filter parameters" do
244
+ assert_filters_hash(:parameters)
245
+ end
246
+
247
+ should "filter cgi data" do
248
+ assert_filters_hash(:cgi_data)
249
+ end
250
+
251
+ should "filter session" do
252
+ assert_filters_hash(:session_data)
253
+ end
254
+
255
+ should "remove rack.request.form_vars" do
256
+ original = {
257
+ "rack.request.form_vars" => "story%5Btitle%5D=The+TODO+label",
258
+ "abc" => "123"
259
+ }
260
+
261
+ notice = build_notice(:cgi_data => original)
262
+ assert_equal({"abc" => "123"}, notice.cgi_data)
263
+ end
264
+
265
+ context "a Notice turned into XML" do
266
+ setup do
267
+ Pulse.configure do |config|
268
+ config.api_key = "1234567890"
269
+ end
270
+
271
+ @exception = build_exception
272
+
273
+ @notice = build_notice({
274
+ :notifier_name => 'a name',
275
+ :notifier_version => '1.2.3',
276
+ :notifier_url => 'http://some.url/path',
277
+ :exception => @exception,
278
+ :controller => "controller",
279
+ :action => "action",
280
+ :url => "http://url.com",
281
+ :parameters => { "paramskey" => "paramsvalue",
282
+ "nestparentkey" => { "nestkey" => "nestvalue" } },
283
+ :session_data => { "sessionkey" => "sessionvalue" },
284
+ :cgi_data => { "cgikey" => "cgivalue" },
285
+ :project_root => "RAILS_ROOT",
286
+ :environment_name => "RAILS_ENV"
287
+ })
288
+
289
+ @xml = @notice.to_xml
290
+
291
+ @document = Nokogiri::XML::Document.parse(@xml)
292
+ end
293
+
294
+ should "validate against the XML schema" do
295
+ assert_valid_notice_document @document
296
+ end
297
+
298
+ should "serialize a Notice to XML when sent #to_xml" do
299
+ assert_valid_node(@document, "//api-key", @notice.api_key)
300
+
301
+ assert_valid_node(@document, "//notifier/name", @notice.notifier_name)
302
+ assert_valid_node(@document, "//notifier/version", @notice.notifier_version)
303
+ assert_valid_node(@document, "//notifier/url", @notice.notifier_url)
304
+
305
+ assert_valid_node(@document, "//error/class", @notice.error_class)
306
+ assert_valid_node(@document, "//error/message", @notice.error_message)
307
+
308
+ assert_valid_node(@document, "//error/backtrace/line/@number", @notice.backtrace.lines.first.number)
309
+ assert_valid_node(@document, "//error/backtrace/line/@file", @notice.backtrace.lines.first.file)
310
+ assert_valid_node(@document, "//error/backtrace/line/@method", @notice.backtrace.lines.first.method)
311
+
312
+ assert_valid_node(@document, "//request/url", @notice.url)
313
+ assert_valid_node(@document, "//request/component", @notice.controller)
314
+ assert_valid_node(@document, "//request/action", @notice.action)
315
+
316
+ assert_valid_node(@document, "//request/params/var/@key", "paramskey")
317
+ assert_valid_node(@document, "//request/params/var", "paramsvalue")
318
+ assert_valid_node(@document, "//request/params/var/@key", "nestparentkey")
319
+ assert_valid_node(@document, "//request/params/var/var/@key", "nestkey")
320
+ assert_valid_node(@document, "//request/params/var/var", "nestvalue")
321
+ assert_valid_node(@document, "//request/session/var/@key", "sessionkey")
322
+ assert_valid_node(@document, "//request/session/var", "sessionvalue")
323
+ assert_valid_node(@document, "//request/cgi-data/var/@key", "cgikey")
324
+ assert_valid_node(@document, "//request/cgi-data/var", "cgivalue")
325
+
326
+ assert_valid_node(@document, "//server-environment/project-root", "RAILS_ROOT")
327
+ assert_valid_node(@document, "//server-environment/environment-name", "RAILS_ENV")
328
+ assert_valid_node(@document, "//server-environment/hostname", hostname)
329
+ end
330
+ end
331
+
332
+ should "not send empty request data" do
333
+ notice = build_notice
334
+ assert_nil notice.url
335
+ assert_nil notice.controller
336
+ assert_nil notice.action
337
+
338
+ xml = notice.to_xml
339
+ document = Nokogiri::XML.parse(xml)
340
+ assert_nil document.at('//request/url')
341
+ assert_nil document.at('//request/component')
342
+ assert_nil document.at('//request/action')
343
+
344
+ assert_valid_notice_document document
345
+ end
346
+
347
+ %w(url controller action).each do |var|
348
+ should "send a request if #{var} is present" do
349
+ notice = build_notice(var.to_sym => 'value')
350
+ xml = notice.to_xml
351
+ document = Nokogiri::XML.parse(xml)
352
+ assert_not_nil document.at('//request')
353
+ end
354
+ end
355
+
356
+ %w(parameters cgi_data session_data).each do |var|
357
+ should "send a request if #{var} is present" do
358
+ notice = build_notice(var.to_sym => { 'key' => 'value' })
359
+ xml = notice.to_xml
360
+ document = Nokogiri::XML.parse(xml)
361
+ assert_not_nil document.at('//request')
362
+ end
363
+ end
364
+
365
+ should "not ignore an exception not matching ignore filters" do
366
+ notice = build_notice(:error_class => 'ArgumentError',
367
+ :ignore => ['Argument'],
368
+ :ignore_by_filters => [lambda { |notice| false }])
369
+ assert !notice.ignore?
370
+ end
371
+
372
+ should "ignore an exception with a matching error class" do
373
+ notice = build_notice(:error_class => 'ArgumentError',
374
+ :ignore => [ArgumentError])
375
+ assert notice.ignore?
376
+ end
377
+
378
+ should "ignore an exception with a matching error class name" do
379
+ notice = build_notice(:error_class => 'ArgumentError',
380
+ :ignore => ['ArgumentError'])
381
+ assert notice.ignore?
382
+ end
383
+
384
+ should "ignore an exception with a matching filter" do
385
+ filter = lambda {|notice| notice.error_class == 'ArgumentError' }
386
+ notice = build_notice(:error_class => 'ArgumentError',
387
+ :ignore_by_filters => [filter])
388
+ assert notice.ignore?
389
+ end
390
+
391
+ should "not raise without an ignore list" do
392
+ notice = build_notice(:ignore => nil, :ignore_by_filters => nil)
393
+ assert_nothing_raised do
394
+ notice.ignore?
395
+ end
396
+ end
397
+
398
+ ignored_error_classes = Pulse::Configuration::IGNORE_DEFAULT
399
+
400
+ ignored_error_classes.each do |ignored_error_class|
401
+ should "ignore #{ignored_error_class} error by default" do
402
+ notice = build_notice(:error_class => ignored_error_class)
403
+ assert notice.ignore?
404
+ end
405
+ end
406
+
407
+ should "act like a hash" do
408
+ notice = build_notice(:error_message => 'some message')
409
+ assert_equal notice.error_message, notice[:error_message]
410
+ end
411
+
412
+ should "return params on notice[:request][:params]" do
413
+ params = { 'one' => 'two' }
414
+ notice = build_notice(:parameters => params)
415
+ assert_equal params, notice[:request][:params]
416
+ end
417
+
418
+ should "ensure #to_hash is called on objects that support it" do
419
+ assert_nothing_raised do
420
+ build_notice(:session => { :object => stub(:to_hash => {}) })
421
+ end
422
+ end
423
+
424
+ should "ensure #to_ary is called on objects that support it" do
425
+ assert_nothing_raised do
426
+ build_notice(:session => { :object => stub(:to_ary => {}) })
427
+ end
428
+ end
429
+
430
+ should "extract data from a rack environment hash" do
431
+ url = "https://subdomain.happylane.com:100/test/file.rb?var=value&var2=value2"
432
+ parameters = { 'var' => 'value', 'var2' => 'value2' }
433
+ env = Rack::MockRequest.env_for(url)
434
+
435
+ notice = build_notice(:rack_env => env)
436
+
437
+ assert_equal url, notice.url
438
+ assert_equal parameters, notice.parameters
439
+ assert_equal 'GET', notice.cgi_data['REQUEST_METHOD']
440
+ end
441
+
442
+ should "show a nice warning when rack environment exceeds rack keyspace" do
443
+ # simulate exception for too big query
444
+ Rack::Request.any_instance.expects(:params).raises(RangeError.new("exceeded available parameter key space"))
445
+
446
+ url = "https://subdomain.happylane.com:100/test/file.rb?var=x"
447
+ env = Rack::MockRequest.env_for(url)
448
+
449
+ notice = build_notice(:rack_env => env)
450
+
451
+ assert_equal url, notice.url
452
+ assert_equal({:message => "failed to call params on Rack::Request -- exceeded available parameter key space"}, notice.parameters)
453
+ assert_equal 'GET', notice.cgi_data['REQUEST_METHOD']
454
+ end
455
+
456
+ should "extract data from a rack environment hash with action_dispatch info" do
457
+ params = { 'controller' => 'users', 'action' => 'index', 'id' => '7' }
458
+ env = Rack::MockRequest.env_for('/', { 'action_dispatch.request.parameters' => params })
459
+
460
+ notice = build_notice(:rack_env => env)
461
+
462
+ assert_equal params, notice.parameters
463
+ assert_equal params['controller'], notice.component
464
+ assert_equal params['action'], notice.action
465
+ end
466
+
467
+ should "extract session data from a rack environment" do
468
+ session_data = { 'something' => 'some value' }
469
+ env = Rack::MockRequest.env_for('/', 'rack.session' => session_data)
470
+
471
+ notice = build_notice(:rack_env => env)
472
+
473
+ assert_equal session_data, notice.session_data
474
+ end
475
+
476
+ should "prefer passed session data to rack session data" do
477
+ session_data = { 'something' => 'some value' }
478
+ env = Rack::MockRequest.env_for('/')
479
+
480
+ notice = build_notice(:rack_env => env, :session_data => session_data)
481
+
482
+ assert_equal session_data, notice.session_data
483
+ end
484
+
485
+ should "prefer passed error_message to exception message" do
486
+ exception = build_exception
487
+ notice = build_notice(:exception => exception,:error_message => "Random ponies")
488
+ assert_equal "BacktracedException: Random ponies", notice.error_message
489
+ end
490
+ end