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 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