scout_apm 6.3.0 → 6.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe18e4045997fdf6b731025ee98c13e2702096a0cfd13ac5f0f651d7a7f4a4db
4
- data.tar.gz: d9b4593ef2b8d5df4983d7b93cb9eaf5cc3f7e6916d8288ae9814e59dd913007
3
+ metadata.gz: 97692f03252aa5c2af4a4f87092742319b9088dace2f3dbcb4c48af766dfb848
4
+ data.tar.gz: 30bc39d151abec5d26723b2de0626319cf80685548a0f68da48074e283c72515
5
5
  SHA512:
6
- metadata.gz: a5b838d8e12286496038899c7dd006b6c3d6507061bcba1a963fc10664fa96e36c603f71e315430eda9eeaa84dabadb21f91dee9e9aa9baf7009f6e03d8f3d9d
7
- data.tar.gz: bee9ed547611139d56eb6f88cf6c1e730dc77dbdf575e89a8b1cd95f86756687174c78b64cf00acd389a74e90198f9750aec334e2dfba195340a0a9753c280bd
6
+ metadata.gz: 0c6a06a5e08a3a753a7110568f44d9c9c6b0163aeeabf3be281a2de9df6a2357f14515de7419c9dedd79e8f7e8266dca67cbf58b3dbac2e6efb54f283ba4d433
7
+ data.tar.gz: 13bacea25583bf396a3e4d0edee2e957a756296a3c4fcf6083a5a42b5e0bb2e57c90a9751dd6b04f7d86f0315d8c30222ba4d3d0c9c679aa2d73eb1eb38b2eb3
data/.gitignore CHANGED
@@ -19,3 +19,4 @@ lib/*.bundle
19
19
  lib/*.so
20
20
  log/scout_apm.log
21
21
  gems/*.lock
22
+ gems/vendor/
data/CHANGELOG.markdown CHANGED
@@ -1,5 +1,9 @@
1
1
  # Pending
2
2
 
3
+ # 6.3.1
4
+
5
+ - Capture route names for Grape 4 (#635)
6
+
3
7
  # 6.3.0
4
8
 
5
9
  - Fix span leak on `ActionController::Live` streaming requests (#631)
@@ -10,4 +10,5 @@ gem 'moped'
10
10
  gem 'actionpack'
11
11
  gem 'actionview'
12
12
  gem 'httpx'
13
+ gem 'webmock'
13
14
  gem 'grape'
@@ -7,4 +7,9 @@ gem 'moped'
7
7
  gem 'actionpack'
8
8
  gem 'actionview'
9
9
  gem 'httpx'
10
+ gem 'webmock'
10
11
  gem 'grape'
12
+ # json 3.0 (released 2026-09) dropped the `quirks_mode` keyword that Grape 3's
13
+ # ActiveSupport JSON formatter still passes, so any Grape 3 JSON request raises
14
+ # ArgumentError. Pin to the 2.x line until this matrix moves off Grape 3.
15
+ gem 'json', '< 3'
@@ -1,3 +1,4 @@
1
1
  eval_gemfile("../Gemfile")
2
2
 
3
3
  gem 'typhoeus'
4
+ gem 'webmock'
@@ -51,6 +51,36 @@ module ScoutApm
51
51
  end
52
52
  end
53
53
 
54
+ module GrapeEndpointNaming
55
+ # Grape >= 4 moved endpoint route metadata off the public `options`
56
+ # Hash and onto `Endpoint#config` (a Grape::Endpoint::Options Data
57
+ # object). Support both so the instrument works across Grape 3 and 4.
58
+ def self.name_for(endpoint)
59
+ # `config` is a protected reader on Grape::Endpoint, so a public
60
+ # respond_to? misses it: check with `true` and read via `send`.
61
+ if endpoint.respond_to?(:config, true) && endpoint.send(:config).respond_to?(:http_methods)
62
+ config = endpoint.send(:config)
63
+ method = config.http_methods && config.http_methods.first
64
+ api = config.api || (endpoint.respond_to?(:api) ? endpoint.api : nil)
65
+ path = config.path && config.path.first
66
+ else
67
+ method = endpoint.options[:method] && endpoint.options[:method].first
68
+ api = endpoint.options[:for]
69
+ path = endpoint.options[:path] && endpoint.options[:path].first
70
+ end
71
+
72
+ ["Grape",
73
+ method,
74
+ api.to_s,
75
+ endpoint.namespace.sub(%r{\A/}, ''), # removing leading slashes
76
+ path,
77
+ ].compact.map { |n| n.to_s }.join("/")
78
+ rescue => e
79
+ ScoutApm::Agent.instance.context.logger.info("Error getting Grape Endpoint Name. Error: #{e.message}. Options: #{endpoint.options.inspect}")
80
+ "Grape/Unknown"
81
+ end
82
+ end
83
+
54
84
  module GrapeEndpointInstruments
55
85
  def run_with_scout_instruments(*args)
56
86
  request = ::Grape::Request.new(env || args.first)
@@ -64,17 +94,7 @@ module ScoutApm
64
94
 
65
95
  req.set_headers(request.headers)
66
96
 
67
- begin
68
- name = ["Grape",
69
- self.options[:method].first,
70
- self.options[:for].to_s,
71
- self.namespace.sub(%r{\A/}, ''), # removing leading slashes
72
- self.options[:path].first,
73
- ].compact.map{ |n| n.to_s }.join("/")
74
- rescue => e
75
- ScoutApm::Agent.instance.context.logger.info("Error getting Grape Endpoint Name. Error: #{e.message}. Options: #{self.options.inspect}")
76
- name = "Grape/Unknown"
77
- end
97
+ name = GrapeEndpointNaming.name_for(self)
78
98
 
79
99
  req.start_layer( ScoutApm::Layer.new("Controller", name) )
80
100
  begin
@@ -101,17 +121,7 @@ module ScoutApm
101
121
 
102
122
  req.set_headers(request.headers)
103
123
 
104
- begin
105
- name = ["Grape",
106
- self.options[:method].first,
107
- self.options[:for].to_s,
108
- self.namespace.sub(%r{\A/}, ''), # removing leading slashes
109
- self.options[:path].first,
110
- ].compact.map{ |n| n.to_s }.join("/")
111
- rescue => e
112
- ScoutApm::Agent.instance.context.logger.info("Error getting Grape Endpoint Name. Error: #{e.message}. Options: #{self.options.inspect}")
113
- name = "Grape/Unknown"
114
- end
124
+ name = GrapeEndpointNaming.name_for(self)
115
125
 
116
126
  req.start_layer( ScoutApm::Layer.new("Controller", name) )
117
127
  begin
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "6.3.0"
2
+ VERSION = "6.3.1"
3
3
  end
@@ -53,6 +53,31 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
53
53
  assert_includes body_content, 'hello'
54
54
  end
55
55
 
56
+ # Grape >= 4 moved endpoint route metadata (method/path/api) off the
57
+ # public `options` Hash and onto the protected `Endpoint#config` object.
58
+ # Reading the old `options[...]` keys under Grape 4 silently returns nil,
59
+ # so every request gets named "Grape/Unknown" instead of the real
60
+ # endpoint name.
61
+ def test_endpoint_name_reflects_the_route
62
+ captured_names = []
63
+ original_new = ScoutApm::Layer.method(:new)
64
+ ScoutApm::Layer.define_singleton_method(:new) do |type, name, *rest|
65
+ captured_names << name if type == "Controller"
66
+ original_new.call(type, name, *rest)
67
+ end
68
+
69
+ begin
70
+ status, _headers, _body = HelloAPI.call(Rack::MockRequest.env_for('/hello'))
71
+ ensure
72
+ ScoutApm::Layer.define_singleton_method(:new, original_new)
73
+ end
74
+
75
+ assert_equal 200, status
76
+ refute_empty captured_names
77
+ refute_equal "Grape/Unknown", captured_names.first
78
+ assert_equal "Grape/GET/#{HelloAPI}//hello", captured_names.first
79
+ end
80
+
56
81
  # Either instrumentation method is valid: alias_method is the default,
57
82
  # prepend is used when configured (`use_prepend`/`prepend_instruments`)
58
83
  # or when the instrument falls back to it for safety.
@@ -4,17 +4,34 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
4
4
  require 'scout_apm/instruments/httpx'
5
5
 
6
6
  require 'httpx'
7
+ require 'webmock'
7
8
 
8
9
  class HTTPXTest < Minitest::Test
10
+ include WebMock::API
11
+
9
12
  def setup
13
+ super # clears Thread.current[:scout_request] so a stale request from a
14
+ # prior test can't swallow this test's layers
15
+ WebMock.enable!
16
+ WebMock.disable_net_connect!
17
+
10
18
  @context = ScoutApm::AgentContext.new
11
19
  @recorder = FakeRecorder.new
12
20
  ScoutApm::Agent.instance.context.recorder = @recorder
13
21
  ScoutApm::Instruments::HTTPX.new(@context).install(prepend: false)
14
22
  end
15
23
 
24
+ def teardown
25
+ WebMock.reset!
26
+ WebMock.allow_net_connect!
27
+ WebMock.disable!
28
+ super
29
+ end
16
30
 
17
31
  def test_httpx
32
+ stub_request(:get, /news\.ycombinator\.com/).to_return(status: 200, body: "")
33
+ stub_request(:get, /google\.com/).to_return(status: 200, body: "")
34
+
18
35
  responses = HTTPX.get(
19
36
  "https://news.ycombinator.com/news",
20
37
  "https://news.ycombinator.com/news?p=2",
@@ -27,11 +44,18 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
27
44
  end
28
45
 
29
46
  def test_httpx_post_request
47
+ stub_request(:post, /httpbin\.org\/post/).to_return(status: 200, body: "")
48
+
30
49
  HTTPX.post("https://httpbin.org/post", json: { test: "data" })
31
50
  assert_recorded(@recorder, "HTTP", "POST", "httpbin.org/post")
32
51
  end
33
52
 
34
53
  def test_instruments_httpx_error_handling
54
+ # An error response still flows through Session#request, so the layer is
55
+ # started and stopped, and the request is recorded exactly once.
56
+ stub_request(:get, /thisshouldnotexistatall12345\.com/)
57
+ .to_return(status: 500, body: "")
58
+
35
59
  begin
36
60
  HTTPX.get("https://thisshouldnotexistatall12345.com")
37
61
  rescue
@@ -41,6 +65,10 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
41
65
  end
42
66
 
43
67
  def test_httpx_request_retry
68
+ # A timed-out request is still dispatched through Session#request, so the
69
+ # layer is recorded even though the response errors.
70
+ stub_request(:get, /httpbin\.org\/delay/).to_timeout
71
+
44
72
  begin
45
73
  HTTPX.with(timeout: { connect_timeout: 0.25, request_timeout: 0.25 })
46
74
  .get("https://httpbin.org/delay/5")
@@ -50,14 +78,17 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
50
78
  end
51
79
 
52
80
  def test_multiple_plugins
81
+ stub_request(:get, /news\.ycombinator\.com/).to_return(status: 200, body: "")
82
+ stub_request(:get, "http://httpbin.org/redirect/2")
83
+ .to_return(status: 302, headers: { "Location" => "http://httpbin.org/redirect/1" })
84
+ stub_request(:get, "http://httpbin.org/redirect/1")
85
+ .to_return(status: 302, headers: { "Location" => "http://httpbin.org/get" })
86
+ stub_request(:get, "http://httpbin.org/get").to_return(status: 200, body: "")
87
+
53
88
  session = HTTPX.plugin(:persistent).plugin(:follow_redirects)
54
89
 
55
90
  session.get("https://news.ycombinator.com/news")
56
- begin
57
- session.get("http://httpbin.org/redirect/2")
58
- rescue
59
- skip "httpbin.org not available"
60
- end
91
+ session.get("http://httpbin.org/redirect/2")
61
92
 
62
93
  assert_equal 2, @recorder.requests.length, "Expected 2 requests to be recorded"
63
94
  end
@@ -4,15 +4,31 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("typhoeus")
4
4
  require 'scout_apm/instruments/typhoeus'
5
5
 
6
6
  require 'typhoeus'
7
+ require 'webmock'
7
8
 
8
9
  class TyphoeusTest < Minitest::Test
10
+ include WebMock::API
11
+
9
12
  def setup
13
+ super # clears Thread.current[:scout_request] so a stale request from a
14
+ # prior test can't swallow this test's layers
15
+ WebMock.enable!
16
+ WebMock.disable_net_connect!
17
+ stub_request(:any, /example\.com/).to_return(status: 200, body: "")
18
+
10
19
  @context = ScoutApm::AgentContext.new
11
20
  @recorder = FakeRecorder.new
12
21
  ScoutApm::Agent.instance.context.recorder = @recorder
13
22
  ScoutApm::Instruments::Typhoeus.new(@context).install(prepend: false)
14
23
  end
15
24
 
25
+ def teardown
26
+ WebMock.reset!
27
+ WebMock.allow_net_connect!
28
+ WebMock.disable!
29
+ super
30
+ end
31
+
16
32
  def test_instruments_typhoeus_hydra
17
33
  hydra = Typhoeus::Hydra.new
18
34
  2.times.map{ hydra.queue(Typhoeus::Request.new("example.com", followlocation: true)) }
@@ -39,4 +55,4 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("typhoeus")
39
55
  assert_equal desc, req.root_layer.desc if !desc.nil?
40
56
  end
41
57
  end
42
- end
58
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.0
4
+ version: 6.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -523,7 +523,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
523
523
  - !ruby/object:Gem::Version
524
524
  version: '0'
525
525
  requirements: []
526
- rubygems_version: 4.0.16
526
+ rubygems_version: 4.0.20
527
527
  specification_version: 4
528
528
  summary: Ruby application performance monitoring
529
529
  test_files: []