appmap 0.45.1 → 0.46.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -0
- data/CHANGELOG.md +7 -0
- data/README.md +10 -11
- data/lib/appmap.rb +0 -1
- data/lib/appmap/class_map.rb +7 -15
- data/lib/appmap/config.rb +15 -8
- data/lib/appmap/event.rb +29 -28
- data/lib/appmap/handler/function.rb +1 -1
- data/lib/appmap/handler/rails/request_handler.rb +124 -0
- data/lib/appmap/handler/rails/sql_handler.rb +152 -0
- data/lib/appmap/handler/rails/template.rb +149 -0
- data/lib/appmap/hook/method.rb +1 -1
- data/lib/appmap/railtie.rb +5 -5
- data/lib/appmap/trace.rb +46 -6
- data/lib/appmap/version.rb +1 -1
- data/package-lock.json +3 -3
- data/spec/abstract_controller_base_spec.rb +65 -6
- data/spec/class_map_spec.rb +3 -3
- data/spec/hook_spec.rb +2 -2
- metadata +5 -4
- data/lib/appmap/rails/request_handler.rb +0 -122
- data/lib/appmap/rails/sql_handler.rb +0 -150
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b563a80bb77c42dd44d9fd299ed9b1a273abaf9d43256e23a0856fc06849f74c
|
4
|
+
data.tar.gz: 25dead3dd3c057ee4585c8af0e85e8d495e689a18f3aa02d850bdf470c98d5ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6bc94e6ee51daf178d9db411f2393538e91e4cc6a985c9b1d28ccc08feda7b6b59708d9f5f0f2cbd2184b058ff39d245c2c84e4cb25a64d5e6ddac96587c2a76
|
7
|
+
data.tar.gz: 252d3955b6daedfb687091c8fb735c3f61d4cca3d7fa6ac44182c9c87f2185f53e886fa4e96f3896c35a7b5bb67e3302120dfa4e978324783b72d446f531b630
|
data/.travis.yml
CHANGED
@@ -17,6 +17,16 @@ before_script:
|
|
17
17
|
cache:
|
18
18
|
bundler: true
|
19
19
|
|
20
|
+
before_install:
|
21
|
+
# see https://blog.travis-ci.com/docker-rate-limits
|
22
|
+
# and also https://www.docker.com/blog/what-you-need-to-know-about-upcoming-docker-hub-rate-limiting/
|
23
|
+
# if we do not use authorized account,
|
24
|
+
# the pulls-per-IP quota is shared with other Travis users
|
25
|
+
- >
|
26
|
+
if [ ! -z "$DOCKERHUB_PASSWORD" ] && [ ! -z "$DOCKERHUB_USERNAME" ]; then
|
27
|
+
echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin ;
|
28
|
+
fi
|
29
|
+
|
20
30
|
|
21
31
|
# GEM_ALTERNATIVE_NAME only needed for deployment
|
22
32
|
jobs:
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.46.0](https://github.com/applandinc/appmap-ruby/compare/v0.45.1...v0.46.0) (2021-05-12)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* Record view template rendering events and template paths ([973b258](https://github.com/applandinc/appmap-ruby/commit/973b2581b6e2d4e15a1b93331e4e95a88678faae))
|
7
|
+
|
1
8
|
## [0.45.1](https://github.com/applandinc/appmap-ruby/compare/v0.45.0...v0.45.1) (2021-05-04)
|
2
9
|
|
3
10
|
|
data/README.md
CHANGED
@@ -9,7 +9,6 @@
|
|
9
9
|
- [Minitest](#minitest)
|
10
10
|
- [Cucumber](#cucumber)
|
11
11
|
- [Remote recording](#remote-recording)
|
12
|
-
- [Server process recording](#server-process-recording)
|
13
12
|
- [AppMap for VSCode](#appmap-for-vscode)
|
14
13
|
- [AppMap Swagger](#appmap-swagger)
|
15
14
|
- [Uploading AppMaps](#uploading-appmaps)
|
@@ -326,25 +325,25 @@ if defined?(AppMap)
|
|
326
325
|
end
|
327
326
|
```
|
328
327
|
|
329
|
-
2. Download and unpack the [AppLand browser extension](https://github.com/applandinc/appland-browser-extension). Install into Chrome using `chrome://extensions/`. Turn on "Developer Mode" and then load the extension using the "Load unpacked" button.
|
328
|
+
2. (optional) Download and unpack the [AppLand browser extension](https://github.com/applandinc/appland-browser-extension). Install into Chrome using `chrome://extensions/`. Turn on "Developer Mode" and then load the extension using the "Load unpacked" button.
|
330
329
|
|
331
|
-
3. Start your Rails application server
|
330
|
+
3. Start your Rails application server, with `APPMAP_RECORD=true`. For example:
|
332
331
|
|
333
332
|
```sh-session
|
334
|
-
$ bundle exec rails server
|
333
|
+
$ APPMAP_RECORD=true bundle exec rails server
|
335
334
|
```
|
336
335
|
|
337
|
-
4.
|
336
|
+
4. Start the recording
|
338
337
|
|
339
|
-
|
340
|
-
|
341
|
-
6. Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
|
338
|
+
Option 1: Open the AppLand browser extension and push `Start`.
|
339
|
+
Option 2: `curl -XPOST localhost:3000/_appmap/record` (be sure and get the port number right)
|
342
340
|
|
343
|
-
|
341
|
+
5. Use your app. For example, perform a login flow, or run through a manual UI test.
|
344
342
|
|
345
|
-
|
343
|
+
6. Finish the recording.
|
346
344
|
|
347
|
-
|
345
|
+
Option 1: Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
|
346
|
+
Option 2: `curl -XDELETE localhost:3000/_appmap/record > recording.appmap.json` - Saves the recording as a local file.
|
348
347
|
|
349
348
|
|
350
349
|
# AppMap for VSCode
|
data/lib/appmap.rb
CHANGED
data/lib/appmap/class_map.rb
CHANGED
@@ -82,16 +82,13 @@ module AppMap
|
|
82
82
|
protected
|
83
83
|
|
84
84
|
def add_function(root, method)
|
85
|
-
package = method.package
|
86
|
-
static = method.static
|
87
|
-
|
88
85
|
object_infos = [
|
89
86
|
{
|
90
|
-
name: package
|
87
|
+
name: method.package,
|
91
88
|
type: 'package'
|
92
89
|
}
|
93
90
|
]
|
94
|
-
object_infos += method.
|
91
|
+
object_infos += method.class_name.split('::').map do |name|
|
95
92
|
{
|
96
93
|
name: name,
|
97
94
|
type: 'class'
|
@@ -100,7 +97,7 @@ module AppMap
|
|
100
97
|
function_info = {
|
101
98
|
name: method.name,
|
102
99
|
type: 'function',
|
103
|
-
static: static
|
100
|
+
static: method.static
|
104
101
|
}
|
105
102
|
location = method.source_location
|
106
103
|
|
@@ -108,20 +105,15 @@ module AppMap
|
|
108
105
|
if location
|
109
106
|
location_file, lineno = location
|
110
107
|
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
111
|
-
[ location_file, lineno ].join(':')
|
108
|
+
[ location_file, lineno ].compact.join(':')
|
112
109
|
else
|
113
|
-
[ method.
|
110
|
+
[ method.class_name, method.static ? '.' : '#', method.name ].join
|
114
111
|
end
|
115
112
|
|
116
|
-
comment =
|
117
|
-
method.comment
|
118
|
-
rescue MethodSource::SourceNotFoundError
|
119
|
-
nil
|
120
|
-
end
|
121
|
-
|
113
|
+
comment = method.comment
|
122
114
|
function_info[:comment] = comment unless comment.blank?
|
123
115
|
|
124
|
-
function_info[:labels] = parse_labels(comment) + (
|
116
|
+
function_info[:labels] = parse_labels(comment) + (method.labels || [])
|
125
117
|
object_infos << function_info
|
126
118
|
|
127
119
|
parent = root
|
data/lib/appmap/config.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'appmap/handler/net_http'
|
4
|
+
require 'appmap/handler/rails/template'
|
5
|
+
|
3
6
|
module AppMap
|
4
7
|
class Config
|
5
8
|
# Specifies a code +path+ to be mapped.
|
@@ -120,15 +123,19 @@ module AppMap
|
|
120
123
|
# Methods that should always be hooked, with their containing
|
121
124
|
# package and labels that should be applied to them.
|
122
125
|
HOOKED_METHODS = {
|
123
|
-
'ActionView::Renderer' => TargetMethods.new(:render, Package.build_from_gem('actionview', package_name: 'action_view', labels: %w[mvc.view], optional: true)
|
124
|
-
|
125
|
-
|
126
|
-
'
|
127
|
-
|
128
|
-
|
126
|
+
'ActionView::Renderer' => TargetMethods.new(:render, Package.build_from_gem('actionview', shallow: false, package_name: 'action_view', labels: %w[mvc.view], optional: true).tap do |package|
|
127
|
+
package.handler_class = AppMap::Handler::Rails::Template::RenderHandler if package
|
128
|
+
end),
|
129
|
+
'ActionView::Resolver' => TargetMethods.new(%i[find_all find_all_anywhere], Package.build_from_gem('actionview', shallow: false, package_name: 'action_view', labels: %w[mvc.template.resolver], optional: true).tap do |package|
|
130
|
+
package.handler_class = AppMap::Handler::Rails::Template::ResolverHandler if package
|
131
|
+
end),
|
132
|
+
'ActionDispatch::Request::Session' => TargetMethods.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.session], optional: true)),
|
133
|
+
'ActionDispatch::Cookies::CookieJar' => TargetMethods.new(%i[[]= clear update delete recycle], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.cookie], optional: true)),
|
134
|
+
'ActionDispatch::Cookies::EncryptedCookieJar' => TargetMethods.new(%i[[]=], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.cookie crypto.encrypt], optional: true)),
|
135
|
+
'CanCan::ControllerAdditions' => TargetMethods.new(%i[authorize! can? cannot?], Package.build_from_gem('cancancan', shallow: false, labels: %w[security.authorization], optional: true)),
|
136
|
+
'CanCan::Ability' => TargetMethods.new(%i[authorize!], Package.build_from_gem('cancancan', shallow: false, labels: %w[security.authorization], optional: true)),
|
129
137
|
'ActionController::Instrumentation' => [
|
130
|
-
TargetMethods.new(%i[process_action send_file send_data redirect_to], Package.build_from_gem('actionpack', package_name: 'action_controller', labels: %w[mvc.controller], optional: true))
|
131
|
-
TargetMethods.new(%i[render], Package.build_from_gem('actionpack', package_name: 'action_controller', labels: %w[mvc.view], optional: true)),
|
138
|
+
TargetMethods.new(%i[process_action send_file send_data redirect_to], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_controller', labels: %w[mvc.controller], optional: true))
|
132
139
|
]
|
133
140
|
}.freeze
|
134
141
|
|
data/lib/appmap/event.rb
CHANGED
@@ -21,10 +21,10 @@ module AppMap
|
|
21
21
|
LIMIT = 100
|
22
22
|
|
23
23
|
class << self
|
24
|
-
def build_from_invocation(
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
def build_from_invocation(event_type, event:)
|
25
|
+
event.id = AppMap::Event.next_id_counter
|
26
|
+
event.event = event_type
|
27
|
+
event.thread_id = Thread.current.object_id
|
28
28
|
end
|
29
29
|
|
30
30
|
# Gets a display string for a value. This is not meant to be a machine deserializable value.
|
@@ -48,8 +48,6 @@ module AppMap
|
|
48
48
|
nil
|
49
49
|
end
|
50
50
|
|
51
|
-
protected
|
52
|
-
|
53
51
|
# Heuristic for dynamically defined class whose name can be nil
|
54
52
|
def best_class_name(value)
|
55
53
|
value_cls = value.class
|
@@ -103,19 +101,20 @@ module AppMap
|
|
103
101
|
attr_accessor :defined_class, :method_id, :path, :lineno, :parameters, :receiver, :static
|
104
102
|
|
105
103
|
class << self
|
106
|
-
def build_from_invocation(
|
104
|
+
def build_from_invocation(defined_class, method, receiver, arguments, event: MethodCall.new)
|
105
|
+
event ||= MethodCall.new
|
107
106
|
defined_class ||= 'Class'
|
108
|
-
|
107
|
+
event.tap do
|
109
108
|
static = receiver.is_a?(Module)
|
110
|
-
|
111
|
-
|
109
|
+
event.defined_class = defined_class
|
110
|
+
event.method_id = method.name.to_s
|
112
111
|
if method.source_location
|
113
112
|
path = method.source_location[0]
|
114
113
|
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
|
115
|
-
|
116
|
-
|
114
|
+
event.path = path
|
115
|
+
event.lineno = method.source_location[1]
|
117
116
|
else
|
118
|
-
|
117
|
+
event.path = [ defined_class, static ? '.' : '#', method.name ].join
|
119
118
|
end
|
120
119
|
|
121
120
|
# Check if the method has key parameters. If there are any they'll always be last.
|
@@ -123,7 +122,7 @@ module AppMap
|
|
123
122
|
has_key = [[:dummy], *method.parameters].last.first.to_s.start_with?('key') && arguments[-1].is_a?(Hash)
|
124
123
|
kwargs = has_key && arguments[-1].dup || {}
|
125
124
|
|
126
|
-
|
125
|
+
event.parameters = method.parameters.map.with_index do |method_param, idx|
|
127
126
|
param_type, param_name = method_param
|
128
127
|
param_name ||= 'arg'
|
129
128
|
value = case param_type
|
@@ -144,13 +143,13 @@ module AppMap
|
|
144
143
|
kind: param_type
|
145
144
|
}
|
146
145
|
end
|
147
|
-
|
146
|
+
event.receiver = {
|
148
147
|
class: best_class_name(receiver),
|
149
148
|
object_id: receiver.__id__,
|
150
149
|
value: display_string(receiver)
|
151
150
|
}
|
152
|
-
|
153
|
-
MethodEvent.build_from_invocation(
|
151
|
+
event.static = static
|
152
|
+
MethodEvent.build_from_invocation(:call, event: event)
|
154
153
|
end
|
155
154
|
end
|
156
155
|
end
|
@@ -175,11 +174,12 @@ module AppMap
|
|
175
174
|
attr_accessor :parent_id, :elapsed
|
176
175
|
|
177
176
|
class << self
|
178
|
-
def build_from_invocation(
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
177
|
+
def build_from_invocation(parent_id, elapsed: nil, event: MethodReturnIgnoreValue.new)
|
178
|
+
event ||= MethodReturnIgnoreValue.new
|
179
|
+
event.tap do |_|
|
180
|
+
event.parent_id = parent_id
|
181
|
+
event.elapsed = elapsed
|
182
|
+
MethodEvent.build_from_invocation(:return, event: event)
|
183
183
|
end
|
184
184
|
end
|
185
185
|
end
|
@@ -187,7 +187,7 @@ module AppMap
|
|
187
187
|
def to_h
|
188
188
|
super.tap do |h|
|
189
189
|
h[:parent_id] = parent_id
|
190
|
-
h[:elapsed] = elapsed
|
190
|
+
h[:elapsed] = elapsed if elapsed
|
191
191
|
end
|
192
192
|
end
|
193
193
|
end
|
@@ -196,10 +196,11 @@ module AppMap
|
|
196
196
|
attr_accessor :return_value, :exceptions
|
197
197
|
|
198
198
|
class << self
|
199
|
-
def build_from_invocation(
|
200
|
-
|
199
|
+
def build_from_invocation(parent_id, return_value, exception, elapsed: nil, event: MethodReturn.new)
|
200
|
+
event ||= MethodReturn.new
|
201
|
+
event.tap do |_|
|
201
202
|
if return_value
|
202
|
-
|
203
|
+
event.return_value = {
|
203
204
|
class: best_class_name(return_value),
|
204
205
|
value: display_string(return_value),
|
205
206
|
object_id: return_value.__id__
|
@@ -220,9 +221,9 @@ module AppMap
|
|
220
221
|
next_exception = next_exception.cause
|
221
222
|
end
|
222
223
|
|
223
|
-
|
224
|
+
event.exceptions = exceptions
|
224
225
|
end
|
225
|
-
MethodReturnIgnoreValue.build_from_invocation(
|
226
|
+
MethodReturnIgnoreValue.build_from_invocation(parent_id, elapsed: elapsed, event: event)
|
226
227
|
end
|
227
228
|
end
|
228
229
|
end
|
@@ -11,7 +11,7 @@ module AppMap
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def handle_return(call_event_id, elapsed, return_value, exception)
|
14
|
-
AppMap::Event::MethodReturn.build_from_invocation(call_event_id,
|
14
|
+
AppMap::Event::MethodReturn.build_from_invocation(call_event_id, return_value, exception, elapsed: elapsed)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/event'
|
4
|
+
require 'appmap/hook'
|
5
|
+
|
6
|
+
module AppMap
|
7
|
+
module Handler
|
8
|
+
module Rails
|
9
|
+
module RequestHandler
|
10
|
+
class HTTPServerRequest < AppMap::Event::MethodEvent
|
11
|
+
attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
|
12
|
+
|
13
|
+
def initialize(request)
|
14
|
+
super AppMap::Event.next_id_counter, :call, Thread.current.object_id
|
15
|
+
|
16
|
+
self.request_method = request.request_method
|
17
|
+
self.normalized_path_info = normalized_path(request)
|
18
|
+
self.mime_type = request.headers['Content-Type']
|
19
|
+
self.headers = AppMap::Util.select_headers(request.env)
|
20
|
+
self.authorization = request.headers['Authorization']
|
21
|
+
self.path_info = request.path_info.split('?')[0]
|
22
|
+
# ActionDispatch::Http::ParameterFilter is deprecated
|
23
|
+
parameter_filter_cls = \
|
24
|
+
if defined?(ActiveSupport::ParameterFilter)
|
25
|
+
ActiveSupport::ParameterFilter
|
26
|
+
else
|
27
|
+
ActionDispatch::Http::ParameterFilter
|
28
|
+
end
|
29
|
+
self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_h
|
33
|
+
super.tap do |h|
|
34
|
+
h[:http_server_request] = {
|
35
|
+
request_method: request_method,
|
36
|
+
path_info: path_info,
|
37
|
+
mime_type: mime_type,
|
38
|
+
normalized_path_info: normalized_path_info,
|
39
|
+
authorization: authorization,
|
40
|
+
headers: headers,
|
41
|
+
}.compact
|
42
|
+
|
43
|
+
unless params.blank?
|
44
|
+
h[:message] = params.keys.map do |key|
|
45
|
+
val = params[key]
|
46
|
+
{
|
47
|
+
name: key,
|
48
|
+
class: val.class.name,
|
49
|
+
value: self.class.display_string(val),
|
50
|
+
object_id: val.__id__,
|
51
|
+
}.tap do |message|
|
52
|
+
properties = object_properties(val)
|
53
|
+
message[:properties] = properties if properties
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def normalized_path(request, router = ::Rails.application.routes.router)
|
63
|
+
router.recognize request do |route, _|
|
64
|
+
app = route.app
|
65
|
+
next unless app.matches? request
|
66
|
+
return normalized_path request, app.rack_app.routes.router if app.engine?
|
67
|
+
|
68
|
+
return route.path.spec.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
|
74
|
+
attr_accessor :status, :mime_type, :headers
|
75
|
+
|
76
|
+
def initialize(response, parent_id, elapsed)
|
77
|
+
super AppMap::Event.next_id_counter, :return, Thread.current.object_id
|
78
|
+
|
79
|
+
self.status = response.status
|
80
|
+
self.mime_type = response.headers['Content-Type']
|
81
|
+
self.parent_id = parent_id
|
82
|
+
self.elapsed = elapsed
|
83
|
+
self.headers = AppMap::Util.select_headers(response.headers)
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_h
|
87
|
+
super.tap do |h|
|
88
|
+
h[:http_server_response] = {
|
89
|
+
status_code: status,
|
90
|
+
mime_type: mime_type,
|
91
|
+
headers: headers
|
92
|
+
}.compact
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class HookMethod < AppMap::Hook::Method
|
98
|
+
def initialize
|
99
|
+
# ActionController::Instrumentation has issued start_processing.action_controller and
|
100
|
+
# process_action.action_controller since Rails 3. Therefore it's a stable place to hook
|
101
|
+
# the request. Rails controller notifications can't be used directly because they don't
|
102
|
+
# provide response headers, and we want the Content-Type.
|
103
|
+
super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def before_hook(receiver, defined_class, _) # args
|
109
|
+
call_event = HTTPServerRequest.new(receiver.request)
|
110
|
+
# http_server_request events are i/o and do not require a package name.
|
111
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
|
112
|
+
[ call_event, TIME_NOW.call ]
|
113
|
+
end
|
114
|
+
|
115
|
+
def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
|
116
|
+
elapsed = TIME_NOW.call - start_time
|
117
|
+
return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
|
118
|
+
AppMap.tracing.record_event return_event
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|