appsignal 3.9.3-java → 3.11.0-java

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +22 -19
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +180 -0
  5. data/Gemfile +1 -0
  6. data/README.md +0 -1
  7. data/Rakefile +1 -1
  8. data/benchmark.rake +99 -42
  9. data/build_matrix.yml +10 -12
  10. data/gemfiles/webmachine1.gemfile +5 -4
  11. data/lib/appsignal/cli/demo.rb +0 -1
  12. data/lib/appsignal/config.rb +57 -97
  13. data/lib/appsignal/demo.rb +15 -20
  14. data/lib/appsignal/environment.rb +6 -1
  15. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +1 -0
  16. data/lib/appsignal/event_formatter.rb +3 -2
  17. data/lib/appsignal/helpers/instrumentation.rb +490 -16
  18. data/lib/appsignal/hooks/action_cable.rb +21 -16
  19. data/lib/appsignal/hooks/active_job.rb +15 -14
  20. data/lib/appsignal/hooks/delayed_job.rb +1 -1
  21. data/lib/appsignal/hooks/shoryuken.rb +3 -63
  22. data/lib/appsignal/integrations/action_cable.rb +5 -7
  23. data/lib/appsignal/integrations/active_support_notifications.rb +1 -0
  24. data/lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb +36 -35
  25. data/lib/appsignal/integrations/data_mapper.rb +1 -0
  26. data/lib/appsignal/integrations/delayed_job_plugin.rb +27 -33
  27. data/lib/appsignal/integrations/dry_monitor.rb +1 -0
  28. data/lib/appsignal/integrations/excon.rb +1 -0
  29. data/lib/appsignal/integrations/http.rb +1 -0
  30. data/lib/appsignal/integrations/net_http.rb +1 -0
  31. data/lib/appsignal/integrations/object.rb +6 -0
  32. data/lib/appsignal/integrations/padrino.rb +21 -25
  33. data/lib/appsignal/integrations/que.rb +13 -20
  34. data/lib/appsignal/integrations/railtie.rb +1 -1
  35. data/lib/appsignal/integrations/rake.rb +45 -15
  36. data/lib/appsignal/integrations/redis.rb +1 -0
  37. data/lib/appsignal/integrations/redis_client.rb +1 -0
  38. data/lib/appsignal/integrations/resque.rb +2 -5
  39. data/lib/appsignal/integrations/shoryuken.rb +75 -0
  40. data/lib/appsignal/integrations/sidekiq.rb +7 -25
  41. data/lib/appsignal/integrations/unicorn.rb +1 -0
  42. data/lib/appsignal/integrations/webmachine.rb +12 -9
  43. data/lib/appsignal/logger.rb +7 -3
  44. data/lib/appsignal/probes/helpers.rb +1 -0
  45. data/lib/appsignal/probes/mri.rb +1 -0
  46. data/lib/appsignal/probes/sidekiq.rb +1 -0
  47. data/lib/appsignal/probes.rb +3 -0
  48. data/lib/appsignal/rack/abstract_middleware.rb +67 -24
  49. data/lib/appsignal/rack/body_wrapper.rb +143 -0
  50. data/lib/appsignal/rack/event_handler.rb +39 -8
  51. data/lib/appsignal/rack/generic_instrumentation.rb +6 -4
  52. data/lib/appsignal/rack/grape_middleware.rb +3 -2
  53. data/lib/appsignal/rack/hanami_middleware.rb +1 -1
  54. data/lib/appsignal/rack/instrumentation_middleware.rb +62 -0
  55. data/lib/appsignal/rack/rails_instrumentation.rb +1 -3
  56. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -3
  57. data/lib/appsignal/rack/streaming_listener.rb +14 -59
  58. data/lib/appsignal/rack.rb +60 -0
  59. data/lib/appsignal/span.rb +1 -0
  60. data/lib/appsignal/transaction.rb +353 -104
  61. data/lib/appsignal/utils/data.rb +0 -1
  62. data/lib/appsignal/utils/hash_sanitizer.rb +0 -1
  63. data/lib/appsignal/utils/integration_logger.rb +0 -13
  64. data/lib/appsignal/utils/integration_memory_logger.rb +0 -13
  65. data/lib/appsignal/utils/json.rb +0 -1
  66. data/lib/appsignal/utils/query_params_sanitizer.rb +0 -1
  67. data/lib/appsignal/utils/stdout_and_logger_message.rb +0 -1
  68. data/lib/appsignal/utils.rb +6 -0
  69. data/lib/appsignal/version.rb +1 -1
  70. data/lib/appsignal.rb +9 -6
  71. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  72. data/spec/lib/appsignal/config_spec.rb +139 -43
  73. data/spec/lib/appsignal/hooks/action_cable_spec.rb +43 -74
  74. data/spec/lib/appsignal/hooks/activejob_spec.rb +9 -0
  75. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +2 -443
  76. data/spec/lib/appsignal/hooks/rake_spec.rb +100 -17
  77. data/spec/lib/appsignal/hooks/shoryuken_spec.rb +0 -171
  78. data/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb +459 -0
  79. data/spec/lib/appsignal/integrations/padrino_spec.rb +181 -131
  80. data/spec/lib/appsignal/integrations/que_spec.rb +3 -4
  81. data/spec/lib/appsignal/integrations/shoryuken_spec.rb +167 -0
  82. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +4 -4
  83. data/spec/lib/appsignal/integrations/sinatra_spec.rb +10 -2
  84. data/spec/lib/appsignal/integrations/webmachine_spec.rb +77 -17
  85. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +144 -11
  86. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +263 -0
  87. data/spec/lib/appsignal/rack/event_handler_spec.rb +81 -10
  88. data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +70 -17
  89. data/spec/lib/appsignal/rack/grape_middleware_spec.rb +1 -1
  90. data/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb +38 -0
  91. data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +4 -2
  92. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +43 -120
  93. data/spec/lib/appsignal/rack_spec.rb +63 -0
  94. data/spec/lib/appsignal/transaction_spec.rb +1675 -953
  95. data/spec/lib/appsignal/utils/integration_logger_spec.rb +12 -16
  96. data/spec/lib/appsignal/utils/integration_memory_logger_spec.rb +0 -10
  97. data/spec/lib/appsignal_spec.rb +517 -13
  98. data/spec/support/helpers/transaction_helpers.rb +44 -20
  99. data/spec/support/matchers/transaction.rb +15 -1
  100. data/spec/support/mocks/dummy_app.rb +1 -1
  101. data/spec/support/testing.rb +1 -1
  102. metadata +12 -4
  103. data/support/check_versions +0 -22
@@ -15,31 +15,64 @@ if DependencyHelper.webmachine_present?
15
15
 
16
16
  describe Appsignal::Integrations::WebmachineIntegration do
17
17
  let(:request) do
18
- Webmachine::Request.new("GET", "http://google.com:80/foo", {}, nil)
18
+ Webmachine::Request.new(
19
+ "GET",
20
+ "http://google.com:80/foo?param1=value1&param2=value2",
21
+ {
22
+ "REQUEST_METHOD" => "GET",
23
+ "PATH_INFO" => "/some/path",
24
+ "ignored_header" => "something"
25
+ },
26
+ nil
27
+ )
19
28
  end
20
- let(:resource) { double(:trace? => false, :handle_exception => true, :"code=" => nil) }
21
- let(:response) { Response.new }
22
- let(:fsm) { Webmachine::Decision::FSM.new(resource, request, response) }
23
- before(:context) { start_agent }
24
- around { |example| keep_transactions { example.run } }
29
+ let(:app) do
30
+ proc do
31
+ def to_html
32
+ "Some HTML"
33
+ end
34
+ end
35
+ end
36
+ let(:resource) do
37
+ app_block = app
38
+ Class.new(Webmachine::Resource) do
39
+ class_eval(&app_block) if app_block
25
40
 
26
- # Make sure the request responds to the method we need to get query params.
27
- describe "request" do
28
- it "responds to #query" do
29
- expect(request).to respond_to(:query)
41
+ def self.name
42
+ "MyResource"
43
+ end
30
44
  end
31
45
  end
46
+ let(:resource_instance) { resource.new(request, response) }
47
+ let(:response) { Webmachine::Response.new }
48
+ let(:fsm) { Webmachine::Decision::FSM.new(resource_instance, request, response) }
49
+ before { start_agent }
50
+ around { |example| keep_transactions { example.run } }
32
51
 
33
52
  describe "#run" do
34
- before { allow(fsm).to receive(:call).and_call_original }
35
-
36
53
  it "creates a transaction" do
37
54
  expect { fsm.run }.to(change { created_transactions.count }.by(1))
38
55
  end
39
56
 
40
57
  it "sets the action" do
41
58
  fsm.run
42
- expect(last_transaction).to have_action("RSpec::Mocks::Double#GET")
59
+ expect(last_transaction).to have_action("MyResource#GET")
60
+ end
61
+
62
+ context "with action already set" do
63
+ let(:app) do
64
+ proc do
65
+ def to_html
66
+ Appsignal.set_action("Custom Action")
67
+ "Some HTML"
68
+ end
69
+ end
70
+ end
71
+
72
+ it "doesn't overwrite the action" do
73
+ fsm.run
74
+ expect(last_transaction).to have_action("Custom Action")
75
+ end
43
76
  end
44
77
 
45
78
  it "records an instrumentation event" do
@@ -47,16 +80,43 @@ if DependencyHelper.webmachine_present?
47
80
  expect(last_transaction).to include_event("name" => "process_action.webmachine")
48
81
  end
49
82
 
83
+ it "sets the params" do
84
+ fsm.run
85
+ expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
86
+ end
87
+
88
+ it "sets the headers" do
89
+ fsm.run
90
+ expect(last_transaction).to include_environment(
91
+ "REQUEST_METHOD" => "GET",
92
+ "PATH_INFO" => "/some/path"
93
+ )
94
+ end
95
+
50
96
  it "closes the transaction" do
51
97
  fsm.run
52
98
  expect(last_transaction).to be_completed
53
99
  expect(current_transaction?).to be_falsy
54
100
  end
55
101
 
56
- it "sets a response code" do
57
- expect(fsm.response.code).to be_nil
58
- fsm.run
59
- expect(fsm.response.code).not_to be_nil
102
+ context "with parent transaction" do
103
+ let(:transaction) { http_request_transaction }
104
+ before { set_current_transaction(transaction) }
105
+
106
+ it "sets the action" do
107
+ fsm.run
108
+ expect(last_transaction).to have_action("MyResource#GET")
109
+ end
110
+
111
+ it "sets the params" do
112
+ fsm.run
113
+ last_transaction._sample
114
+ expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
115
+ end
116
+
117
+ it "does not close the transaction" do
118
+ expect(last_transaction).to_not be_completed
119
+ end
60
120
  end
61
121
  end
62
122
 
@@ -5,7 +5,8 @@ describe Appsignal::Rack::AbstractMiddleware do
5
5
  Rack::MockRequest.env_for(
6
6
  request_path,
7
7
  "REQUEST_METHOD" => "GET",
8
- :params => { "page" => 2, "query" => "lorem" }
8
+ :params => { "page" => 2, "query" => "lorem" },
9
+ "rack.session" => { "session" => "data", "user_id" => 123 }
9
10
  )
10
11
  end
11
12
  let(:options) { {} }
@@ -45,6 +46,11 @@ describe Appsignal::Rack::AbstractMiddleware do
45
46
  expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
46
47
  end
47
48
 
49
+ it "wraps the response body in a BodyWrapper subclass" do
50
+ _status, _headers, body = make_request
51
+ expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
52
+ end
53
+
48
54
  context "without an error" do
49
55
  before { make_request }
50
56
 
@@ -56,8 +62,20 @@ describe Appsignal::Rack::AbstractMiddleware do
56
62
  expect(last_transaction).to_not have_error
57
63
  end
58
64
 
59
- it "records an instrumentation event" do
60
- expect(last_transaction).to include_event(:name => "process.abstract")
65
+ context "without :instrument_event_name option set" do
66
+ let(:options) { {} }
67
+
68
+ it "does not record an instrumentation event" do
69
+ expect(last_transaction).to_not include_event
70
+ end
71
+ end
72
+
73
+ context "with :instrument_event_name option set" do
74
+ let(:options) { { :instrument_event_name => "event_name.category" } }
75
+
76
+ it "records an instrumentation event" do
77
+ expect(last_transaction).to include_event(:name => "event_name.category")
78
+ end
61
79
  end
62
80
 
63
81
  it "completes the transaction" do
@@ -66,8 +84,8 @@ describe Appsignal::Rack::AbstractMiddleware do
66
84
  .to be_kind_of(Appsignal::Transaction::NilTransaction)
67
85
  end
68
86
 
69
- context "when instrument_span_name option is nil" do
70
- let(:options) { { :instrument_span_name => nil } }
87
+ context "when instrument_event_name option is nil" do
88
+ let(:options) { { :instrument_event_name => nil } }
71
89
 
72
90
  it "does not record an instrumentation event" do
73
91
  expect(last_transaction).to_not include_events
@@ -148,21 +166,61 @@ describe Appsignal::Rack::AbstractMiddleware do
148
166
  end
149
167
 
150
168
  context "with appsignal.route env" do
169
+ before { env["appsignal.route"] = "POST /my-route" }
170
+
151
171
  it "reports the appsignal.route value as the action name" do
152
- env["appsignal.route"] = "POST /my-route"
153
172
  make_request
154
173
 
155
174
  expect(last_transaction).to have_action("POST /my-route")
156
175
  end
176
+
177
+ it "prints a deprecation warning" do
178
+ err_stream = std_stream
179
+ capture_std_streams(std_stream, err_stream) do
180
+ make_request
181
+ end
182
+
183
+ expect(err_stream.read).to include(
184
+ "Setting the action name with the request env 'appsignal.route' is deprecated."
185
+ )
186
+ end
187
+
188
+ it "logs a deprecation warning" do
189
+ logs = capture_logs { make_request }
190
+ expect(logs).to contains_log(
191
+ :warn,
192
+ "Setting the action name with the request env 'appsignal.route' is deprecated."
193
+ )
194
+ end
157
195
  end
158
196
 
159
197
  context "with appsignal.action env" do
160
- it "reports the appsignal.route value as the action name" do
161
- env["appsignal.action"] = "POST /my-action"
198
+ before { env["appsignal.action"] = "POST /my-action" }
199
+
200
+ it "reports the appsignal.action value as the action name" do
162
201
  make_request
163
202
 
164
203
  expect(last_transaction).to have_action("POST /my-action")
165
204
  end
205
+
206
+ it "prints a deprecation warning" do
207
+ err_stream = std_stream
208
+ capture_std_streams(std_stream, err_stream) do
209
+ make_request
210
+ end
211
+
212
+ expect(err_stream.read).to include(
213
+ "Setting the action name with the request env 'appsignal.action' is deprecated."
214
+ )
215
+ end
216
+
217
+ it "logs a deprecation warning" do
218
+ logs = capture_logs { make_request }
219
+ expect(logs).to contains_log(
220
+ :warn,
221
+ "Setting the action name with the request env 'appsignal.action' is deprecated."
222
+ )
223
+ end
166
224
  end
167
225
 
168
226
  describe "request metadata" do
@@ -190,7 +248,7 @@ describe Appsignal::Rack::AbstractMiddleware do
190
248
  end
191
249
  end
192
250
 
193
- context "with fetching the request method raises an error" do
251
+ context "when fetching the request method raises an error" do
194
252
  class BrokenRequestMethodRequest < Rack::Request
195
253
  def request_method
196
254
  raise "uh oh!"
@@ -198,11 +256,16 @@ describe Appsignal::Rack::AbstractMiddleware do
198
256
  end
199
257
 
200
258
  let(:options) { { :request_class => BrokenRequestMethodRequest } }
259
+
201
260
  it "does not store the invalid HTTP request method" do
202
261
  env["REQUEST_METHOD"] = "FOO"
203
- make_request
262
+ logs = capture_logs { make_request }
204
263
 
205
264
  expect(last_transaction).to_not include_metadata("method" => anything)
265
+ expect(logs).to contains_log(
266
+ :error,
267
+ "Exception while fetching the HTTP request method: RuntimeError: uh oh"
268
+ )
206
269
  end
207
270
  end
208
271
 
@@ -228,6 +291,35 @@ describe Appsignal::Rack::AbstractMiddleware do
228
291
  expect(last_transaction).to include_params("custom" => "param")
229
292
  end
230
293
  end
294
+
295
+ context "when fetching the request method raises an error" do
296
+ class BrokenRequestParamsRequest < Rack::Request
297
+ def params
298
+ raise "uh oh!"
299
+ end
300
+ end
301
+
302
+ let(:options) do
303
+ { :request_class => BrokenRequestParamsRequest, :params_method => :params }
304
+ end
305
+
306
+ it "does not store the invalid HTTP request method" do
307
+ logs = capture_logs { make_request }
308
+
309
+ expect(last_transaction).to_not include_params
310
+ expect(logs).to contains_log(
311
+ :error,
312
+ "Exception while fetching params " \
313
+ "from 'BrokenRequestParamsRequest#params': RuntimeError uh oh!"
314
+ )
315
+ end
316
+ end
317
+
318
+ it "sets session data" do
319
+ make_request
320
+
321
+ expect(last_transaction).to include_session_data("session" => "data", "user_id" => 123)
322
+ end
231
323
  end
232
324
 
233
325
  context "with queue start header" do
@@ -259,6 +351,10 @@ describe Appsignal::Rack::AbstractMiddleware do
259
351
  def filtered_params
260
352
  { "abc" => "123" }
261
353
  end
354
+
355
+ def session
356
+ { "data" => "value" }
357
+ end
262
358
  end
263
359
 
264
360
  context "with overridden request class and params method" do
@@ -271,11 +367,19 @@ describe Appsignal::Rack::AbstractMiddleware do
271
367
 
272
368
  expect(last_transaction).to include_params("abc" => "123")
273
369
  end
370
+
371
+ it "uses the overridden request class to fetch session data" do
372
+ make_request
373
+
374
+ expect(last_transaction).to include_session_data("data" => "value")
375
+ end
274
376
  end
275
377
 
276
378
  context "with parent instrumentation" do
379
+ let(:transaction) { http_request_transaction }
277
380
  before do
278
- env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = http_request_transaction
381
+ env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = transaction
382
+ set_current_transaction(transaction)
279
383
  end
280
384
 
281
385
  it "uses the existing transaction" do
@@ -284,6 +388,35 @@ describe Appsignal::Rack::AbstractMiddleware do
284
388
  expect { make_request }.to_not(change { created_transactions.count })
285
389
  end
286
390
 
391
+ it "wraps the response body in a BodyWrapper subclass" do
392
+ _status, _headers, body = make_request
393
+ expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
394
+
395
+ body.to_ary
396
+ response_events =
397
+ last_transaction.to_h["events"].count do |event|
398
+ event["name"] == "process_response_body.rack"
399
+ end
400
+ expect(response_events).to eq(1)
401
+ end
402
+
403
+ context "when response body is already a BodyWrapper subclass" do
404
+ let(:body) { Appsignal::Rack::BodyWrapper.wrap(["hello!"], transaction) }
405
+ let(:app) { DummyApp.new { [200, {}, body] } }
406
+
407
+ it "doesn't wrap the body again" do
408
+ _status, _headers, body = make_request
409
+ expect(body).to eq(body)
410
+
411
+ body.to_ary
412
+ response_events =
413
+ last_transaction.to_h["events"].count do |event|
414
+ event["name"] == "process_response_body.rack"
415
+ end
416
+ expect(response_events).to eq(1)
417
+ end
418
+ end
419
+
287
420
  context "with error" do
288
421
  let(:app) { lambda { |_env| raise ExampleException, "error message" } }
289
422
 
@@ -0,0 +1,263 @@
1
+ describe Appsignal::Rack::BodyWrapper do
2
+ let(:transaction) { http_request_transaction }
3
+ before do
4
+ start_agent
5
+ set_current_transaction(transaction)
6
+ end
7
+
8
+ describe "with a body that supports all possible features" do
9
+ it "reduces the supported methods to just each()" do
10
+ # which is the safest thing to do, since the body is likely broken
11
+ fake_body = double(
12
+ :each => nil,
13
+ :call => nil,
14
+ :to_ary => [],
15
+ :to_path => "/tmp/foo.bin",
16
+ :close => nil
17
+ )
18
+
19
+ wrapped = described_class.wrap(fake_body, transaction)
20
+ expect(wrapped).to respond_to(:each)
21
+ expect(wrapped).to_not respond_to(:to_ary)
22
+ expect(wrapped).to_not respond_to(:call)
23
+ expect(wrapped).to respond_to(:close)
24
+ end
25
+ end
26
+
27
+ describe "with a body only supporting each()" do
28
+ it "wraps with appropriate class" do
29
+ fake_body = double(:each => nil)
30
+
31
+ wrapped = described_class.wrap(fake_body, transaction)
32
+ expect(wrapped).to respond_to(:each)
33
+ expect(wrapped).to_not respond_to(:to_ary)
34
+ expect(wrapped).to_not respond_to(:call)
35
+ expect(wrapped).to respond_to(:close)
36
+ end
37
+
38
+ it "reads out the body in full using each" do
39
+ fake_body = double
40
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
41
+
42
+ wrapped = described_class.wrap(fake_body, transaction)
43
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
44
+
45
+ expect(transaction).to include_event(
46
+ "name" => "process_response_body.rack",
47
+ "title" => "Process Rack response body (#each)"
48
+ )
49
+ end
50
+
51
+ it "returns an Enumerator if each() gets called without a block" do
52
+ fake_body = double
53
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
54
+
55
+ wrapped = described_class.wrap(fake_body, transaction)
56
+ enum = wrapped.each
57
+ expect(enum).to be_kind_of(Enumerator)
58
+ expect { |b| enum.each(&b) }.to yield_successive_args("a", "b", "c")
59
+
60
+ expect(transaction).to_not include_event("name" => "process_response_body.rack")
61
+ end
62
+
63
+ it "sets the exception raised inside each() on the transaction" do
64
+ fake_body = double
65
+ expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
66
+
67
+ wrapped = described_class.wrap(fake_body, transaction)
68
+ expect do
69
+ expect { |b| wrapped.each(&b) }.to yield_control
70
+ end.to raise_error(ExampleException, "error message")
71
+
72
+ expect(transaction).to have_error("ExampleException", "error message")
73
+ end
74
+
75
+ it "closes the body and tracks an instrumentation event when it gets closed" do
76
+ fake_body = double(:close => nil)
77
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
78
+
79
+ wrapped = described_class.wrap(fake_body, transaction)
80
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
81
+ wrapped.close
82
+
83
+ expect(transaction).to include_event("name" => "close_response_body.rack")
84
+ end
85
+ end
86
+
87
+ describe "with a body supporting both each() and call" do
88
+ it "wraps with the wrapper that conceals call() and exposes each" do
89
+ fake_body = double
90
+ allow(fake_body).to receive(:each)
91
+ allow(fake_body).to receive(:call)
92
+
93
+ wrapped = described_class.wrap(fake_body, transaction)
94
+ expect(wrapped).to respond_to(:each)
95
+ expect(wrapped).to_not respond_to(:to_ary)
96
+ expect(wrapped).to_not respond_to(:call)
97
+ expect(wrapped).to_not respond_to(:to_path)
98
+ expect(wrapped).to respond_to(:close)
99
+ end
100
+ end
101
+
102
+ describe "with a body supporting both to_ary and each" do
103
+ let(:fake_body) { double(:each => nil, :to_ary => []) }
104
+
105
+ it "wraps with appropriate class" do
106
+ wrapped = described_class.wrap(fake_body, transaction)
107
+ expect(wrapped).to respond_to(:each)
108
+ expect(wrapped).to respond_to(:to_ary)
109
+ expect(wrapped).to_not respond_to(:call)
110
+ expect(wrapped).to_not respond_to(:to_path)
111
+ expect(wrapped).to respond_to(:close)
112
+ end
113
+
114
+ it "reads out the body in full using each" do
115
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
116
+
117
+ wrapped = described_class.wrap(fake_body, transaction)
118
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
119
+
120
+ expect(transaction).to include_event(
121
+ "name" => "process_response_body.rack",
122
+ "title" => "Process Rack response body (#each)"
123
+ )
124
+ end
125
+
126
+ it "sets the exception raised inside each() into the Appsignal transaction" do
127
+ expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
128
+
129
+ wrapped = described_class.wrap(fake_body, transaction)
130
+ expect do
131
+ expect { |b| wrapped.each(&b) }.to yield_control
132
+ end.to raise_error(ExampleException, "error message")
133
+
134
+ expect(transaction).to have_error("ExampleException", "error message")
135
+ end
136
+
137
+ it "reads out the body in full using to_ary" do
138
+ expect(fake_body).to receive(:to_ary).and_return(["one", "two", "three"])
139
+
140
+ wrapped = described_class.wrap(fake_body, transaction)
141
+ expect(wrapped.to_ary).to eq(["one", "two", "three"])
142
+
143
+ expect(transaction).to include_event(
144
+ "name" => "process_response_body.rack",
145
+ "title" => "Process Rack response body (#to_ary)"
146
+ )
147
+ end
148
+
149
+ it "sends the exception raised inside to_ary() into the Appsignal and closes transaction" do
150
+ fake_body = double
151
+ allow(fake_body).to receive(:each)
152
+ expect(fake_body).to receive(:to_ary).once.and_raise(ExampleException, "error message")
153
+ expect(fake_body).to_not receive(:close) # Per spec we expect the body has closed itself
154
+
155
+ wrapped = described_class.wrap(fake_body, transaction)
156
+ expect do
157
+ wrapped.to_ary
158
+ end.to raise_error(ExampleException, "error message")
159
+
160
+ expect(transaction).to have_error("ExampleException", "error message")
161
+ end
162
+ end
163
+
164
+ describe "with a body supporting both to_path and each" do
165
+ let(:fake_body) { double(:each => nil, :to_path => nil) }
166
+
167
+ it "wraps with appropriate class" do
168
+ wrapped = described_class.wrap(fake_body, transaction)
169
+ expect(wrapped).to respond_to(:each)
170
+ expect(wrapped).to_not respond_to(:to_ary)
171
+ expect(wrapped).to_not respond_to(:call)
172
+ expect(wrapped).to respond_to(:to_path)
173
+ expect(wrapped).to respond_to(:close)
174
+ end
175
+
176
+ it "reads out the body in full using each()" do
177
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
178
+
179
+ wrapped = described_class.wrap(fake_body, transaction)
180
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
181
+
182
+ expect(transaction).to include_event(
183
+ "name" => "process_response_body.rack",
184
+ "title" => "Process Rack response body (#each)"
185
+ )
186
+ end
187
+
188
+ it "sets the exception raised inside each() into the Appsignal transaction" do
189
+ expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
190
+
191
+ wrapped = described_class.wrap(fake_body, transaction)
192
+ expect do
193
+ expect { |b| wrapped.each(&b) }.to yield_control
194
+ end.to raise_error(ExampleException, "error message")
195
+
196
+ expect(transaction).to have_error("ExampleException", "error message")
197
+ end
198
+
199
+ it "sets the exception raised inside to_path() into the Appsignal transaction" do
200
+ allow(fake_body).to receive(:to_path).once.and_raise(ExampleException, "error message")
201
+
202
+ wrapped = described_class.wrap(fake_body, transaction)
203
+ expect do
204
+ wrapped.to_path
205
+ end.to raise_error(ExampleException, "error message")
206
+
207
+ expect(transaction).to have_error("ExampleException", "error message")
208
+ end
209
+
210
+ it "exposes to_path to the sender" do
211
+ allow(fake_body).to receive(:to_path).and_return("/tmp/file.bin")
212
+
213
+ wrapped = described_class.wrap(fake_body, transaction)
214
+ expect(wrapped.to_path).to eq("/tmp/file.bin")
215
+
216
+ expect(transaction).to include_event(
217
+ "name" => "process_response_body.rack",
218
+ "title" => "Process Rack response body (#to_path)"
219
+ )
220
+ end
221
+ end
222
+
223
+ describe "with a body only supporting call()" do
224
+ let(:fake_body) { double(:call => nil) }
225
+
226
+ it "wraps with appropriate class" do
227
+ wrapped = described_class.wrap(fake_body, transaction)
228
+ expect(wrapped).to_not respond_to(:each)
229
+ expect(wrapped).to_not respond_to(:to_ary)
230
+ expect(wrapped).to respond_to(:call)
231
+ expect(wrapped).to_not respond_to(:to_path)
232
+ expect(wrapped).to respond_to(:close)
233
+ end
234
+
235
+ it "passes the stream into the call() of the body" do
236
+ fake_rack_stream = double("stream")
237
+ expect(fake_body).to receive(:call).with(fake_rack_stream)
238
+
239
+ wrapped = described_class.wrap(fake_body, transaction)
240
+ wrapped.call(fake_rack_stream)
241
+
242
+ expect(transaction).to include_event(
243
+ "name" => "process_response_body.rack",
244
+ "title" => "Process Rack response body (#call)"
245
+ )
246
+ end
247
+
248
+ it "sets the exception raised inside call() into the Appsignal transaction" do
249
+ fake_rack_stream = double
250
+ allow(fake_body).to receive(:call)
251
+ .with(fake_rack_stream)
252
+ .and_raise(ExampleException, "error message")
253
+
254
+ wrapped = described_class.wrap(fake_body, transaction)
255
+
256
+ expect do
257
+ wrapped.call(fake_rack_stream)
258
+ end.to raise_error(ExampleException, "error message")
259
+
260
+ expect(transaction).to have_error("ExampleException", "error message")
261
+ end
262
+ end
263
+ end