scout_apm 5.7.1 → 6.0.0
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 +4 -4
- data/.github/workflows/test.yml +2 -0
- data/CHANGELOG.markdown +21 -1
- data/README.markdown +20 -8
- data/gems/instruments.gemfile +1 -0
- data/lib/scout_apm/auto_instrument/instruction_sequence.rb +2 -1
- data/lib/scout_apm/auto_instrument/parser.rb +150 -2
- data/lib/scout_apm/auto_instrument/prism.rb +357 -0
- data/lib/scout_apm/auto_instrument/rails.rb +9 -155
- data/lib/scout_apm/auto_instrument/requirements.rb +11 -0
- data/lib/scout_apm/background_job_integrations/delayed_job.rb +25 -1
- data/lib/scout_apm/background_job_integrations/faktory.rb +7 -1
- data/lib/scout_apm/background_job_integrations/good_job.rb +7 -1
- data/lib/scout_apm/background_job_integrations/legacy_sneakers.rb +7 -1
- data/lib/scout_apm/background_job_integrations/que.rb +7 -1
- data/lib/scout_apm/background_job_integrations/shoryuken.rb +7 -1
- data/lib/scout_apm/background_job_integrations/sidekiq.rb +89 -1
- data/lib/scout_apm/background_job_integrations/sneakers.rb +7 -1
- data/lib/scout_apm/background_job_integrations/solid_queue.rb +19 -1
- data/lib/scout_apm/config.rb +32 -7
- data/lib/scout_apm/context.rb +3 -1
- data/lib/scout_apm/error_service/error_record.rb +5 -1
- data/lib/scout_apm/instrument_manager.rb +2 -0
- data/lib/scout_apm/instruments/http_client.rb +10 -0
- data/lib/scout_apm/instruments/httpx.rb +119 -0
- data/lib/scout_apm/instruments/opensearch.rb +131 -0
- data/lib/scout_apm/limited_layer.rb +5 -2
- data/lib/scout_apm/logger.rb +1 -1
- data/lib/scout_apm/sampling.rb +25 -13
- data/lib/scout_apm/server_integrations/puma.rb +21 -4
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +9 -4
- data/test/unit/auto_instrument/controller-ast.prism.txt +1015 -0
- data/test/unit/auto_instrument/controller-instrumented.rb +36 -11
- data/test/unit/auto_instrument/controller.rb +25 -0
- data/test/unit/auto_instrument/hash_shorthand_controller-instrumented.rb +28 -10
- data/test/unit/auto_instrument/hash_shorthand_controller.rb +19 -1
- data/test/unit/auto_instrument_test.rb +7 -1
- data/test/unit/background_job_integrations/faktory_test.rb +109 -0
- data/test/unit/background_job_integrations/shoryuken_test.rb +81 -0
- data/test/unit/background_job_integrations/sidekiq_test.rb +38 -0
- data/test/unit/config_test.rb +14 -0
- data/test/unit/error_service/error_buffer_test.rb +32 -0
- data/test/unit/error_test.rb +3 -3
- data/test/unit/ignored_uris_test.rb +7 -0
- data/test/unit/instruments/http_client_test.rb +0 -2
- data/test/unit/instruments/httpx_test.rb +78 -0
- data/test/unit/limited_layer_test.rb +4 -4
- data/test/unit/sampling_test.rb +10 -10
- metadata +10 -3
- data/lib/scout_apm/utils/time.rb +0 -12
- /data/test/unit/auto_instrument/{controller-ast.txt → controller-ast.parser.txt} +0 -0
|
@@ -10,40 +10,65 @@ class ClientsController < ApplicationController
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def new
|
|
14
|
+
::ScoutApm::AutoInstrument("super do |something|...",["ROOT/test/unit/auto_instrument/controller.rb:14:in `new'"]){super do |something|
|
|
15
|
+
@client = Client.new
|
|
16
|
+
end}
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def create
|
|
14
|
-
@client = ::ScoutApm::AutoInstrument("Client.new(params[:client])",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
15
|
-
if ::ScoutApm::AutoInstrument("@client.save",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
16
|
-
::ScoutApm::AutoInstrument("redirect_to @client",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
20
|
+
@client = ::ScoutApm::AutoInstrument("Client.new(params[:client])",["ROOT/test/unit/auto_instrument/controller.rb:20:in `create'"]){Client.new(params[:client])}
|
|
21
|
+
if ::ScoutApm::AutoInstrument("@client.save",["ROOT/test/unit/auto_instrument/controller.rb:21:in `create'"]){@client.save}
|
|
22
|
+
::ScoutApm::AutoInstrument("redirect_to @client",["ROOT/test/unit/auto_instrument/controller.rb:22:in `create'"]){redirect_to @client}
|
|
17
23
|
else
|
|
18
24
|
# This line overrides the default rendering behavior, which
|
|
19
25
|
# would have been to render the "create" view.
|
|
20
|
-
::ScoutApm::AutoInstrument("render \"new\"",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
26
|
+
::ScoutApm::AutoInstrument("render \"new\"",["ROOT/test/unit/auto_instrument/controller.rb:26:in `create'"]){render "new"}
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def edit
|
|
25
|
-
@client = ::ScoutApm::AutoInstrument("Client.new(params[:client])",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
31
|
+
@client = ::ScoutApm::AutoInstrument("Client.new(params[:client])",["ROOT/test/unit/auto_instrument/controller.rb:31:in `edit'"]){Client.new(params[:client])}
|
|
26
32
|
|
|
27
|
-
if ::ScoutApm::AutoInstrument("request.post?",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
28
|
-
::ScoutApm::AutoInstrument("@client.transaction do...",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
33
|
+
if ::ScoutApm::AutoInstrument("request.post?",["ROOT/test/unit/auto_instrument/controller.rb:33:in `edit'"]){request.post?}
|
|
34
|
+
::ScoutApm::AutoInstrument("@client.transaction do...",["ROOT/test/unit/auto_instrument/controller.rb:34:in `edit'"]){@client.transaction do
|
|
29
35
|
@client.update_attributes(params[:client])
|
|
30
36
|
end}
|
|
31
37
|
end
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def data
|
|
35
|
-
@clients = ::ScoutApm::AutoInstrument("Client.all",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
41
|
+
@clients = ::ScoutApm::AutoInstrument("Client.all",["ROOT/test/unit/auto_instrument/controller.rb:41:in `data'"]){Client.all}
|
|
36
42
|
|
|
37
|
-
formatter = ::ScoutApm::AutoInstrument("proc do |row|...",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
43
|
+
formatter = ::ScoutApm::AutoInstrument("proc do |row|...",["ROOT/test/unit/auto_instrument/controller.rb:43:in `data'"]){proc do |row|
|
|
38
44
|
row.to_json
|
|
39
45
|
end}
|
|
40
46
|
|
|
41
|
-
::ScoutApm::AutoInstrument("respond_with @clients.each(&formatter).join(\"\\n\"), :content_type => 'application/json; boundary=NL'",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
47
|
+
::ScoutApm::AutoInstrument("respond_with @clients.each(&formatter).join(\"\\n\"), :content_type => 'application/json; boundary=NL'",["ROOT/test/unit/auto_instrument/controller.rb:47:in `data'"]){respond_with @clients.each(&formatter).join("\n"), :content_type => 'application/json; boundary=NL'}
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
def things
|
|
45
51
|
x = {}
|
|
46
52
|
x[:this] ||= 'foo'
|
|
47
|
-
x[:that] &&= ::ScoutApm::AutoInstrument("'foo'.size",["ROOT/test/unit/auto_instrument/controller.rb:
|
|
53
|
+
x[:that] &&= ::ScoutApm::AutoInstrument("'foo'.size",["ROOT/test/unit/auto_instrument/controller.rb:53:in `things'"]){'foo'.size}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def do_something
|
|
57
|
+
::ScoutApm::AutoInstrument("wrap_call(\"123\",...",["ROOT/test/unit/auto_instrument/controller.rb:57:in `do_something'"]){wrap_call("123",
|
|
58
|
+
something: -> { puts "Do something" }
|
|
59
|
+
) do
|
|
60
|
+
|
|
61
|
+
raw_data = '{ "key": "123" }'
|
|
62
|
+
|
|
63
|
+
payload = begin
|
|
64
|
+
Marshal.load(raw_data)
|
|
65
|
+
rescue
|
|
66
|
+
puts 'Failed with bad/unhelpful error message'
|
|
67
|
+
end
|
|
68
|
+
end}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def wrap_call(*args, **kwargs)
|
|
72
|
+
yield
|
|
48
73
|
end
|
|
49
74
|
end
|
|
@@ -10,6 +10,12 @@ class ClientsController < ApplicationController
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def new
|
|
14
|
+
super do |something|
|
|
15
|
+
@client = Client.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def create
|
|
14
20
|
@client = Client.new(params[:client])
|
|
15
21
|
if @client.save
|
|
@@ -46,4 +52,23 @@ class ClientsController < ApplicationController
|
|
|
46
52
|
x[:this] ||= 'foo'
|
|
47
53
|
x[:that] &&= 'foo'.size
|
|
48
54
|
end
|
|
55
|
+
|
|
56
|
+
def do_something
|
|
57
|
+
wrap_call("123",
|
|
58
|
+
something: -> { puts "Do something" }
|
|
59
|
+
) do
|
|
60
|
+
|
|
61
|
+
raw_data = '{ "key": "123" }'
|
|
62
|
+
|
|
63
|
+
payload = begin
|
|
64
|
+
Marshal.load(raw_data)
|
|
65
|
+
rescue
|
|
66
|
+
puts 'Failed with bad/unhelpful error message'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def wrap_call(*args, **kwargs)
|
|
72
|
+
yield
|
|
73
|
+
end
|
|
49
74
|
end
|
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
|
|
2
2
|
class HashShorthandController < ApplicationController
|
|
3
3
|
def hash
|
|
4
|
+
::ScoutApm::AutoInstrument("THREAD.current[:ternary_check] = true",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:4:in `hash'"]){THREAD.current[:ternary_check] = true}
|
|
4
5
|
json = {
|
|
5
6
|
static: "static",
|
|
6
|
-
shorthand: ::ScoutApm::AutoInstrument("shorthand:",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
7
|
-
longhand: ::ScoutApm::AutoInstrument("longhand: longhand",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
8
|
-
longhand_different_key: ::ScoutApm::AutoInstrument("longhand",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
9
|
-
hash_rocket: ::ScoutApm::AutoInstrument(":hash_rocket => hash_rocket",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
10
|
-
:hash_rocket_different_key => ::ScoutApm::AutoInstrument("hash_rocket",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
11
|
-
non_nil_receiver: ::ScoutApm::AutoInstrument("non_nil_receiver.value",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
7
|
+
shorthand: ::ScoutApm::AutoInstrument("shorthand:",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:7:in `hash'"]){shorthand},
|
|
8
|
+
longhand: ::ScoutApm::AutoInstrument("longhand: longhand",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:8:in `hash'"]){longhand},
|
|
9
|
+
longhand_different_key: ::ScoutApm::AutoInstrument("longhand",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:9:in `hash'"]){longhand},
|
|
10
|
+
hash_rocket: ::ScoutApm::AutoInstrument(":hash_rocket => hash_rocket",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:10:in `hash'"]){hash_rocket},
|
|
11
|
+
:hash_rocket_different_key => ::ScoutApm::AutoInstrument("hash_rocket",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:11:in `hash'"]){hash_rocket},
|
|
12
|
+
non_nil_receiver: ::ScoutApm::AutoInstrument("non_nil_receiver.value",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:12:in `hash'"]){non_nil_receiver.value},
|
|
12
13
|
nested: {
|
|
13
|
-
shorthand: ::ScoutApm::AutoInstrument("shorthand:",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
14
|
+
shorthand: ::ScoutApm::AutoInstrument("shorthand:",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:14:in `hash'"]){shorthand},
|
|
14
15
|
},
|
|
15
|
-
nested_call: ::ScoutApm::AutoInstrument("nested_call(params[\"timestamp\"])",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
16
|
+
nested_call: ::ScoutApm::AutoInstrument("nested_call(params[\"timestamp\"])",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:16:in `hash'"]){nested_call(params["timestamp"])},
|
|
17
|
+
nested_with_ternaries: {
|
|
18
|
+
truthy: ::ScoutApm::AutoInstrument("THREAD.current[:ternary_check] == true",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:18:in `hash'"]){THREAD.current[:ternary_check] == true} ? 1 : 0,
|
|
19
|
+
falsy: ::ScoutApm::AutoInstrument("THREAD.current[:ternary_check] == false",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:19:in `hash'"]){THREAD.current[:ternary_check] == false} ? 1 : 0,
|
|
20
|
+
},
|
|
21
|
+
ternary: ::ScoutApm::AutoInstrument("ternary",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:21:in `hash'"]){ternary} ? ::ScoutApm::AutoInstrument("ternary",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:21:in `hash'"]){ternary} : nil,
|
|
16
22
|
}
|
|
17
|
-
::ScoutApm::AutoInstrument("render json:",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
23
|
+
::ScoutApm::AutoInstrument("render json:",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:23:in `hash'"]){render json:}
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
private
|
|
21
27
|
|
|
28
|
+
def simple_method
|
|
29
|
+
"simple"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def inner_method
|
|
33
|
+
"inner"
|
|
34
|
+
end
|
|
35
|
+
|
|
22
36
|
def nested_call(noop)
|
|
23
37
|
noop
|
|
24
38
|
end
|
|
25
39
|
|
|
40
|
+
def ternary
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
26
44
|
def shorthand
|
|
27
45
|
"shorthand"
|
|
28
46
|
end
|
|
@@ -36,6 +54,6 @@ class HashShorthandController < ApplicationController
|
|
|
36
54
|
end
|
|
37
55
|
|
|
38
56
|
def non_nil_receiver
|
|
39
|
-
::ScoutApm::AutoInstrument("OpenStruct.new(value: \"value\")",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:
|
|
57
|
+
::ScoutApm::AutoInstrument("OpenStruct.new(value: \"value\")",["ROOT/test/unit/auto_instrument/hash_shorthand_controller.rb:57:in `non_nil_receiver'"]){OpenStruct.new(value: "value")}
|
|
40
58
|
end
|
|
41
59
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
class HashShorthandController < ApplicationController
|
|
3
3
|
def hash
|
|
4
|
+
THREAD.current[:ternary_check] = true
|
|
4
5
|
json = {
|
|
5
6
|
static: "static",
|
|
6
7
|
shorthand:,
|
|
@@ -12,17 +13,34 @@ class HashShorthandController < ApplicationController
|
|
|
12
13
|
nested: {
|
|
13
14
|
shorthand:,
|
|
14
15
|
},
|
|
15
|
-
nested_call: nested_call(params["timestamp"])
|
|
16
|
+
nested_call: nested_call(params["timestamp"]),
|
|
17
|
+
nested_with_ternaries: {
|
|
18
|
+
truthy: THREAD.current[:ternary_check] == true ? 1 : 0,
|
|
19
|
+
falsy: THREAD.current[:ternary_check] == false ? 1 : 0,
|
|
20
|
+
},
|
|
21
|
+
ternary: ternary ? ternary : nil,
|
|
16
22
|
}
|
|
17
23
|
render json:
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
private
|
|
21
27
|
|
|
28
|
+
def simple_method
|
|
29
|
+
"simple"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def inner_method
|
|
33
|
+
"inner"
|
|
34
|
+
end
|
|
35
|
+
|
|
22
36
|
def nested_call(noop)
|
|
23
37
|
noop
|
|
24
38
|
end
|
|
25
39
|
|
|
40
|
+
def ternary
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
26
44
|
def shorthand
|
|
27
45
|
"shorthand"
|
|
28
46
|
end
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'prism'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
end
|
|
5
|
+
|
|
1
6
|
require 'test_helper'
|
|
2
7
|
|
|
3
8
|
require 'scout_apm/auto_instrument'
|
|
@@ -21,7 +26,8 @@ class AutoInstrumentTest < Minitest::Test
|
|
|
21
26
|
# test controller.rb file, which will be different on different environments.
|
|
22
27
|
# This normalizes backtraces across environments.
|
|
23
28
|
def normalize_backtrace(string)
|
|
24
|
-
|
|
29
|
+
# Keep tests simple and just use UTF-8 encoding.
|
|
30
|
+
result = string.gsub(ROOT, "ROOT").force_encoding("UTF-8")
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
# Use this to automatically update the test fixtures.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'scout_apm/background_job_integrations/faktory'
|
|
3
|
+
|
|
4
|
+
class FaktoryTest < Minitest::Test
|
|
5
|
+
FaktoryMiddleware = ScoutApm::BackgroundJobIntegrations::FaktoryMiddleware
|
|
6
|
+
|
|
7
|
+
def test_middleware_call_job_exception_with_error_monitoring
|
|
8
|
+
# Test that error buffer is called on exception
|
|
9
|
+
fake_request = mock
|
|
10
|
+
fake_request.expects(:annotate_request)
|
|
11
|
+
fake_request.expects(:start_layer).twice
|
|
12
|
+
fake_request.expects(:stop_layer).twice
|
|
13
|
+
fake_request.expects(:error!)
|
|
14
|
+
|
|
15
|
+
fake_context = mock
|
|
16
|
+
fake_error_buffer = mock
|
|
17
|
+
fake_context.expects(:error_buffer).returns(fake_error_buffer)
|
|
18
|
+
|
|
19
|
+
expected_env = {
|
|
20
|
+
:custom_controller => "TestJob",
|
|
21
|
+
:custom_action => "critical"
|
|
22
|
+
}
|
|
23
|
+
fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
|
|
24
|
+
|
|
25
|
+
ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
|
|
26
|
+
ScoutApm::Agent.instance.expects(:context).returns(fake_context)
|
|
27
|
+
|
|
28
|
+
worker_instance = mock
|
|
29
|
+
job = {
|
|
30
|
+
"queue" => "critical",
|
|
31
|
+
"jobtype" => "TestJob",
|
|
32
|
+
"enqueued_at" => Time.now.iso8601
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
assert_raises RuntimeError do
|
|
36
|
+
FaktoryMiddleware.new.call(worker_instance, job) do
|
|
37
|
+
raise RuntimeError, "Job failed"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_middleware_call_activejob_wrapper
|
|
43
|
+
# Test ActiveJob job class extraction
|
|
44
|
+
fake_request = mock
|
|
45
|
+
fake_request.expects(:annotate_request)
|
|
46
|
+
fake_request.expects(:start_layer).twice
|
|
47
|
+
fake_request.expects(:stop_layer).twice
|
|
48
|
+
fake_request.expects(:error!)
|
|
49
|
+
|
|
50
|
+
fake_context = mock
|
|
51
|
+
fake_error_buffer = mock
|
|
52
|
+
fake_context.expects(:error_buffer).returns(fake_error_buffer)
|
|
53
|
+
|
|
54
|
+
expected_env = {
|
|
55
|
+
:custom_controller => "MyRealJob", # Should extract from custom.wrapped
|
|
56
|
+
:custom_action => "default"
|
|
57
|
+
}
|
|
58
|
+
fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
|
|
59
|
+
|
|
60
|
+
ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
|
|
61
|
+
ScoutApm::Agent.instance.expects(:context).returns(fake_context)
|
|
62
|
+
|
|
63
|
+
# ActiveJob wrapper scenario
|
|
64
|
+
worker_instance = mock
|
|
65
|
+
job = {
|
|
66
|
+
"queue" => "default",
|
|
67
|
+
"jobtype" => "ActiveJob::QueueAdapters::FaktoryAdapter::JobWrapper",
|
|
68
|
+
"custom" => { "wrapped" => "MyRealJob" },
|
|
69
|
+
"created_at" => Time.now.iso8601
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
assert_raises RuntimeError do
|
|
73
|
+
FaktoryMiddleware.new.call(worker_instance, job) do
|
|
74
|
+
raise RuntimeError, "ActiveJob failed"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_middleware_call_missing_queue_fallback
|
|
80
|
+
# Test behavior when queue is missing
|
|
81
|
+
fake_request = mock
|
|
82
|
+
fake_request.expects(:annotate_request)
|
|
83
|
+
fake_request.expects(:start_layer).twice
|
|
84
|
+
fake_request.expects(:stop_layer).twice
|
|
85
|
+
fake_request.expects(:error!)
|
|
86
|
+
|
|
87
|
+
fake_context = mock
|
|
88
|
+
fake_error_buffer = mock
|
|
89
|
+
fake_context.expects(:error_buffer).returns(fake_error_buffer)
|
|
90
|
+
|
|
91
|
+
expected_env = {
|
|
92
|
+
:custom_controller => "UnknownJob", # Fallback when jobtype missing
|
|
93
|
+
:custom_action => nil # No queue
|
|
94
|
+
}
|
|
95
|
+
fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
|
|
96
|
+
|
|
97
|
+
ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
|
|
98
|
+
ScoutApm::Agent.instance.expects(:context).returns(fake_context)
|
|
99
|
+
|
|
100
|
+
worker_instance = mock
|
|
101
|
+
job = {} # Empty job data
|
|
102
|
+
|
|
103
|
+
assert_raises RuntimeError do
|
|
104
|
+
FaktoryMiddleware.new.call(worker_instance, job) do
|
|
105
|
+
raise RuntimeError, "Job failed"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'scout_apm/background_job_integrations/shoryuken'
|
|
3
|
+
|
|
4
|
+
class ShoryukenTest < Minitest::Test
|
|
5
|
+
ShoryukenIntegration = ScoutApm::BackgroundJobIntegrations::Shoryuken
|
|
6
|
+
ShoryukenMiddleware = ScoutApm::BackgroundJobIntegrations::ShoryukenMiddleware
|
|
7
|
+
|
|
8
|
+
def test_middleware_call_job_exception_with_error_monitoring
|
|
9
|
+
# Test that error buffer is called on exception
|
|
10
|
+
fake_request = mock
|
|
11
|
+
fake_request.expects(:annotate_request)
|
|
12
|
+
fake_request.expects(:start_layer).twice
|
|
13
|
+
fake_request.expects(:stop_layer).twice
|
|
14
|
+
fake_request.expects(:error!)
|
|
15
|
+
|
|
16
|
+
fake_context = mock
|
|
17
|
+
fake_error_buffer = mock
|
|
18
|
+
fake_context.expects(:error_buffer).returns(fake_error_buffer)
|
|
19
|
+
|
|
20
|
+
expected_env = {
|
|
21
|
+
:custom_controller => "TestWorker",
|
|
22
|
+
:custom_action => "test-queue"
|
|
23
|
+
}
|
|
24
|
+
fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
|
|
25
|
+
|
|
26
|
+
ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
|
|
27
|
+
ScoutApm::Agent.instance.expects(:context).returns(fake_context)
|
|
28
|
+
|
|
29
|
+
worker_instance = mock
|
|
30
|
+
mock_class = mock
|
|
31
|
+
mock_class.expects(:to_s).twice.returns("TestWorker")
|
|
32
|
+
worker_instance.expects(:class).twice.returns(mock_class)
|
|
33
|
+
queue = "test-queue"
|
|
34
|
+
msg = mock
|
|
35
|
+
msg.expects(:attributes).returns({'SentTimestamp' => '1534873927868'})
|
|
36
|
+
body = {}
|
|
37
|
+
|
|
38
|
+
assert_raises RuntimeError do
|
|
39
|
+
ShoryukenMiddleware.new.call(worker_instance, queue, msg, body) do
|
|
40
|
+
raise RuntimeError, "Job failed"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_middleware_call_activejob_wrapper
|
|
46
|
+
# Test ActiveJob job class extraction
|
|
47
|
+
fake_request = mock
|
|
48
|
+
fake_request.expects(:annotate_request)
|
|
49
|
+
fake_request.expects(:start_layer).twice
|
|
50
|
+
fake_request.expects(:stop_layer).twice
|
|
51
|
+
fake_request.expects(:error!)
|
|
52
|
+
|
|
53
|
+
fake_context = mock
|
|
54
|
+
fake_error_buffer = mock
|
|
55
|
+
fake_context.expects(:error_buffer).returns(fake_error_buffer)
|
|
56
|
+
|
|
57
|
+
expected_env = {
|
|
58
|
+
:custom_controller => "MyRealJob", # Should extract from body
|
|
59
|
+
:custom_action => "priority-queue"
|
|
60
|
+
}
|
|
61
|
+
fake_error_buffer.expects(:capture).with(kind_of(RuntimeError), expected_env)
|
|
62
|
+
|
|
63
|
+
ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
|
|
64
|
+
ScoutApm::Agent.instance.expects(:context).returns(fake_context)
|
|
65
|
+
|
|
66
|
+
worker_instance = mock
|
|
67
|
+
mock_class = mock
|
|
68
|
+
mock_class.expects(:to_s).returns("ActiveJob::QueueAdapters::ShoryukenAdapter::JobWrapper")
|
|
69
|
+
worker_instance.expects(:class).returns(mock_class)
|
|
70
|
+
queue = "priority-queue"
|
|
71
|
+
msg = mock
|
|
72
|
+
msg.expects(:attributes).returns({'SentTimestamp' => '1534873927868'})
|
|
73
|
+
body = { "job_class" => "MyRealJob" }
|
|
74
|
+
|
|
75
|
+
assert_raises RuntimeError do
|
|
76
|
+
ShoryukenMiddleware.new.call(worker_instance, queue, msg, body) do
|
|
77
|
+
raise RuntimeError, "ActiveJob failed"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -125,4 +125,42 @@ class SidekiqTest < Minitest::Test
|
|
|
125
125
|
msg = {}
|
|
126
126
|
assert_equal 0, SidekiqMiddleware.new.latency(msg, 200)
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
########################################
|
|
130
|
+
# Job Arguments Capture
|
|
131
|
+
########################################
|
|
132
|
+
def test_capture_job_args_when_enabled_with_valid_job
|
|
133
|
+
test_job_class = Class.new do
|
|
134
|
+
def self.name
|
|
135
|
+
'TestJobWithArgs'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def perform(user_id, email, filter_param, dict_val)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Object.const_set('TestJobWithArgs', test_job_class)
|
|
143
|
+
|
|
144
|
+
SidekiqMiddleware.any_instance.stubs(:capture_job_args?).returns(true)
|
|
145
|
+
SidekiqMiddleware.any_instance.stubs(:filtered_params_config).returns(["filter_param"])
|
|
146
|
+
|
|
147
|
+
fake_request = mock
|
|
148
|
+
fake_request.expects(:annotate_request)
|
|
149
|
+
fake_request.expects(:start_layer).twice
|
|
150
|
+
fake_request.expects(:stop_layer).twice
|
|
151
|
+
|
|
152
|
+
ScoutApm::RequestManager.stubs(:lookup).returns(fake_request)
|
|
153
|
+
ScoutApm::Context.expects(:add).with({ user_id: 123, email: 'test@example.com', filter_param: '[FILTERED]', dict_val: '[UNSUPPORTED TYPE]' })
|
|
154
|
+
|
|
155
|
+
msg = {
|
|
156
|
+
'class' => 'TestJobWithArgs',
|
|
157
|
+
'args' => [{ 'arguments' => [123, 'test@example.com', 'secret', {a: 1}] }]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
block_called = false
|
|
161
|
+
SidekiqMiddleware.new.call(nil, msg, "defaultqueue") { block_called = true }
|
|
162
|
+
assert block_called
|
|
163
|
+
|
|
164
|
+
Object.send(:remove_const, 'TestJobWithArgs')
|
|
165
|
+
end
|
|
128
166
|
end
|
data/test/unit/config_test.rb
CHANGED
|
@@ -92,6 +92,20 @@ class ConfigTest < Minitest::Test
|
|
|
92
92
|
assert_nil coercion.coerce(nil)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
def test_sample_rate_coercion
|
|
96
|
+
coercion = ScoutApm::Config::SampleRateCoercion.new
|
|
97
|
+
assert_in_delta 1.0, coercion.coerce("1")
|
|
98
|
+
assert_in_delta 0.015, coercion.coerce("1.5")
|
|
99
|
+
assert_in_delta 1.0, coercion.coerce(1)
|
|
100
|
+
assert_in_delta 0.015, coercion.coerce(1.5)
|
|
101
|
+
assert_in_delta 0.0, coercion.coerce("0")
|
|
102
|
+
assert_in_delta 0.0, coercion.coerce(0)
|
|
103
|
+
assert_in_delta 0.0, coercion.coerce("")
|
|
104
|
+
assert_in_delta 0.0, coercion.coerce(nil)
|
|
105
|
+
assert_in_delta 0.5, coercion.coerce("0.5")
|
|
106
|
+
assert_in_delta 0, coercion.coerce("-2.5")
|
|
107
|
+
end
|
|
108
|
+
|
|
95
109
|
def test_any_keys_found
|
|
96
110
|
ENV.stubs(:has_key?).returns(nil)
|
|
97
111
|
|
|
@@ -25,6 +25,7 @@ class ErrorBufferTest < Minitest::Test
|
|
|
25
25
|
|
|
26
26
|
exception = exceptions[0]
|
|
27
27
|
expected_env_keys = [
|
|
28
|
+
"REQUEST_METHOD",
|
|
28
29
|
"ANOTHER_HEADER",
|
|
29
30
|
"HTTP_X_FORWARDED_FOR",
|
|
30
31
|
"HTTP_USER_AGENT",
|
|
@@ -67,4 +68,35 @@ class ErrorBufferTest < Minitest::Test
|
|
|
67
68
|
def ex(msg="Whoops")
|
|
68
69
|
FakeError.new(msg)
|
|
69
70
|
end
|
|
71
|
+
|
|
72
|
+
def test_user_context_is_flattened_with_user_prefix
|
|
73
|
+
config = make_fake_config(
|
|
74
|
+
'errors_enabled' => true,
|
|
75
|
+
'errors_env_capture' => %w()
|
|
76
|
+
)
|
|
77
|
+
test_context = ScoutApm::AgentContext.new().tap { |c| c.config = config }
|
|
78
|
+
|
|
79
|
+
ScoutApm::Agent.instance.stub(:context, test_context) do
|
|
80
|
+
eb = ScoutApm::ErrorService::ErrorBuffer.new(test_context)
|
|
81
|
+
|
|
82
|
+
# Set user context using add_user
|
|
83
|
+
ScoutApm::Context.add_user(id: 2)
|
|
84
|
+
# Set extra context using add
|
|
85
|
+
ScoutApm::Context.add(organization: 3)
|
|
86
|
+
|
|
87
|
+
eb.capture(ex, env)
|
|
88
|
+
exceptions = eb.instance_variable_get(:@error_records)
|
|
89
|
+
|
|
90
|
+
assert_equal 1, exceptions.length
|
|
91
|
+
exception = exceptions[0]
|
|
92
|
+
|
|
93
|
+
# User context should be flattened with "user_" prefix
|
|
94
|
+
assert_equal 2, exception.context["user_id"]
|
|
95
|
+
# Extra context should remain as-is
|
|
96
|
+
assert_equal 3, exception.context[:organization]
|
|
97
|
+
|
|
98
|
+
# Should NOT have nested :user key
|
|
99
|
+
refute exception.context.key?(:user)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
70
102
|
end
|
data/test/unit/error_test.rb
CHANGED
|
@@ -55,7 +55,7 @@ class ErrorTest < Minitest::Test
|
|
|
55
55
|
|
|
56
56
|
assert_equal "No context or env", exceptions[3].message
|
|
57
57
|
assert_equal "AnotherError", exceptions[3].exception_class
|
|
58
|
-
assert_equal
|
|
58
|
+
assert_equal assert_default_context, exceptions[3].context
|
|
59
59
|
|
|
60
60
|
assert_equal "Whoops", exceptions[4].message
|
|
61
61
|
assert_equal "ErrorTest::FakeError", exceptions[4].exception_class
|
|
@@ -84,8 +84,8 @@ class ErrorTest < Minitest::Test
|
|
|
84
84
|
}
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
def
|
|
88
|
-
{
|
|
87
|
+
def assert_default_context
|
|
88
|
+
{transaction_id: ScoutApm::RequestManager.lookup.transaction_id}
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def ex(msg="Whoops")
|
|
@@ -19,4 +19,11 @@ class IgnoredUrlsTest < Minitest::Test
|
|
|
19
19
|
assert_equal false, i.ignore?("/users/2/health")
|
|
20
20
|
assert_equal true, i.ignore?("/admin/dashboard")
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
def test_ignores_prefix_regex
|
|
24
|
+
i = ScoutApm::IgnoredUris.new(["/slow/\\d+/notifications", "/health"])
|
|
25
|
+
assert_equal true, i.ignore?("/slow/123/notifications")
|
|
26
|
+
assert_equal false, i.ignore?("/slow/abcd/notifications")
|
|
27
|
+
assert_equal true, i.ignore?("/health")
|
|
28
|
+
end
|
|
22
29
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
|
|
2
|
+
require 'test_helper'
|
|
3
|
+
|
|
4
|
+
require 'scout_apm/instruments/httpx'
|
|
5
|
+
|
|
6
|
+
require 'httpx'
|
|
7
|
+
|
|
8
|
+
class HTTPXTest < Minitest::Test
|
|
9
|
+
def setup
|
|
10
|
+
@context = ScoutApm::AgentContext.new
|
|
11
|
+
@recorder = FakeRecorder.new
|
|
12
|
+
ScoutApm::Agent.instance.context.recorder = @recorder
|
|
13
|
+
ScoutApm::Instruments::HTTPX.new(@context).install(prepend: false)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_httpx
|
|
18
|
+
responses = HTTPX.get(
|
|
19
|
+
"https://news.ycombinator.com/news",
|
|
20
|
+
"https://news.ycombinator.com/news?p=2",
|
|
21
|
+
"https://google.com/q=me"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert_equal 1, @recorder.requests.length
|
|
25
|
+
|
|
26
|
+
assert_recorded(@recorder, "HTTP", "GET", "3 requests")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_httpx_post_request
|
|
30
|
+
HTTPX.post("https://httpbin.org/post", json: { test: "data" })
|
|
31
|
+
assert_recorded(@recorder, "HTTP", "POST", "httpbin.org/post")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_instruments_httpx_error_handling
|
|
35
|
+
begin
|
|
36
|
+
HTTPX.get("https://thisshouldnotexistatall12345.com")
|
|
37
|
+
rescue
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
assert_equal 1, @recorder.requests.length
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_httpx_request_retry
|
|
44
|
+
begin
|
|
45
|
+
HTTPX.with(timeout: { connect_timeout: 0.25, request_timeout: 0.25 })
|
|
46
|
+
.get("https://httpbin.org/delay/5")
|
|
47
|
+
rescue
|
|
48
|
+
end
|
|
49
|
+
assert_equal 1, @recorder.requests.length
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_multiple_plugins
|
|
53
|
+
session = HTTPX.plugin(:persistent).plugin(:follow_redirects)
|
|
54
|
+
|
|
55
|
+
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
|
|
61
|
+
|
|
62
|
+
assert_equal 2, @recorder.requests.length, "Expected 2 requests to be recorded"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def assert_recorded(recorder, type, name, desc = nil)
|
|
68
|
+
req = recorder.requests.first
|
|
69
|
+
assert req, "recorder recorded no layers"
|
|
70
|
+
assert_equal type, req.root_layer.type
|
|
71
|
+
assert_equal name, req.root_layer.name
|
|
72
|
+
if !desc.nil?
|
|
73
|
+
assert req.root_layer.desc.include?(desc),
|
|
74
|
+
"Expected description to include '#{desc}', got '#{req.root_layer.desc}'"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|