appsignal 3.6.0 → 3.6.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6402d39c536a0e4fe970163c92d3207cad1af89c279d0bec8494abf90844434c
4
- data.tar.gz: c989b01ded89a0c2643ba5b4bb02d24b02012ee25383b451fee192eae59ab611
3
+ metadata.gz: 0fe85cc642a413909a757ef143180354523bf05327df1f0eae2c0a29a1ce4570
4
+ data.tar.gz: ae2bd9370beec37c374885e5bf10b5316394243ed73038f5d377553734789597
5
5
  SHA512:
6
- metadata.gz: 65908eb0b138011eccd79c0fe0de47f3c2b150bc333eb656fde66f541529fe1801d0404a6dadfc875cbbf53b442613970dabe221fc2ae52fafd55927bb408bc8
7
- data.tar.gz: 2852aeb49f06d45606bc58a81c0e344b62326340aff7c81f363a2f081346b2147fd60eb7c38a0065c1dfed94ca4f148b1b866aafc6bada1734a4d1911746e07a
6
+ metadata.gz: 042fcd15607fa4ea121422ae7064181d90f2654d51dcea8f553aa3a6af8cafedc9ad60e5d6a3e74f15c701109d1aae65fb5ac66d1e597ca2dc83e91ec6342e52
7
+ data.tar.gz: 9e732c7b94dfa76013a8def22d31202feb6c7e4ecffb74a74408e2787ed8ae7b98fd8477b9a2af7242edc35d912d65f2c60a9228ef2ce019574082919ae4d622
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # AppSignal for Ruby gem Changelog
2
2
 
3
+ ## 3.6.2
4
+
5
+ _Published on 2024-03-08._
6
+
7
+ ### Fixed
8
+
9
+ - [c3921865](https://github.com/appsignal/appsignal-ruby/commit/c392186573a72fd9afe22299fabcd14dcfe96139) patch - Revert Rack middleware changes (see [changelog](https://github.com/appsignal/appsignal-ruby/blob/main/CHANGELOG.md#360)) to fix issues relating to Unicorn broken pipe errors and multiple requests merging into a single sample.
10
+
11
+ ## 3.6.1
12
+
13
+ _Published on 2024-03-05._
14
+
15
+ ### Added
16
+
17
+ - [8974d201](https://github.com/appsignal/appsignal-ruby/commit/8974d20144407fce7a274ebaeb771ef76705d901) patch - Add `activejob_report_errors` config option. When set to `"none"`, ActiveJob jobs will no longer report errors. This can be used in combination with [custom exception reporting](https://docs.appsignal.com/ruby/instrumentation/exception-handling.html). By default, the config option has the value `"all"`, which reports all errors.
18
+
3
19
  ## 3.6.0
4
20
 
5
21
  _Published on 2024-02-26._
@@ -11,6 +11,7 @@ module Appsignal
11
11
  include Appsignal::Utils::DeprecationMessage
12
12
 
13
13
  DEFAULT_CONFIG = {
14
+ :activejob_report_errors => "all",
14
15
  :ca_file_path => File.expand_path(File.join("../../../resources/cacert.pem"), __FILE__),
15
16
  :debug => false,
16
17
  :dns_servers => [],
@@ -65,6 +66,7 @@ module Appsignal
65
66
 
66
67
  ENV_TO_KEY_MAPPING = {
67
68
  "APPSIGNAL_ACTIVE" => :active,
69
+ "APPSIGNAL_ACTIVE_JOB_REPORT_ERRORS" => :activejob_report_errors,
68
70
  "APPSIGNAL_APP_NAME" => :name,
69
71
  "APPSIGNAL_BIND_ADDRESS" => :bind_address,
70
72
  "APPSIGNAL_CA_FILE_PATH" => :ca_file_path,
@@ -112,6 +114,7 @@ module Appsignal
112
114
  }.freeze
113
115
  # @api private
114
116
  ENV_STRING_KEYS = %w[
117
+ APPSIGNAL_ACTIVE_JOB_REPORT_ERRORS
115
118
  APPSIGNAL_APP_NAME
116
119
  APPSIGNAL_BIND_ADDRESS
117
120
  APPSIGNAL_CA_FILE_PATH
@@ -56,7 +56,7 @@ module Appsignal
56
56
  super
57
57
  rescue Exception => exception # rubocop:disable Lint/RescueException
58
58
  job_status = :failed
59
- transaction.set_error(exception)
59
+ transaction_set_error(transaction, exception)
60
60
  raise exception
61
61
  ensure
62
62
  if transaction
@@ -82,6 +82,14 @@ module Appsignal
82
82
  tags.merge(:status => :processed)
83
83
  end
84
84
  end
85
+
86
+ private
87
+
88
+ def transaction_set_error(transaction, exception)
89
+ return if Appsignal.config[:activejob_report_errors] == "none"
90
+
91
+ transaction.set_error(exception)
92
+ end
85
93
  end
86
94
 
87
95
  module ActiveJobHelpers
@@ -16,9 +16,7 @@ module Appsignal
16
16
  if Appsignal.active?
17
17
  call_with_appsignal_monitoring(env)
18
18
  else
19
- nil_transaction = Appsignal::Transaction::NilTransaction.new
20
- status, headers, obody = @app.call(env)
21
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
19
+ @app.call(env)
22
20
  end
23
21
  end
24
22
 
@@ -29,30 +27,19 @@ module Appsignal
29
27
  Appsignal::Transaction::HTTP_REQUEST,
30
28
  request
31
29
  )
32
- # We need to complete the transaction if there is an exception inside the `call`
33
- # of the app. If there isn't one and the app returns us a Rack response triplet, we let
34
- # the BodyWrapper complete the transaction when #close gets called on it
35
- # (guaranteed by the webserver)
36
- complete_transaction_without_body = false
37
30
  begin
38
31
  Appsignal.instrument("process_action.generic") do
39
- status, headers, obody = @app.call(env)
40
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
32
+ @app.call(env)
41
33
  end
42
34
  rescue Exception => error # rubocop:disable Lint/RescueException
43
35
  transaction.set_error(error)
44
- complete_transaction_without_body = true
45
36
  raise error
46
37
  ensure
47
- default_action = env["appsignal.route"] || env["appsignal.action"] || "unknown"
48
- transaction.set_action_if_nil(default_action)
38
+ transaction.set_action_if_nil(env["appsignal.route"] || "unknown")
49
39
  transaction.set_metadata("path", request.path)
50
40
  transaction.set_metadata("method", request.request_method)
51
41
  transaction.set_http_or_background_queue_start
52
-
53
- # Transaction gets completed when the body gets read out, except in cases when
54
- # the app failed before returning us the Rack response triplet.
55
- Appsignal::Transaction.complete_current! if complete_transaction_without_body
42
+ Appsignal::Transaction.complete_current!
56
43
  end
57
44
  end
58
45
  end
@@ -16,9 +16,7 @@ module Appsignal
16
16
  if Appsignal.active?
17
17
  call_with_appsignal_monitoring(env)
18
18
  else
19
- nil_transaction = Appsignal::Transaction::NilTransaction.new
20
- status, headers, obody = @app.call(env)
21
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
19
+ @app.call(env)
22
20
  end
23
21
  end
24
22
 
@@ -30,17 +28,10 @@ module Appsignal
30
28
  request,
31
29
  :params_method => :filtered_parameters
32
30
  )
33
- # We need to complete the transaction if there is an exception exception inside the `call`
34
- # of the app. If there isn't one and the app returns us a Rack response triplet, we let
35
- # the BodyWrapper complete the transaction when #close gets called on it
36
- # (guaranteed by the webserver)
37
- complete_transaction_without_body = false
38
31
  begin
39
- status, headers, obody = @app.call(env)
40
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
32
+ @app.call(env)
41
33
  rescue Exception => error # rubocop:disable Lint/RescueException
42
34
  transaction.set_error(error)
43
- complete_transaction_without_body = true
44
35
  raise error
45
36
  ensure
46
37
  controller = env["action_controller.instance"]
@@ -54,10 +45,7 @@ module Appsignal
54
45
  rescue => error
55
46
  Appsignal.internal_logger.error("Unable to report HTTP request method: '#{error}'")
56
47
  end
57
-
58
- # Transaction gets completed when the body gets read out, except in cases when
59
- # the app failed before returning us the Rack response triplet.
60
- Appsignal::Transaction.complete_current! if complete_transaction_without_body
48
+ Appsignal::Transaction.complete_current!
61
49
  end
62
50
  end
63
51
 
@@ -42,9 +42,7 @@ module Appsignal
42
42
  if Appsignal.active?
43
43
  call_with_appsignal_monitoring(env)
44
44
  else
45
- nil_transaction = Appsignal::Transaction::NilTransaction.new
46
- status, headers, obody = @app.call(env)
47
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
45
+ @app.call(env)
48
46
  end
49
47
  end
50
48
 
@@ -58,19 +56,12 @@ module Appsignal
58
56
  request,
59
57
  options
60
58
  )
61
- # We need to complete the transaction if there is an exception exception inside the `call`
62
- # of the app. If there isn't one and the app returns us a Rack response triplet, we let
63
- # the BodyWrapper complete the transaction when #close gets called on it
64
- # (guaranteed by the webserver)
65
- complete_transaction_without_body = false
66
59
  begin
67
60
  Appsignal.instrument("process_action.sinatra") do
68
- status, headers, obody = @app.call(env)
69
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
61
+ @app.call(env)
70
62
  end
71
63
  rescue Exception => error # rubocop:disable Lint/RescueException
72
64
  transaction.set_error(error)
73
- complete_transaction_without_body = true
74
65
  raise error
75
66
  ensure
76
67
  # If raise_error is off versions of Sinatra don't raise errors, but store
@@ -82,10 +73,7 @@ module Appsignal
82
73
  transaction.set_metadata("path", request.path)
83
74
  transaction.set_metadata("method", request.request_method)
84
75
  transaction.set_http_or_background_queue_start
85
-
86
- # Transaction gets completed when the body gets read out, except in cases when
87
- # the app failed before returning us the Rack response triplet.
88
- Appsignal::Transaction.complete_current! if complete_transaction_without_body
76
+ Appsignal::Transaction.complete_current!
89
77
  end
90
78
  end
91
79
 
@@ -16,9 +16,7 @@ module Appsignal
16
16
  if Appsignal.active?
17
17
  call_with_appsignal_monitoring(env)
18
18
  else
19
- nil_transaction = Appsignal::Transaction::NilTransaction.new
20
- status, headers, obody = @app.call(env)
21
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
19
+ @app.call(env)
22
20
  end
23
21
  end
24
22
 
@@ -30,35 +28,46 @@ module Appsignal
30
28
  request
31
29
  )
32
30
 
33
- # We need to complete the transaction if there is an exception exception inside the `call`
34
- # of the app. If there isn't one and the app returns us a Rack response triplet, we let
35
- # the BodyWrapper complete the transaction when #close gets called on it
36
- # (guaranteed by the webserver)
37
- complete_transaction_without_body = false
38
-
39
31
  # Instrument a `process_action`, to set params/action name
40
- begin
32
+ status, headers, body =
41
33
  Appsignal.instrument("process_action.rack") do
42
- status, headers, obody = @app.call(env)
43
- [status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
34
+ @app.call(env)
35
+ rescue Exception => e # rubocop:disable Lint/RescueException
36
+ transaction.set_error(e)
37
+ raise e
38
+ ensure
39
+ transaction.set_action_if_nil(env["appsignal.action"])
40
+ transaction.set_metadata("path", request.path)
41
+ transaction.set_metadata("method", request.request_method)
42
+ transaction.set_http_or_background_queue_start
44
43
  end
45
- rescue Exception => error # rubocop:disable Lint/RescueException
46
- transaction.set_error(error)
47
- complete_transaction_without_body = true
48
- raise error
49
- ensure
50
- transaction.set_action_if_nil(env["appsignal.action"])
51
- transaction.set_metadata("path", request.path)
52
- transaction.set_metadata("method", request.request_method)
53
- transaction.set_http_or_background_queue_start
54
44
 
55
- # Transaction gets completed when the body gets read out, except in cases when
56
- # the app failed before returning us the Rack response triplet.
57
- Appsignal::Transaction.complete_current! if complete_transaction_without_body
58
- end
45
+ # Wrap the result body with our StreamWrapper
46
+ [status, headers, StreamWrapper.new(body, transaction)]
59
47
  end
60
48
  end
61
49
  end
62
50
 
63
- StreamWrapper = Rack::EnumerableBodyWrapper
51
+ class StreamWrapper
52
+ def initialize(stream, transaction)
53
+ @stream = stream
54
+ @transaction = transaction
55
+ end
56
+
57
+ def each(&block)
58
+ @stream.each(&block)
59
+ rescue Exception => e # rubocop:disable Lint/RescueException
60
+ @transaction.set_error(e)
61
+ raise e
62
+ end
63
+
64
+ def close
65
+ @stream.close if @stream.respond_to?(:close)
66
+ rescue Exception => e # rubocop:disable Lint/RescueException
67
+ @transaction.set_error(e)
68
+ raise e
69
+ ensure
70
+ Appsignal::Transaction.complete_current!
71
+ end
72
+ end
64
73
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.6.0"
4
+ VERSION = "3.6.2"
5
5
  end
data/lib/appsignal.rb CHANGED
@@ -305,6 +305,5 @@ require "appsignal/garbage_collection"
305
305
  require "appsignal/integrations/railtie" if defined?(::Rails)
306
306
  require "appsignal/transaction"
307
307
  require "appsignal/version"
308
- require "appsignal/rack/body_wrapper"
309
308
  require "appsignal/rack/generic_instrumentation"
310
309
  require "appsignal/transmitter"
@@ -152,6 +152,7 @@ describe Appsignal::Config do
152
152
  it "merges with the default config" do
153
153
  expect(config.config_hash).to eq(
154
154
  :active => true,
155
+ :activejob_report_errors => "all",
155
156
  :ca_file_path => File.join(resources_dir, "cacert.pem"),
156
157
  :debug => false,
157
158
  :dns_servers => [],
@@ -222,6 +222,31 @@ if DependencyHelper.active_job_present?
222
222
  expect(events).to eq(expected_perform_events)
223
223
  end
224
224
 
225
+ context "with activejob_report_errors set to none" do
226
+ it "does not report the error" do
227
+ Appsignal.config = project_fixture_config("production")
228
+ Appsignal.config[:activejob_report_errors] = "none"
229
+
230
+ # Other calls we're testing in another test
231
+ allow(Appsignal).to receive(:increment_counter)
232
+ tags = { :queue => queue }
233
+ expect(Appsignal).to receive(:increment_counter)
234
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
235
+ expect(Appsignal).to receive(:increment_counter)
236
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
237
+
238
+ expect do
239
+ perform_job(ActiveJobErrorTestJob)
240
+ end.to raise_error(RuntimeError, "uh oh")
241
+
242
+ transaction = last_transaction
243
+ transaction_hash = transaction.to_h
244
+ expect(transaction_hash).to include(
245
+ "error" => nil
246
+ )
247
+ end
248
+ end
249
+
225
250
  if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
226
251
  context "with priority" do
227
252
  before do
@@ -50,7 +50,7 @@ describe Appsignal::Rack::GenericInstrumentation do
50
50
  expect(app).to receive(:call).with(env)
51
51
  end
52
52
 
53
- context "with an exception raised from call()", :error => true do
53
+ context "with an exception", :error => true do
54
54
  let(:error) { ExampleException }
55
55
  let(:app) do
56
56
  double.tap do |d|
@@ -58,9 +58,8 @@ describe Appsignal::Rack::GenericInstrumentation do
58
58
  end
59
59
  end
60
60
 
61
- it "records the exception and completes the transaction" do
61
+ it "records the exception" do
62
62
  expect_any_instance_of(Appsignal::Transaction).to receive(:set_error).with(error)
63
- expect(Appsignal::Transaction).to receive(:complete_current!)
64
63
  end
65
64
  end
66
65
 
@@ -65,8 +65,7 @@ if DependencyHelper.rails_present?
65
65
 
66
66
  describe "#call_with_appsignal_monitoring" do
67
67
  def run
68
- _status, _headers, body = middleware.call(env)
69
- body.close # Rack will always call close() on the body
68
+ middleware.call(env)
70
69
  end
71
70
 
72
71
  it "calls the wrapped app" do
@@ -127,8 +126,7 @@ if DependencyHelper.rails_present?
127
126
  end
128
127
  end
129
128
 
130
- it "records the exception and completes the transaction" do
131
- expect(Appsignal::Transaction).to receive(:complete_current!)
129
+ it "records the exception" do
132
130
  expect { run }.to raise_error(error)
133
131
 
134
132
  transaction_hash = last_transaction.to_h
@@ -3,9 +3,7 @@ if DependencyHelper.sinatra_present?
3
3
 
4
4
  module SinatraRequestHelpers
5
5
  def make_request(env)
6
- _status, _headers, body = middleware.call(env)
7
- # Close the body so that the transaction gets completed
8
- body&.close
6
+ middleware.call(env)
9
7
  end
10
8
 
11
9
  def make_request_with_error(env, error)
@@ -90,11 +90,10 @@ describe Appsignal::Rack::StreamingListener do
90
90
  context "with an exception in the instrumentation call" do
91
91
  let(:error) { ExampleException }
92
92
 
93
- it "should add the exception to the transaction and complete the transaction" do
93
+ it "should add the exception to the transaction" do
94
94
  allow(app).to receive(:call).and_raise(error)
95
95
 
96
96
  expect(transaction).to receive(:set_error).with(error)
97
- expect(Appsignal::Transaction).to receive(:complete_current!).and_call_original
98
97
 
99
98
  expect do
100
99
  listener.call_with_appsignal_monitoring(env)
@@ -102,19 +101,64 @@ describe Appsignal::Rack::StreamingListener do
102
101
  end
103
102
  end
104
103
 
105
- it "should wrap the response body in a wrapper" do
104
+ it "should wrap the body in a wrapper" do
105
+ expect(Appsignal::StreamWrapper).to receive(:new)
106
+ .with("body", transaction)
107
+ .and_return(wrapper)
108
+
106
109
  body = listener.call_with_appsignal_monitoring(env)[2]
107
110
 
108
- expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
111
+ expect(body).to be_a(Appsignal::StreamWrapper)
109
112
  end
110
113
  end
111
114
  end
112
115
 
113
116
  describe Appsignal::StreamWrapper do
114
- it ".new returns an EnumerableWrapper" do
115
- fake_body = double(:each => nil)
116
- fake_txn = double
117
- stream_wrapper = Appsignal::StreamWrapper.new(fake_body, fake_txn)
118
- expect(stream_wrapper).to be_kind_of(Appsignal::Rack::EnumerableBodyWrapper)
117
+ let(:stream) { double }
118
+ let(:transaction) do
119
+ Appsignal::Transaction.create(SecureRandom.uuid, Appsignal::Transaction::HTTP_REQUEST, {})
120
+ end
121
+ let(:wrapper) { Appsignal::StreamWrapper.new(stream, transaction) }
122
+
123
+ describe "#each" do
124
+ it "calls the original stream" do
125
+ expect(stream).to receive(:each)
126
+
127
+ wrapper.each
128
+ end
129
+
130
+ context "when #each raises an error" do
131
+ let(:error) { ExampleException }
132
+
133
+ it "records the exception" do
134
+ allow(stream).to receive(:each).and_raise(error)
135
+
136
+ expect(transaction).to receive(:set_error).with(error)
137
+
138
+ expect { wrapper.send(:each) }.to raise_error(error)
139
+ end
140
+ end
141
+ end
142
+
143
+ describe "#close" do
144
+ it "closes the original stream and completes the transaction" do
145
+ expect(stream).to receive(:close)
146
+ expect(Appsignal::Transaction).to receive(:complete_current!)
147
+
148
+ wrapper.close
149
+ end
150
+
151
+ context "when #close raises an error" do
152
+ let(:error) { ExampleException }
153
+
154
+ it "records the exception and completes the transaction" do
155
+ allow(stream).to receive(:close).and_raise(error)
156
+
157
+ expect(transaction).to receive(:set_error).with(error)
158
+ expect(transaction).to receive(:complete)
159
+
160
+ expect { wrapper.send(:close) }.to raise_error(error)
161
+ end
162
+ end
119
163
  end
120
164
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appsignal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.0
4
+ version: 3.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Beekman
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-02-26 00:00:00.000000000 Z
13
+ date: 2024-03-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -268,7 +268,6 @@ files:
268
268
  - lib/appsignal/probes/helpers.rb
269
269
  - lib/appsignal/probes/mri.rb
270
270
  - lib/appsignal/probes/sidekiq.rb
271
- - lib/appsignal/rack/body_wrapper.rb
272
271
  - lib/appsignal/rack/generic_instrumentation.rb
273
272
  - lib/appsignal/rack/rails_instrumentation.rb
274
273
  - lib/appsignal/rack/sinatra_instrumentation.rb
@@ -367,7 +366,6 @@ files:
367
366
  - spec/lib/appsignal/probes/gvl_spec.rb
368
367
  - spec/lib/appsignal/probes/mri_spec.rb
369
368
  - spec/lib/appsignal/probes/sidekiq_spec.rb
370
- - spec/lib/appsignal/rack/body_wrapper_spec.rb
371
369
  - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
372
370
  - spec/lib/appsignal/rack/rails_instrumentation_spec.rb
373
371
  - spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb
@@ -453,7 +451,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
453
451
  - !ruby/object:Gem::Version
454
452
  version: '0'
455
453
  requirements: []
456
- rubygems_version: 3.4.15
454
+ rubygems_version: 3.3.7
457
455
  signing_key:
458
456
  specification_version: 4
459
457
  summary: Logs performance and exception data from your app to appsignal.com
@@ -1,161 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Appsignal
4
- # @api private
5
- module Rack
6
- class BodyWrapper
7
- def self.wrap(original_body, appsignal_transaction)
8
- # The logic of how Rack treats a response body differs based on which methods
9
- # the body responds to. This means that to support the Rack 3.x spec in full
10
- # we need to return a wrapper which matches the API of the wrapped body as closely
11
- # as possible. Pick the wrapper from the most specific to the least specific.
12
- # See https://github.com/rack/rack/blob/main/SPEC.rdoc#the-body-
13
- #
14
- # What is important is that our Body wrapper responds to the same methods Rack
15
- # (or a webserver) would be checking and calling, and passes through that functionality
16
- # to the original body. This can be done using delegation via i.e. SimpleDelegate
17
- # but we also need "close" to get called correctly so that the Appsignal transaction
18
- # gets completed - which will not happen, for example, when #to_ary gets called
19
- # just on the delegated Rack body.
20
- #
21
- # This comment https://github.com/rails/rails/pull/49627#issuecomment-1769802573
22
- # is of particular interest to understand why this has to be somewhat complicated.
23
- if original_body.respond_to?(:to_path)
24
- PathableBodyWrapper.new(original_body, appsignal_transaction)
25
- elsif original_body.respond_to?(:to_ary)
26
- ArrayableBodyWrapper.new(original_body, appsignal_transaction)
27
- elsif !original_body.respond_to?(:each) && original_body.respond_to?(:call)
28
- # This body only supports #call, so we must be running a Rack 3 application
29
- # It is possible that a body exposes both `each` and `call` in the hopes of
30
- # being backwards-compatible with both Rack 3.x and Rack 2.x, however
31
- # this is not going to work since the SPEC says that if both are available,
32
- # `each` should be used and `call` should be ignored.
33
- # So for that case we can drop by to our default EnumerableBodyWrapper
34
- CallableBodyWrapper.new(original_body, appsignal_transaction)
35
- else
36
- EnumerableBodyWrapper.new(original_body, appsignal_transaction)
37
- end
38
- end
39
-
40
- def initialize(body, appsignal_transaction)
41
- @body_already_closed = false
42
- @body = body
43
- @transaction = appsignal_transaction
44
- end
45
-
46
- # This must be present in all Rack bodies and will be called by the serving adapter
47
- def close
48
- # The @body_already_closed check is needed so that if `to_ary`
49
- # of the body has already closed itself (as prescribed) we do not
50
- # attempt to close it twice
51
- if !@body_already_closed && @body.respond_to?(:close)
52
- Appsignal.instrument("response_body_close.rack") { @body.close }
53
- end
54
- @body_already_closed = true
55
- rescue Exception => error # rubocop:disable Lint/RescueException
56
- @transaction.set_error(error)
57
- raise error
58
- ensure
59
- complete_transaction!
60
- end
61
-
62
- def complete_transaction!
63
- # We need to call the Transaction class method and not
64
- # @transaction.complete because the transaction is still
65
- # thread-local and it needs to remove itself from the
66
- # thread variables correctly, which does not happen on
67
- # Transaction#complete.
68
- #
69
- # In the future it would be a good idea to ensure
70
- # that the current transaction is the same as @transaction,
71
- # or allow @transaction to complete itself and remove
72
- # itself from Thread.current
73
- Appsignal::Transaction.complete_current!
74
- end
75
- end
76
-
77
- # The standard Rack body wrapper which exposes "each" for iterating
78
- # over the response body. This is supported across all 3 major Rack
79
- # versions.
80
- #
81
- # @api private
82
- class EnumerableBodyWrapper < BodyWrapper
83
- def each(&blk)
84
- # This is a workaround for the Rails bug when there was a bit too much
85
- # eagerness in implementing to_ary, see:
86
- # https://github.com/rails/rails/pull/44953
87
- # https://github.com/rails/rails/pull/47092
88
- # https://github.com/rails/rails/pull/49627
89
- # https://github.com/rails/rails/issues/49588
90
- # While the Rack SPEC does not mandate `each` to be callable
91
- # in a blockless way it is still a good idea to have it in place.
92
- return enum_for(:each) unless block_given?
93
-
94
- Appsignal.instrument("process_response_body.rack", "Process Rack response body (#each)") do
95
- @body.each(&blk)
96
- end
97
- rescue Exception => error # rubocop:disable Lint/RescueException
98
- @transaction.set_error(error)
99
- raise error
100
- end
101
- end
102
-
103
- # The callable response bodies are a new Rack 3.x feature, and would not work
104
- # with older Rack versions. They must not respond to `each` because
105
- # "If it responds to each, you must call each and not call". This is why
106
- # it inherits from BodyWrapper directly and not from EnumerableBodyWrapper
107
- #
108
- # @api private
109
- class CallableBodyWrapper < BodyWrapper
110
- def call(stream)
111
- # `stream` will be closed by the app we are calling, no need for us
112
- # to close it ourselves
113
- Appsignal.instrument("process_response_body.rack", "Process Rack response body (#call)") do
114
- @body.call(stream)
115
- end
116
- rescue Exception => error # rubocop:disable Lint/RescueException
117
- @transaction.set_error(error)
118
- raise error
119
- end
120
- end
121
-
122
- # "to_ary" takes precedence over "each" and allows the response body
123
- # to be read eagerly. If the body supports that method, it takes precedence
124
- # over "each":
125
- # "Middleware may call to_ary directly on the Body and return a new Body in its place"
126
- # One could "fold" both the to_ary API and the each() API into one Body object, but
127
- # to_ary must also call "close" after it executes - and in the Rails implementation
128
- # this pecularity was not handled properly.
129
- #
130
- # @api private
131
- class ArrayableBodyWrapper < EnumerableBodyWrapper
132
- def to_ary
133
- @body_already_closed = true
134
- Appsignal.instrument(
135
- "process_response_body.rack",
136
- "Process Rack response body (#to_ary)"
137
- ) do
138
- @body.to_ary
139
- end
140
- rescue Exception => error # rubocop:disable Lint/RescueException
141
- @transaction.set_error(error)
142
- raise error
143
- ensure
144
- # We do not call "close" on ourselves as the only action
145
- # we need to complete is completing the transaction.
146
- complete_transaction!
147
- end
148
- end
149
-
150
- # Having "to_path" on a body allows Rack to serve out a static file, or to
151
- # pass that file to the downstream webserver for sending using X-Sendfile
152
- class PathableBodyWrapper < EnumerableBodyWrapper
153
- def to_path
154
- Appsignal.instrument("response_body_to_path.rack") { @body.to_path }
155
- rescue Exception => error # rubocop:disable Lint/RescueException
156
- @transaction.set_error(error)
157
- raise error
158
- end
159
- end
160
- end
161
- end
@@ -1,220 +0,0 @@
1
- describe Appsignal::Rack::BodyWrapper do
2
- let(:nil_txn) { Appsignal::Transaction::NilTransaction.new }
3
-
4
- describe "with a body that supports all possible features" do
5
- it "reduces the supported methods to just each()" do
6
- # which is the safest thing to do, since the body is likely broken
7
- fake_body = double(:each => nil, :call => nil, :to_ary => [], :to_path => "/tmp/foo.bin",
8
- :close => nil)
9
- wrapped = described_class.wrap(fake_body, nil_txn)
10
- expect(wrapped).to respond_to(:each)
11
- expect(wrapped).not_to respond_to(:to_ary)
12
- expect(wrapped).not_to respond_to(:call)
13
- expect(wrapped).to respond_to(:close)
14
- end
15
- end
16
-
17
- describe "with a body only supporting each()" do
18
- it "wraps with appropriate class" do
19
- fake_body = double
20
- allow(fake_body).to receive(:each)
21
-
22
- wrapped = described_class.wrap(fake_body, nil_txn)
23
- expect(wrapped).to respond_to(:each)
24
- expect(wrapped).not_to respond_to(:to_ary)
25
- expect(wrapped).not_to respond_to(:call)
26
- expect(wrapped).to respond_to(:close)
27
- end
28
-
29
- it "reads out the body in full using each" do
30
- fake_body = double
31
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
32
- wrapped = described_class.wrap(fake_body, nil_txn)
33
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
34
- end
35
-
36
- it "returns an Enumerator if each() gets called without a block" do
37
- fake_body = double
38
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
39
-
40
- wrapped = described_class.wrap(fake_body, nil_txn)
41
- enum = wrapped.each
42
- expect(enum).to be_kind_of(Enumerator)
43
- expect { |b| enum.each(&b) }.to yield_successive_args("a", "b", "c")
44
- end
45
-
46
- it "sets the exception raised inside each() into the Appsignal transaction" do
47
- fake_body = double
48
- expect(fake_body).to receive(:each).once.and_raise(Exception.new("Oops"))
49
-
50
- txn = double("Appsignal transaction", "nil_transaction?" => false)
51
- expect(txn).to receive(:set_error).once.with(instance_of(Exception))
52
-
53
- wrapped = described_class.wrap(fake_body, txn)
54
- expect do
55
- expect { |b| wrapped.each(&b) }.to yield_control
56
- end.to raise_error(/Oops/)
57
- end
58
-
59
- it "closes the body and the transaction when it gets closed" do
60
- fake_body = double
61
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
62
-
63
- txn = double("Appsignal transaction", "nil_transaction?" => false)
64
- expect(Appsignal::Transaction).to receive(:complete_current!).once
65
-
66
- wrapped = described_class.wrap(fake_body, txn)
67
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
68
- expect { wrapped.close }.not_to raise_error
69
- end
70
- end
71
-
72
- describe "with a body supporting both each() and call" do
73
- it "wraps with the wrapper that conceals call() and exposes each" do
74
- fake_body = double
75
- allow(fake_body).to receive(:each)
76
- allow(fake_body).to receive(:call)
77
-
78
- wrapped = described_class.wrap(fake_body, nil_txn)
79
- expect(wrapped).to respond_to(:each)
80
- expect(wrapped).not_to respond_to(:to_ary)
81
- expect(wrapped).not_to respond_to(:call)
82
- expect(wrapped).not_to respond_to(:to_path)
83
- expect(wrapped).to respond_to(:close)
84
- end
85
- end
86
-
87
- describe "with a body supporting both to_ary and each" do
88
- let(:fake_body) { double(:each => nil, :to_ary => []) }
89
- it "wraps with appropriate class" do
90
- wrapped = described_class.wrap(fake_body, nil_txn)
91
- expect(wrapped).to respond_to(:each)
92
- expect(wrapped).to respond_to(:to_ary)
93
- expect(wrapped).not_to respond_to(:call)
94
- expect(wrapped).not_to respond_to(:to_path)
95
- expect(wrapped).to respond_to(:close)
96
- end
97
-
98
- it "reads out the body in full using each" do
99
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
100
-
101
- wrapped = described_class.wrap(fake_body, nil_txn)
102
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
103
- end
104
-
105
- it "sets the exception raised inside each() into the Appsignal transaction" do
106
- expect(fake_body).to receive(:each).once.and_raise(Exception.new("Oops"))
107
-
108
- txn = double("Appsignal transaction", "nil_transaction?" => false)
109
- expect(txn).to receive(:set_error).once.with(instance_of(Exception))
110
-
111
- wrapped = described_class.wrap(fake_body, txn)
112
- expect do
113
- expect { |b| wrapped.each(&b) }.to yield_control
114
- end.to raise_error(/Oops/)
115
- end
116
-
117
- it "reads out the body in full using to_ary" do
118
- expect(fake_body).to receive(:to_ary).and_return(["one", "two", "three"])
119
-
120
- wrapped = described_class.wrap(fake_body, nil_txn)
121
- expect(wrapped.to_ary).to eq(["one", "two", "three"])
122
- end
123
-
124
- it "sends the exception raised inside to_ary() into the Appsignal and closes txn" do
125
- fake_body = double
126
- allow(fake_body).to receive(:each)
127
- expect(fake_body).to receive(:to_ary).once.and_raise(Exception.new("Oops"))
128
- expect(fake_body).not_to receive(:close) # Per spec we expect the body has closed itself
129
-
130
- txn = double("Appsignal transaction", "nil_transaction?" => false)
131
- expect(txn).to receive(:set_error).once.with(instance_of(Exception))
132
- expect(Appsignal::Transaction).to receive(:complete_current!).once
133
-
134
- wrapped = described_class.wrap(fake_body, txn)
135
- expect { wrapped.to_ary }.to raise_error(/Oops/)
136
- end
137
- end
138
-
139
- describe "with a body supporting both to_path and each" do
140
- let(:fake_body) { double(:each => nil, :to_path => nil) }
141
-
142
- it "wraps with appropriate class" do
143
- wrapped = described_class.wrap(fake_body, nil_txn)
144
- expect(wrapped).to respond_to(:each)
145
- expect(wrapped).not_to respond_to(:to_ary)
146
- expect(wrapped).not_to respond_to(:call)
147
- expect(wrapped).to respond_to(:to_path)
148
- expect(wrapped).to respond_to(:close)
149
- end
150
-
151
- it "reads out the body in full using each()" do
152
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
153
-
154
- wrapped = described_class.wrap(fake_body, nil_txn)
155
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
156
- end
157
-
158
- it "sets the exception raised inside each() into the Appsignal transaction" do
159
- expect(fake_body).to receive(:each).once.and_raise(Exception.new("Oops"))
160
-
161
- txn = double("Appsignal transaction", "nil_transaction?" => false)
162
- expect(txn).to receive(:set_error).once.with(instance_of(Exception))
163
-
164
- wrapped = described_class.wrap(fake_body, txn)
165
- expect do
166
- expect { |b| wrapped.each(&b) }.to yield_control
167
- end.to raise_error(/Oops/)
168
- end
169
-
170
- it "sets the exception raised inside to_path() into the Appsignal transaction" do
171
- allow(fake_body).to receive(:to_path).once.and_raise(Exception.new("Oops"))
172
-
173
- txn = double("Appsignal transaction", "nil_transaction?" => false)
174
- expect(txn).to receive(:set_error).once.with(instance_of(Exception))
175
- expect(txn).not_to receive(:complete) # gets called by the caller via close()
176
-
177
- wrapped = described_class.wrap(fake_body, txn)
178
- expect { wrapped.to_path }.to raise_error(/Oops/)
179
- end
180
-
181
- it "exposes to_path to the sender" do
182
- allow(fake_body).to receive(:to_path).and_return("/tmp/file.bin")
183
-
184
- wrapped = described_class.wrap(fake_body, nil_txn)
185
- expect(wrapped.to_path).to eq("/tmp/file.bin")
186
- end
187
- end
188
-
189
- describe "with a body only supporting call()" do
190
- let(:fake_body) { double(:call => nil) }
191
- it "wraps with appropriate class" do
192
- wrapped = described_class.wrap(fake_body, nil_txn)
193
- expect(wrapped).not_to respond_to(:each)
194
- expect(wrapped).not_to respond_to(:to_ary)
195
- expect(wrapped).to respond_to(:call)
196
- expect(wrapped).not_to respond_to(:to_path)
197
- expect(wrapped).to respond_to(:close)
198
- end
199
-
200
- it "passes the stream into the call() of the body" do
201
- fake_rack_stream = double("stream")
202
- expect(fake_body).to receive(:call).with(fake_rack_stream)
203
-
204
- wrapped = described_class.wrap(fake_body, nil_txn)
205
- expect { wrapped.call(fake_rack_stream) }.not_to raise_error
206
- end
207
-
208
- it "sets the exception raised inside call() into the Appsignal transaction" do
209
- fake_rack_stream = double
210
- allow(fake_body).to receive(:call).with(fake_rack_stream).and_raise(Exception.new("Oopsie"))
211
-
212
- txn = double("Appsignal transaction", "nil_transaction?" => false)
213
- expect(txn).to receive(:set_error).once.with(instance_of(Exception))
214
- expect(txn).not_to receive(:complete) # gets called by the caller via close()
215
- wrapped = described_class.wrap(fake_body, txn)
216
-
217
- expect { wrapped.call(fake_rack_stream) }.to raise_error(/Oopsie/)
218
- end
219
- end
220
- end