scout_apm 5.8.0 β 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 +14 -2
- 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 +15 -1
- data/lib/scout_apm/background_job_integrations/sidekiq.rb +89 -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 +1 -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/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 -3
- 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/sidekiq_test.rb +38 -0
- data/test/unit/config_test.rb +14 -0
- data/test/unit/error_service/error_buffer_test.rb +31 -0
- data/test/unit/error_test.rb +1 -1
- 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/sampling_test.rb +10 -10
- metadata +8 -2
- /data/test/unit/auto_instrument/{controller-ast.txt β controller-ast.parser.txt} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c8fea1cfd66466534fa0ca5e7aaac6cdb332156e02a4e933d6fb883eb9fd68e
|
|
4
|
+
data.tar.gz: d6f8ab26c4a7d77cde8f0f4fce2b1ef4f849635e839169fe9163e9b64b796294
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d38a64cd133729432a38611167a4551e5ebb208eeddd1d346a5622d0484ba82a4fedb1ac14e385cc48671cd1902726350662202db35a88a28cbde9fb7613702
|
|
7
|
+
data.tar.gz: 7755eb50a09652dba9fa4168893f51f3684b0f14c3fa681c8b2b0af3473eeb9d5b1cec98da46e49612b9f490bc5e9355a187d8658ced8616af0d6d0ad2bdef25
|
data/.github/workflows/test.yml
CHANGED
data/CHANGELOG.markdown
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
#
|
|
2
|
-
-
|
|
1
|
+
# 6.0.0
|
|
2
|
+
- Add Prism AutoInstruments Support (#582) (#594)
|
|
3
|
+
- Add OpenSearch instrumentation (#563)
|
|
4
|
+
- Add HTTPX instrumentation (#588)
|
|
5
|
+
- Add ability to automatically capture Sidekiq job args as context
|
|
6
|
+
- `job_params_capture` - Set to true to enable job argument capturing
|
|
7
|
+
- `job_params_filter` - A list of arguments to filter (automatically includes Rails filtered_parameters)
|
|
8
|
+
- Fix user error context being incorrectly flattened (#581)
|
|
9
|
+
- Handle Delayed Job PerformableMethod jobs for error tracking (#584)
|
|
10
|
+
- Require 'httpclient' library on instrumentation install (#586)
|
|
11
|
+
- Add support for Puma 7.0 hook format (#589)
|
|
12
|
+
- Update sample rate to support floats (#564)
|
|
13
|
+
- Reintegrate Regex support for ignored URIs (#574)
|
|
3
14
|
|
|
4
15
|
# 5.8.0
|
|
5
16
|
- Add error monitoring to SolidQueue, faktory, goodjob, que, shoryuken, sneakers (#571)
|
|
@@ -7,6 +18,7 @@
|
|
|
7
18
|
- Remove unused time util conflict with DeviseTokenAuth (#575)
|
|
8
19
|
- Add transaction ID to error records (#568)-
|
|
9
20
|
- Adds request method to captured error context (#577)
|
|
21
|
+
- Enable Delayed Job error tracking (#565)
|
|
10
22
|
|
|
11
23
|
# 5.7.1
|
|
12
24
|
- Update error capture API to use context (#560)
|
data/README.markdown
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/scoutapp/scout_apm_ruby/actions)
|
|
4
4
|
|
|
5
|
-
A Ruby gem for detailed Rails application performance monitoring π. Metrics and transaction traces are
|
|
6
|
-
reported to [Scout](https://
|
|
7
|
-
service.
|
|
5
|
+
A Ruby gem for detailed Rails application performance monitoring π. Metrics, errors and transaction traces are
|
|
6
|
+
reported to [Scout](https://www.scoutapm.com), a hosted application monitoring
|
|
7
|
+
service. We have a free plan for small apps and a 14-day all-access trial to test out all
|
|
8
|
+
the features. If you want to send us Rails logs, add our [other
|
|
9
|
+
gem](https://github.com/scoutapp/scout_apm_ruby_logging) and we will correlate them with
|
|
10
|
+
your performance data!
|
|
8
11
|
|
|
9
12
|
## What's the special sauce? π€
|
|
10
13
|
|
|
@@ -12,7 +15,6 @@ The Scout agent is engineered to do some wonderful things:
|
|
|
12
15
|
|
|
13
16
|
* A unique focus on identifying those hard-to-investigate outliers like memory bloat, N+1s, and user-specific problems. [See an example workflow](http://scoutapp.com/newrelic-alternative).
|
|
14
17
|
* [Low-overhead](http://blog.scoutapp.com/articles/2016/02/07/overhead-benchmarks-new-relic-vs-scout)
|
|
15
|
-
* View your performance metrics during development with [DevTrace](https://docs.scoutapm.com/#devtrace) and in production via [server_timing](https://github.com/scoutapp/ruby_server_timing).
|
|
16
18
|
* Production-Safe profiling of custom code via [ScoutProf](https://docs.scoutapm.com/#scoutprof) (BETA).
|
|
17
19
|
|
|
18
20
|
## Getting Started
|
|
@@ -29,8 +31,8 @@ Update your Gemfile
|
|
|
29
31
|
|
|
30
32
|
bundle install
|
|
31
33
|
|
|
32
|
-
Signup for a [Scout](https://scoutapm.com)
|
|
33
|
-
config file at `RAILS_ROOT/config/scout_apm.yml`.
|
|
34
|
+
Signup for a [Scout](https://scoutapm.com/users/sign_up?utm_source=github&utm_medium=github&utm_campaign=scout_apm_ruby)
|
|
35
|
+
account and put the provided config file at `RAILS_ROOT/config/scout_apm.yml`.
|
|
34
36
|
|
|
35
37
|
Your config file should look like:
|
|
36
38
|
|
|
@@ -45,6 +47,16 @@ Your config file should look like:
|
|
|
45
47
|
production:
|
|
46
48
|
<<: *defaults
|
|
47
49
|
|
|
50
|
+
## Error Monitoring
|
|
51
|
+
|
|
52
|
+
All of our accounts include Error Monitoring with 5000 errors/month free. To enable it, add the following to your `scout_apm.yml`:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
# Common/dev/production/etc. whereever you would like to start trying it
|
|
56
|
+
# monitor: true should also be required to ensure your App exists in Scout
|
|
57
|
+
errors_enabled: true
|
|
58
|
+
```
|
|
59
|
+
|
|
48
60
|
## DevTrace Quickstart
|
|
49
61
|
|
|
50
62
|
To use DevTrace, our free, no-signup, in-browser development profiler:
|
|
@@ -71,8 +83,8 @@ SCOUT_DEV_TRACE=true rails server
|
|
|
71
83
|
## Docs
|
|
72
84
|
|
|
73
85
|
For the complete list of supported frameworks, Rubies, configuration options
|
|
74
|
-
and more, see our [help site](https://
|
|
86
|
+
and more, see our [help site](https://scoutapm.com/docs).
|
|
75
87
|
|
|
76
88
|
## Help
|
|
77
89
|
|
|
78
|
-
Email support@
|
|
90
|
+
Email support@scoutapm.com if you need a hand.
|
data/gems/instruments.gemfile
CHANGED
|
@@ -7,7 +7,8 @@ module ScoutApm
|
|
|
7
7
|
def load_iseq(path)
|
|
8
8
|
if Rails.controller_path?(path) & !Rails.ignore?(path)
|
|
9
9
|
begin
|
|
10
|
-
new_code = Rails.rewrite(path)
|
|
10
|
+
new_code = Rails.rewrite(path).force_encoding(Encoding.default_external)
|
|
11
|
+
|
|
11
12
|
return self.compile(new_code, path, path)
|
|
12
13
|
rescue
|
|
13
14
|
warn "Failed to apply auto-instrumentation to #{path}: #{$!}" if ENV['SCOUT_LOG_LEVEL'].to_s.downcase == "debug"
|
|
@@ -4,11 +4,159 @@ raise LoadError, "Parser::TreeRewriter was not defined" unless defined?(Parser::
|
|
|
4
4
|
|
|
5
5
|
module ScoutApm
|
|
6
6
|
module AutoInstrument
|
|
7
|
+
module ParserImplementation
|
|
8
|
+
def self.rewrite(path, code = nil)
|
|
9
|
+
code ||= File.read(path)
|
|
10
|
+
|
|
11
|
+
ast = ::Parser::CurrentRuby.parse(code)
|
|
12
|
+
|
|
13
|
+
buffer = ::Parser::Source::Buffer.new(path)
|
|
14
|
+
buffer.source = code
|
|
15
|
+
|
|
16
|
+
rewriter = Rewriter.new
|
|
17
|
+
|
|
18
|
+
# Rewrite the AST, returns a String with the new form.
|
|
19
|
+
rewriter.rewrite(buffer, ast)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Rewriter < ::Parser::TreeRewriter
|
|
23
|
+
def initialize
|
|
24
|
+
super
|
|
25
|
+
|
|
26
|
+
# Keeps track of the parent - child relationship between nodes:
|
|
27
|
+
@nesting = []
|
|
28
|
+
|
|
29
|
+
# The stack of method nodes (type :def):
|
|
30
|
+
@method = []
|
|
31
|
+
|
|
32
|
+
# The stack of class nodes:
|
|
33
|
+
@scope = []
|
|
34
|
+
|
|
35
|
+
@cache = Cache.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def instrument(source, file_name, line)
|
|
39
|
+
# Don't log huge chunks of code... just the first line:
|
|
40
|
+
if lines = source.lines and lines.count > 1
|
|
41
|
+
source = lines.first.chomp + "..."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
method_name = @method.last.children[0]
|
|
45
|
+
bt = ["#{file_name}:#{line}:in `#{method_name}'"]
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
"::ScoutApm::AutoInstrument("+ source.dump + ",#{bt}){",
|
|
49
|
+
"}"
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Look up 1 or more nodes to check if the parent exists and matches the given type.
|
|
54
|
+
# @param type [Symbol] the symbol type to match.
|
|
55
|
+
# @param up [Integer] how far up to look.
|
|
56
|
+
def parent_type?(type, up = 1)
|
|
57
|
+
parent = @nesting[@nesting.size - up - 1] and parent.type == type
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def on_block(node)
|
|
61
|
+
# If we are not in a method, don't do any instrumentation:
|
|
62
|
+
return if @method.empty?
|
|
63
|
+
|
|
64
|
+
line = node.location.line || 'line?'
|
|
65
|
+
column = node.location.column || 'column?' # not used
|
|
66
|
+
method_name = node.children[0].children[1] || '*unknown*' # not used
|
|
67
|
+
file_name = @source_rewriter.source_buffer.name
|
|
68
|
+
|
|
69
|
+
wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_mlhs(node)
|
|
73
|
+
# Ignore / don't instrument multiple assignment (LHS).
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def on_op_asgn(node)
|
|
78
|
+
process(node.children[2])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def on_or_asgn(node)
|
|
82
|
+
process(node.children[1])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def on_and_asgn(node)
|
|
86
|
+
process(node.children[1])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Handle the method call AST node. If this method doesn't call `super`, no futher rewriting is applied to children.
|
|
90
|
+
def on_send(node)
|
|
91
|
+
# We aren't interested in top level function calls:
|
|
92
|
+
return if @method.empty?
|
|
93
|
+
|
|
94
|
+
if @cache.local_assignments?(node)
|
|
95
|
+
return super
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# This ignores both initial block method invocation `*x*{}`, and subsequent nested invocations `x{*y*}`:
|
|
99
|
+
return if parent_type?(:block)
|
|
100
|
+
|
|
101
|
+
# Extract useful metadata for instrumentation:
|
|
102
|
+
line = node.location.line || 'line?'
|
|
103
|
+
column = node.location.column || 'column?' # not used
|
|
104
|
+
method_name = node.children[1] || '*unknown*' # not used
|
|
105
|
+
file_name = @source_rewriter.source_buffer.name
|
|
106
|
+
|
|
107
|
+
# Wrap the expression with instrumentation:
|
|
108
|
+
wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def on_hash(node)
|
|
112
|
+
node.children.each do |pair|
|
|
113
|
+
# Skip `pair` if we're sure it's not using the hash shorthand syntax
|
|
114
|
+
next if pair.type != :pair
|
|
115
|
+
key_node, value_node = pair.children
|
|
116
|
+
next unless key_node.type == :sym && value_node.type == :send
|
|
117
|
+
key = key_node.children[0]
|
|
118
|
+
next unless value_node.children.size == 2 && value_node.children[0].nil? && key == value_node.children[1]
|
|
119
|
+
|
|
120
|
+
# Extract useful metadata for instrumentation:
|
|
121
|
+
line = pair.location.line || 'line?'
|
|
122
|
+
# column = pair.location.column || 'column?' # not used
|
|
123
|
+
# method_name = key || '*unknown*' # not used
|
|
124
|
+
file_name = @source_rewriter.source_buffer.name
|
|
125
|
+
|
|
126
|
+
instrument_before, instrument_after = instrument(pair.location.expression.source, file_name, line)
|
|
127
|
+
replace(pair.loc.expression, "#{key}: #{instrument_before}#{key}#{instrument_after}")
|
|
128
|
+
end
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Invoked for every AST node as it is processed top to bottom.
|
|
133
|
+
def process(node)
|
|
134
|
+
# We are nesting inside this node:
|
|
135
|
+
@nesting.push(node)
|
|
136
|
+
|
|
137
|
+
if node and node.type == :def
|
|
138
|
+
# If the node is a method, push it on the method stack as well:
|
|
139
|
+
@method.push(node)
|
|
140
|
+
super
|
|
141
|
+
@method.pop
|
|
142
|
+
elsif node and node.type == :class
|
|
143
|
+
@scope.push(node.children[0])
|
|
144
|
+
super
|
|
145
|
+
@scope.pop
|
|
146
|
+
else
|
|
147
|
+
super
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
@nesting.pop
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
7
155
|
class Cache
|
|
8
156
|
def initialize
|
|
9
157
|
@local_assignments = {}
|
|
10
158
|
end
|
|
11
|
-
|
|
159
|
+
|
|
12
160
|
def local_assignments?(node)
|
|
13
161
|
unless @local_assignments.key?(node)
|
|
14
162
|
if node.type == :lvasgn
|
|
@@ -19,7 +167,7 @@ module ScoutApm
|
|
|
19
167
|
@local_assignments[node] = false
|
|
20
168
|
end
|
|
21
169
|
end
|
|
22
|
-
|
|
170
|
+
|
|
23
171
|
return @local_assignments[node]
|
|
24
172
|
end
|
|
25
173
|
end
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
|
|
2
|
+
require 'prism'
|
|
3
|
+
|
|
4
|
+
module ScoutApm
|
|
5
|
+
module AutoInstrument
|
|
6
|
+
module PrismImplementation
|
|
7
|
+
def self.rewrite(path, code = nil)
|
|
8
|
+
code ||= File.read(path)
|
|
9
|
+
|
|
10
|
+
result = Prism.parse(code)
|
|
11
|
+
rewriter = Rewriter.new(path, code)
|
|
12
|
+
rewriter.rewrite(result.value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Rewriter
|
|
16
|
+
def initialize(path, code)
|
|
17
|
+
@path = path
|
|
18
|
+
# Set encoding to ASCII-8bit as Prism uses byte offsets
|
|
19
|
+
@code = code.b
|
|
20
|
+
@replacements = []
|
|
21
|
+
@instrumented_nodes = Set.new
|
|
22
|
+
|
|
23
|
+
# Keeps track of the parent - child relationship between nodes:
|
|
24
|
+
@nesting = []
|
|
25
|
+
|
|
26
|
+
# The stack of method nodes:
|
|
27
|
+
@method = []
|
|
28
|
+
|
|
29
|
+
# The stack of class nodes:
|
|
30
|
+
@scope = []
|
|
31
|
+
|
|
32
|
+
@cache = Cache.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def rewrite(node)
|
|
36
|
+
process(node)
|
|
37
|
+
apply_replacements
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_replacements
|
|
41
|
+
# Sort replacements by start position in reverse order
|
|
42
|
+
# This ensures we apply replacements from end to start, avoiding offset issues
|
|
43
|
+
# when we modify the string
|
|
44
|
+
sorted_replacements = @replacements.sort_by { |r| -r[:start] }
|
|
45
|
+
|
|
46
|
+
result = @code.dup
|
|
47
|
+
sorted_replacements.each do |replacement|
|
|
48
|
+
result[replacement[:start]...replacement[:end]] = replacement[:new_text].b
|
|
49
|
+
end
|
|
50
|
+
# ::RubyVM::InstructionSequence.compile will infer the encoding when compiling
|
|
51
|
+
# and will compile with ASCII-8bit correctly.
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_replacement(start_offset, end_offset, new_text)
|
|
56
|
+
@replacements << {start: start_offset, end: end_offset, new_text: new_text}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def instrument(source, file_name, line)
|
|
60
|
+
# Don't log huge chunks of code... just the first line:
|
|
61
|
+
if lines = source.lines and lines.count > 1
|
|
62
|
+
source = lines.first.chomp + "..."
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
method_name = @method.last.name
|
|
66
|
+
bt = ["#{file_name}:#{line}:in `#{method_name}'"]
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
"::ScoutApm::AutoInstrument("+ source.dump + ",#{bt}){",
|
|
70
|
+
"}"
|
|
71
|
+
]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Look up 1 or more nodes to check if the parent exists and matches the given type.
|
|
75
|
+
# @param type [Class] the node class to match.
|
|
76
|
+
# @param up [Integer] how far up to look.
|
|
77
|
+
def parent_type?(type, up = 1)
|
|
78
|
+
parent = @nesting[@nesting.size - up - 1] and parent.is_a?(type)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def wrap_node(node)
|
|
82
|
+
# Skip if this node or any parent has already been instrumented
|
|
83
|
+
return if @instrumented_nodes.include?(node)
|
|
84
|
+
|
|
85
|
+
# Skip if any ancestor node has been instrumented (to avoid overlapping replacements)
|
|
86
|
+
@nesting.each do |ancestor|
|
|
87
|
+
return if @instrumented_nodes.include?(ancestor)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Skip if any descendant node has already been instrumented (to avoid overlapping replacements)
|
|
91
|
+
# This prevents a parent node from being wrapped when a child node has already been modified
|
|
92
|
+
return if has_instrumented_descendant?(node)
|
|
93
|
+
|
|
94
|
+
start_offset = node.location.start_offset
|
|
95
|
+
end_offset = node.location.end_offset
|
|
96
|
+
line = node.location.start_line
|
|
97
|
+
source = @code[start_offset...end_offset]
|
|
98
|
+
|
|
99
|
+
instrument_before, instrument_after = instrument(source, @path, line)
|
|
100
|
+
new_text = "#{instrument_before}#{source}#{instrument_after}"
|
|
101
|
+
add_replacement(start_offset, end_offset, new_text)
|
|
102
|
+
@instrumented_nodes.add(node)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def has_instrumented_descendant?(node)
|
|
106
|
+
node.compact_child_nodes.any? do |child|
|
|
107
|
+
@instrumented_nodes.include?(child) || has_instrumented_descendant?(child)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def visit_block_node(node)
|
|
112
|
+
# If we are not in a method, don't do any instrumentation:
|
|
113
|
+
return process_children(node) if @method.empty?
|
|
114
|
+
|
|
115
|
+
# If this block is attached to a CallNode, don't wrap it separately
|
|
116
|
+
# The CallNode will wrap the entire call including the block
|
|
117
|
+
return process_children(node) if parent_type?(Prism::CallNode)
|
|
118
|
+
|
|
119
|
+
# If this block is attached to a SuperNode or ForwardingSuperNode, don't wrap it separately
|
|
120
|
+
# The super node will wrap the entire call including the block
|
|
121
|
+
return process_children(node) if parent_type?(Prism::SuperNode) || parent_type?(Prism::ForwardingSuperNode)
|
|
122
|
+
|
|
123
|
+
wrap_node(node)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def visit_multi_target_node(node)
|
|
127
|
+
# Ignore / don't instrument multiple assignment (LHS).
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def visit_call_node(node)
|
|
132
|
+
# We aren't interested in top level function calls:
|
|
133
|
+
return process_children(node) if @method.empty?
|
|
134
|
+
|
|
135
|
+
if @cache.local_assignments?(node)
|
|
136
|
+
return process_children(node)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# This ignores both initial block method invocation and subsequent nested invocations:
|
|
140
|
+
return process_children(node) if parent_type?(Prism::BlockNode)
|
|
141
|
+
|
|
142
|
+
wrap_node(node)
|
|
143
|
+
|
|
144
|
+
# Process children to handle nested calls, but blocks attached to this call
|
|
145
|
+
# won't be wrapped separately (handled by visit_block_node check)
|
|
146
|
+
process_children(node)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def visit_super_node(node)
|
|
150
|
+
# We aren't interested in top level super calls:
|
|
151
|
+
return process_children(node) if @method.empty?
|
|
152
|
+
|
|
153
|
+
# This ignores super calls inside blocks:
|
|
154
|
+
return process_children(node) if parent_type?(Prism::BlockNode)
|
|
155
|
+
|
|
156
|
+
# Only wrap super calls that have a block attached
|
|
157
|
+
# Bare super calls (with or without arguments) are just delegation and shouldn't be instrumented
|
|
158
|
+
if node.block
|
|
159
|
+
wrap_node(node)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Process children to handle nested calls, but blocks attached to this super
|
|
163
|
+
# won't be wrapped separately (handled by visit_block_node check)
|
|
164
|
+
process_children(node)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def visit_forwarding_super_node(node)
|
|
168
|
+
# We aren't interested in top level super calls:
|
|
169
|
+
return process_children(node) if @method.empty?
|
|
170
|
+
|
|
171
|
+
# This ignores super calls inside blocks:
|
|
172
|
+
return process_children(node) if parent_type?(Prism::BlockNode)
|
|
173
|
+
|
|
174
|
+
# Only wrap super calls that have a block attached
|
|
175
|
+
# Bare super calls are just delegation and shouldn't be instrumented
|
|
176
|
+
if node.block
|
|
177
|
+
wrap_node(node)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Process children to handle nested calls, but blocks attached to this super
|
|
181
|
+
# won't be wrapped separately (handled by visit_block_node check)
|
|
182
|
+
process_children(node)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# This is meant to mirror that of the parser implementation.
|
|
186
|
+
# See test/unit/auto_instrument/hash_shorthand_controller-instrumented.rb
|
|
187
|
+
# Non-nil receiver is handled in visit_call_node.
|
|
188
|
+
def visit_hash_node(node)
|
|
189
|
+
# If this hash is a descendant of a CallNode (at any level), don't instrument individual elements
|
|
190
|
+
# The parent CallNode will wrap the entire expression
|
|
191
|
+
# This allows hashes in local variable assignments to be instrumented,
|
|
192
|
+
# but hashes in method calls to be wrapped as a unit
|
|
193
|
+
in_call_node = @nesting.any? { |n| n.is_a?(Prism::CallNode) }
|
|
194
|
+
|
|
195
|
+
node.elements.each do |element|
|
|
196
|
+
if element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode)
|
|
197
|
+
value_node = element.value
|
|
198
|
+
|
|
199
|
+
# Only instrument hash element values if we're not in a CallNode
|
|
200
|
+
# Handles shorthand syntax like `shorthand:` β line 6
|
|
201
|
+
if !in_call_node && value_node.is_a?(Prism::ImplicitNode)
|
|
202
|
+
key = element.key.unescaped
|
|
203
|
+
inner_call = value_node.value
|
|
204
|
+
|
|
205
|
+
line = element.location.start_line
|
|
206
|
+
source = @code[element.location.start_offset...element.location.end_offset]
|
|
207
|
+
file_name = @path
|
|
208
|
+
method_name = @method.last.name
|
|
209
|
+
bt = ["#{file_name}:#{line}:in `#{method_name}'"]
|
|
210
|
+
|
|
211
|
+
instrument_before = "::ScoutApm::AutoInstrument(#{source.dump},#{bt}){"
|
|
212
|
+
instrument_after = "}"
|
|
213
|
+
new_text = "#{key}: #{instrument_before}#{key}#{instrument_after}"
|
|
214
|
+
add_replacement(element.location.start_offset, element.location.end_offset, new_text)
|
|
215
|
+
|
|
216
|
+
@instrumented_nodes.add(value_node)
|
|
217
|
+
@instrumented_nodes.add(inner_call)
|
|
218
|
+
next
|
|
219
|
+
elsif !in_call_node && value_node.is_a?(Prism::CallNode) && value_node.receiver.nil?
|
|
220
|
+
line = element.location.start_line
|
|
221
|
+
key = element.key.unescaped
|
|
222
|
+
value_name = value_node.name.to_s
|
|
223
|
+
pair_source = @code[element.location.start_offset...element.location.end_offset]
|
|
224
|
+
value_source = @code[value_node.location.start_offset...value_node.location.end_offset]
|
|
225
|
+
key_source = @code[element.key.location.start_offset...element.key.location.end_offset]
|
|
226
|
+
file_name = @path
|
|
227
|
+
method_name = @method.last.name
|
|
228
|
+
bt = ["#{file_name}:#{line}:in `#{method_name}'"]
|
|
229
|
+
|
|
230
|
+
has_arguments = value_node.arguments && !value_node.arguments.arguments.empty?
|
|
231
|
+
|
|
232
|
+
# Handles hash_rocket w/ same key/value name and no arguments.
|
|
233
|
+
# See test for more info on backward compatibility on this one.
|
|
234
|
+
# e.g. `hash_rocket: hash_rocket` β line 9
|
|
235
|
+
if key == value_name && !has_arguments && key_source.start_with?(':')
|
|
236
|
+
source_for_dump = pair_source
|
|
237
|
+
instrument_before = "::ScoutApm::AutoInstrument(#{source_for_dump.dump},#{bt}){"
|
|
238
|
+
instrument_after = "}"
|
|
239
|
+
instrumented_value = "#{instrument_before}#{value_source}#{instrument_after}"
|
|
240
|
+
new_text = "#{key}: #{instrumented_value}"
|
|
241
|
+
add_replacement(element.location.start_offset, element.location.end_offset, new_text)
|
|
242
|
+
|
|
243
|
+
# If key == value_name and no arguments β direct shorthand pair
|
|
244
|
+
# e.g. `longhand: longhand` β line 7
|
|
245
|
+
elsif key == value_name && !has_arguments && !key_source.start_with?(':')
|
|
246
|
+
source_for_dump = pair_source
|
|
247
|
+
instrument_before = "::ScoutApm::AutoInstrument(#{source_for_dump.dump},#{bt}){"
|
|
248
|
+
instrument_after = "}"
|
|
249
|
+
instrumented_value = "#{instrument_before}#{value_source}#{instrument_after}"
|
|
250
|
+
add_replacement(value_node.location.start_offset, value_node.location.end_offset, instrumented_value)
|
|
251
|
+
|
|
252
|
+
# If key != value_name β βdifferent key/value nameβ case
|
|
253
|
+
# e.g. `longhand_different_key: longhand` β line 8
|
|
254
|
+
# or `:hash_rocket_different_key => hash_rocket` β line 10
|
|
255
|
+
elsif key != value_name && !has_arguments
|
|
256
|
+
source_for_dump = value_source
|
|
257
|
+
instrument_before = "::ScoutApm::AutoInstrument(#{source_for_dump.dump},#{bt}){"
|
|
258
|
+
instrument_after = "}"
|
|
259
|
+
instrumented_value = "#{instrument_before}#{value_source}#{instrument_after}"
|
|
260
|
+
add_replacement(value_node.location.start_offset, value_node.location.end_offset, instrumented_value)
|
|
261
|
+
|
|
262
|
+
# If value_node has arguments β method call with params
|
|
263
|
+
# e.g. `nested_call(params["timestamp"])` β line 15
|
|
264
|
+
elsif has_arguments
|
|
265
|
+
source_for_dump = value_source
|
|
266
|
+
instrument_before = "::ScoutApm::AutoInstrument(#{source_for_dump.dump},#{bt}){"
|
|
267
|
+
instrument_after = "}"
|
|
268
|
+
instrumented_value = "#{instrument_before}#{value_source}#{instrument_after}"
|
|
269
|
+
add_replacement(value_node.location.start_offset, value_node.location.end_offset, instrumented_value)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
@instrumented_nodes.add(value_node)
|
|
273
|
+
next
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
element.compact_child_nodes.each do |child|
|
|
278
|
+
process(child)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def process(node)
|
|
284
|
+
return unless node
|
|
285
|
+
|
|
286
|
+
# We are nesting inside this node:
|
|
287
|
+
@nesting.push(node)
|
|
288
|
+
|
|
289
|
+
case node
|
|
290
|
+
when Prism::DefNode
|
|
291
|
+
# If the node is a method, push it on the method stack as well:
|
|
292
|
+
@method.push(node)
|
|
293
|
+
process_children(node)
|
|
294
|
+
@method.pop
|
|
295
|
+
when Prism::ClassNode
|
|
296
|
+
# If the node is a method, push it on the scope stack as well:
|
|
297
|
+
@scope.push(node.name)
|
|
298
|
+
process_children(node)
|
|
299
|
+
@scope.pop
|
|
300
|
+
when Prism::BlockNode
|
|
301
|
+
visit_block_node(node)
|
|
302
|
+
when Prism::MultiTargetNode
|
|
303
|
+
visit_multi_target_node(node)
|
|
304
|
+
when Prism::CallNode
|
|
305
|
+
visit_call_node(node)
|
|
306
|
+
when Prism::SuperNode
|
|
307
|
+
visit_super_node(node)
|
|
308
|
+
when Prism::ForwardingSuperNode
|
|
309
|
+
visit_forwarding_super_node(node)
|
|
310
|
+
when Prism::HashNode
|
|
311
|
+
visit_hash_node(node)
|
|
312
|
+
when Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode
|
|
313
|
+
# For op assignment nodes, only process the value
|
|
314
|
+
process(node.value)
|
|
315
|
+
else
|
|
316
|
+
process_children(node)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
@nesting.pop
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def process_children(node)
|
|
323
|
+
node.compact_child_nodes.each do |child|
|
|
324
|
+
process(child)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
class Cache
|
|
331
|
+
def initialize
|
|
332
|
+
@local_assignments = {}
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def local_assignments?(node)
|
|
336
|
+
return false unless node
|
|
337
|
+
return false unless node.respond_to?(:compact_child_nodes)
|
|
338
|
+
|
|
339
|
+
unless @local_assignments.key?(node)
|
|
340
|
+
if node.is_a?(Prism::LocalVariableWriteNode)
|
|
341
|
+
@local_assignments[node] = true
|
|
342
|
+
elsif node.compact_child_nodes.find{|child|
|
|
343
|
+
# Don't check blocks - assignments inside blocks shouldn't affect the parent call
|
|
344
|
+
next if child.is_a?(Prism::BlockNode)
|
|
345
|
+
self.local_assignments?(child)
|
|
346
|
+
}
|
|
347
|
+
@local_assignments[node] = true
|
|
348
|
+
else
|
|
349
|
+
@local_assignments[node] = false
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
return @local_assignments[node]
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|