projectlocker_pulse 0.2.1

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