appmap 0.45.1 → 0.46.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b91b79723565f45d9d59ce341a9944a3a4e1c853bc77c1b176dc7b26ede9df22
4
- data.tar.gz: f9be88ae7e83b801f66ada4087a06923ad9fa4d10e498f1c8b07fe0e301552d7
3
+ metadata.gz: b563a80bb77c42dd44d9fd299ed9b1a273abaf9d43256e23a0856fc06849f74c
4
+ data.tar.gz: 25dead3dd3c057ee4585c8af0e85e8d495e689a18f3aa02d850bdf470c98d5ff
5
5
  SHA512:
6
- metadata.gz: e7eda447c67a44ad10226f5b6e9eb51c82995a4b2df970af27b0e2b5304101d419a6b3843ef6f0a983a3f952da2e3ed55f0de327b2856fcf970961c410c19b79
7
- data.tar.gz: 839df6608d503e74f663d42673d9cf91866592a1715cee1c828c3de23ae472fdff6025ea9ee767a33e384b2ed51f46a999001d354f3e763deba46a5253eb5661
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. For example:
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. Open the AppLand browser extension and push `Start`.
336
+ 4. Start the recording
338
337
 
339
- 5. Use your app. For example, perform a login flow, or run through a manual UI test.
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
- ## Server process recording
341
+ 5. Use your app. For example, perform a login flow, or run through a manual UI test.
344
342
 
345
- Run your Rails server with `APPMAP_RECORD=true`. When the server exits, an *appmap.json* file will be written to the project directory. This is a great way to start the server, interact with your app as a user (or through it's API), and then view an AppMap of everything that happened.
343
+ 6. Finish the recording.
346
344
 
347
- Be sure and set `WEB_CONCURRENCY=1`, if you are using a webserver that can run multiple processes. You only want there to be one process while you are recording, otherwise they will both try and write *appmap.json* and one of them will clobber the other.
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
@@ -9,7 +9,6 @@ end
9
9
 
10
10
  require 'appmap/version'
11
11
  require 'appmap/hook'
12
- require 'appmap/handler/net_http'
13
12
  require 'appmap/config'
14
13
  require 'appmap/trace'
15
14
  require 'appmap/class_map'
@@ -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.name,
87
+ name: method.package,
91
88
  type: 'package'
92
89
  }
93
90
  ]
94
- object_infos += method.defined_class.split('::').map do |name|
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.defined_class, static ? '.' : '#', method.name ].join
110
+ [ method.class_name, method.static ? '.' : '#', method.name ].join
114
111
  end
115
112
 
116
- comment = begin
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) + (package.labels || [])
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
- 'ActionDispatch::Request::Session' => TargetMethods.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.session], optional: true)),
125
- 'ActionDispatch::Cookies::CookieJar' => TargetMethods.new(%i[[]= clear update delete recycle], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.cookie], optional: true)),
126
- 'ActionDispatch::Cookies::EncryptedCookieJar' => TargetMethods.new(%i[[]=], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.cookie crypto.encrypt], optional: true)),
127
- 'CanCan::ControllerAdditions' => TargetMethods.new(%i[authorize! can? cannot?], Package.build_from_gem('cancancan', labels: %w[security.authorization], optional: true)),
128
- 'CanCan::Ability' => TargetMethods.new(%i[authorize!], Package.build_from_gem('cancancan', labels: %w[security.authorization], optional: true)),
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(me, event_type)
25
- me.id = AppMap::Event.next_id_counter
26
- me.event = event_type
27
- me.thread_id = Thread.current.object_id
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(mc = MethodCall.new, defined_class, method, receiver, arguments)
104
+ def build_from_invocation(defined_class, method, receiver, arguments, event: MethodCall.new)
105
+ event ||= MethodCall.new
107
106
  defined_class ||= 'Class'
108
- mc.tap do
107
+ event.tap do
109
108
  static = receiver.is_a?(Module)
110
- mc.defined_class = defined_class
111
- mc.method_id = method.name.to_s
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
- mc.path = path
116
- mc.lineno = method.source_location[1]
114
+ event.path = path
115
+ event.lineno = method.source_location[1]
117
116
  else
118
- mc.path = [ defined_class, static ? '.' : '#', method.name ].join
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
- mc.parameters = method.parameters.map.with_index do |method_param, idx|
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
- mc.receiver = {
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
- mc.static = static
153
- MethodEvent.build_from_invocation(mc, :call)
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(mr = MethodReturnIgnoreValue.new, parent_id, elapsed)
179
- mr.tap do |_|
180
- mr.parent_id = parent_id
181
- mr.elapsed = elapsed
182
- MethodEvent.build_from_invocation(mr, :return)
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(mr = MethodReturn.new, parent_id, elapsed, return_value, exception)
200
- mr.tap do |_|
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
- mr.return_value = {
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
- mr.exceptions = exceptions
224
+ event.exceptions = exceptions
224
225
  end
225
- MethodReturnIgnoreValue.build_from_invocation(mr, parent_id, elapsed)
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, elapsed, return_value, exception)
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