appmap 0.45.0 → 0.48.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: 0db499c7da4baea3f29fb56214a95e18047a07fc0c132ba559f7f6eb7ef9033a
4
- data.tar.gz: 3aabf3bf1a31c39d6844ccf17807a4b7e206783bdf8e373008d3db0d04a854d7
3
+ metadata.gz: 6ce9ca4faa9074d177861610ff47bec4e7b02191e870759a12d1ed9207e9f79a
4
+ data.tar.gz: 6b0157f28774b1d46fdea527c8076846253780040628f3879ec8c18af8c4a60d
5
5
  SHA512:
6
- metadata.gz: a38e51f5c932d9a04b566c2664104e08a56af80fd3277c039603eb719336599bc9764781e1d0835c6c7b702e4e0465587fd4537e292df325c0d9efe4a90c1493
7
- data.tar.gz: 168e2b8c1605cf7b4f8c4d6a5f373b5add4ce1804c4c494f3cc4e3b29da5a08f4e121889bca36e06e1e29aa4c9943c16b76f7868e9331c3a7e5b33af0817f1a0
6
+ metadata.gz: e9d67de222d734c8b650c2c8bb8cef70b371908561dd8df43410f370071cdd1c24b9cc8d4919ccfda59745afefccc29e8af30444f25136aac685384ec079e0ba
7
+ data.tar.gz: 387a7a074b9c56ae423825e0beea405909066528c40ff6d6ce3789843193c6e0c73cafabefd8e091dc9961dd5e2c112f311538e8afdc5bbaf37ca187be2f1606
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,39 @@
1
+ # [0.48.0](https://github.com/applandinc/appmap-ruby/compare/v0.47.1...v0.48.0) (2021-05-19)
2
+
3
+
4
+ ### Features
5
+
6
+ * Hook the code only when APPMAP=true ([dd9e383](https://github.com/applandinc/appmap-ruby/commit/dd9e383024d1d9205a617d46bd64b90820035533))
7
+ * Remove server process recording from doc and tests ([383ba0a](https://github.com/applandinc/appmap-ruby/commit/383ba0ad444922a0a85409477d11bc7ed06a9160))
8
+
9
+ ## [0.47.1](https://github.com/applandinc/appmap-ruby/compare/v0.47.0...v0.47.1) (2021-05-13)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * Add the proper template function hooks for Rails 6.0.7 ([175f489](https://github.com/applandinc/appmap-ruby/commit/175f489acbaed77ad52a18d805e4b6eeae1abfdb))
15
+
16
+ # [0.47.0](https://github.com/applandinc/appmap-ruby/compare/v0.46.0...v0.47.0) (2021-05-13)
17
+
18
+
19
+ ### Features
20
+
21
+ * Emit swagger-style normalized paths instead of Rails-style ones ([5a93cd7](https://github.com/applandinc/appmap-ruby/commit/5a93cd7096ca195146a84a6733c7d502dbcd0272))
22
+
23
+ # [0.46.0](https://github.com/applandinc/appmap-ruby/compare/v0.45.1...v0.46.0) (2021-05-12)
24
+
25
+
26
+ ### Features
27
+
28
+ * Record view template rendering events and template paths ([973b258](https://github.com/applandinc/appmap-ruby/commit/973b2581b6e2d4e15a1b93331e4e95a88678faae))
29
+
30
+ ## [0.45.1](https://github.com/applandinc/appmap-ruby/compare/v0.45.0...v0.45.1) (2021-05-04)
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * Optimize instrumentation and load time ([db4a8ce](https://github.com/applandinc/appmap-ruby/commit/db4a8ceed4103a52caafa46626c66f33fbfeac27))
36
+
1
37
  # [0.45.0](https://github.com/applandinc/appmap-ruby/compare/v0.44.0...v0.45.0) (2021-05-03)
2
38
 
3
39
 
data/README.md CHANGED
@@ -9,11 +9,11 @@
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)
16
15
  - [Development](#development)
16
+ - [Internal architecture](#internal-architecture)
17
17
  - [Running tests](#running-tests)
18
18
  - [Using fixture apps](#using-fixture-apps)
19
19
  - [`test/fixtures`](#testfixtures)
@@ -83,22 +83,6 @@ If you are using Ruby on Rails, require the railtie after Rails is loaded.
83
83
  require 'appmap/railtie' if defined?(AppMap).
84
84
  ```
85
85
 
86
- **application.rb**
87
-
88
- Add this line to *application.rb*, to enable server recording with `APPMAP_RECORD=true`:
89
-
90
- ```ruby
91
- module MyApp
92
- class Application < Rails::Application
93
- ...
94
-
95
- config.appmap.enabled = true if ENV['APPMAP_RECORD']
96
-
97
- ...
98
- end
99
- end
100
- ```
101
-
102
86
  # Configuration
103
87
 
104
88
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -325,25 +309,25 @@ if defined?(AppMap)
325
309
  end
326
310
  ```
327
311
 
328
- 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.
312
+ 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.
329
313
 
330
- 3. Start your Rails application server. For example:
314
+ 3. Start your Rails application server, with `APPMAP_RECORD=true`. For example:
331
315
 
332
316
  ```sh-session
333
- $ bundle exec rails server
317
+ $ APPMAP_RECORD=true bundle exec rails server
334
318
  ```
335
319
 
336
- 4. Open the AppLand browser extension and push `Start`.
320
+ 4. Start the recording
337
321
 
338
- 5. Use your app. For example, perform a login flow, or run through a manual UI test.
322
+ Option 1: Open the AppLand browser extension and push `Start`.
323
+ Option 2: `curl -XPOST localhost:3000/_appmap/record` (be sure and get the port number right)
339
324
 
340
- 6. Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
341
-
342
- ## Server process recording
325
+ 5. Use your app. For example, perform a login flow, or run through a manual UI test.
343
326
 
344
- 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.
327
+ 6. Finish the recording.
345
328
 
346
- 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.
329
+ Option 1: Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
330
+ Option 2: `curl -XDELETE localhost:3000/_appmap/record > recording.appmap.json` - Saves the recording as a local file.
347
331
 
348
332
 
349
333
  # AppMap for VSCode
@@ -369,6 +353,34 @@ For instructions on uploading, see the documentation of the [AppLand CLI](https:
369
353
  # Development
370
354
  [![Build Status](https://travis-ci.com/applandinc/appmap-ruby.svg?branch=master)](https://travis-ci.com/applandinc/appmap-ruby)
371
355
 
356
+ ## Internal architecture
357
+
358
+ **Configuration**
359
+
360
+ *appmap.yml* is loaded into an `AppMap::Config`.
361
+
362
+ **Hooking**
363
+
364
+ Once configuration is loaded, `AppMap::Hook` is enabled. "Hooking" refers to the process of replacing a method
365
+ with a "hooked" version of the method. The hooked method checks to see if tracing is enabled. If so, it wraps the original
366
+ method with calls that record the parameters and return value.
367
+
368
+ **Builtins**
369
+
370
+ `Hook` begins by iterating over builtin classes and modules defined in the `Config`. Builtins include code
371
+ like `openssl` and `net/http`. This code is not dependent on any external libraries being present, and
372
+ `appmap` cannot guarantee that it will be loaded before builtins. Therefore, it's necessary to require it and
373
+ hook it by looking up the classes and modules as constants in the `Object` namespace.
374
+
375
+ **User code and gems**
376
+
377
+ After hooking builtins, `Hook` attaches a [TracePoint](https://ruby-doc.org/core-2.6/TracePoint.html) to `:begin` events.
378
+ This TracePoint is notified each time a new class or module is being evaluated. When this happens, `Hook` uses the `Config`
379
+ to determine whether any code within the evaluated file is configured for hooking. If so, a `TracePoint` is attached to
380
+ `:end` events. Each `:end` event is fired when a class or module definition is completed. When this happens, the `Hook` enumerates
381
+ the public methods of the class or module, hooking the ones that are targeted by the `Config`. Once the `:end` TracePoint leaves
382
+ the scope of the `:begin`, the `:end` TracePoint is disabled.
383
+
372
384
  ## Running tests
373
385
 
374
386
  Before running tests, configure `local.appmap` to point to your local `appmap-ruby` directory.
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'
@@ -99,4 +98,4 @@ module AppMap
99
98
  end
100
99
 
101
100
  require 'appmap/railtie' if defined?(::Rails::Railtie)
102
- AppMap.initialize unless ENV['APPMAP_INITIALIZE'] == 'false'
101
+ AppMap.initialize if ENV['APPMAP'] == 'true'
@@ -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,8 +1,25 @@
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
8
+ # Specifies a code +path+ to be mapped.
9
+ # Options:
10
+ #
11
+ # * +gem+ may indicate a gem name that "owns" the path
12
+ # * +package_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
13
+ # builtins, or when the path to be required is not automatically required when bundler requires the gem.
14
+ # * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
15
+ # * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
16
+ # specific functions, via TargetMethods.
17
+ # * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
5
18
  Package = Struct.new(:path, :gem, :package_name, :exclude, :labels, :shallow) do
19
+ # This is for internal use only.
20
+ private_methods :gem
21
+
22
+ # Specifies the class that will convert code events into event objects.
6
23
  attr_writer :handler_class
7
24
 
8
25
  def handler_class
@@ -18,25 +35,36 @@ module AppMap
18
35
  end
19
36
 
20
37
  class << self
38
+ # Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
39
+ # in appmap.yml. Also used for mapping specific methods via TargetMethods.
21
40
  def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
22
41
  Package.new(path, nil, package_name, exclude, labels, shallow)
23
42
  end
24
43
 
25
- def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
26
- if %w[method_source activesupport].member?(gem)
44
+ # Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
45
+ # a builtin.
46
+ def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [], optional: false, force: false)
47
+ if !force && %w[method_source activesupport].member?(gem)
27
48
  warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
28
49
  return
29
50
  end
30
- Package.new(gem_path(gem), gem, package_name, exclude, labels, shallow)
51
+ path = gem_path(gem, optional)
52
+ if path
53
+ Package.new(path, gem, package_name, exclude, labels, shallow)
54
+ else
55
+ warn "#{gem} is not available in the bundle" if AppMap::Hook::LOG
56
+ end
31
57
  end
32
58
 
33
59
  private_class_method :new
34
60
 
35
61
  protected
36
62
 
37
- def gem_path(gem)
38
- gemspec = Gem.loaded_specs[gem] or raise "Gem #{gem.inspect} not found"
39
- gemspec.gem_dir
63
+ def gem_path(gem, optional)
64
+ gemspec = Gem.loaded_specs[gem]
65
+ # This exception will notify a user that their appmap.yml contains non-existent gems.
66
+ raise "Gem #{gem.inspect} not found" unless gemspec || optional
67
+ gemspec ? gemspec.gem_dir : nil
40
68
  end
41
69
  end
42
70
 
@@ -57,7 +85,33 @@ module AppMap
57
85
  end
58
86
  end
59
87
 
60
- Function = Struct.new(:package, :cls, :labels, :function_names) do
88
+ # Identifies specific methods within a package which should be hooked.
89
+ class TargetMethods # :nodoc:
90
+ attr_reader :method_names, :package
91
+
92
+ def initialize(method_names, package)
93
+ @method_names = method_names
94
+ @package = package
95
+ end
96
+
97
+ def include_method?(method_name)
98
+ Array(method_names).include?(method_name)
99
+ end
100
+
101
+ def to_h
102
+ {
103
+ package: package.name,
104
+ method_names: method_names
105
+ }
106
+ end
107
+ end
108
+ private_constant :TargetMethods
109
+
110
+ # Function represents a specific function configured for hooking by the +functions+
111
+ # entry in appmap.yml. When the Config is initialized, each Function is converted into
112
+ # a Package and TargetMethods. It's called a Function rather than a Method, because Function
113
+ # is the AppMap terminology.
114
+ Function = Struct.new(:package, :cls, :labels, :function_names) do # :nodoc:
61
115
  def to_h
62
116
  {
63
117
  package: package,
@@ -67,80 +121,127 @@ module AppMap
67
121
  }.compact
68
122
  end
69
123
  end
124
+ private_constant :Function
70
125
 
71
- class Hook
72
- attr_reader :method_names, :package
126
+ ClassTargetMethods = Struct.new(:cls, :target_methods) # :nodoc:
127
+ private_constant :ClassTargetMethods
73
128
 
74
- def initialize(method_names, package)
75
- @method_names = method_names
76
- @package = package
129
+ MethodHook = Struct.new(:cls, :method_names, :labels) # :nodoc:
130
+ private_constant :MethodHook
131
+
132
+ class << self
133
+ def package_hooks(gem_name, methods, handler_class: nil, package_name: nil)
134
+ Array(methods).map do |method|
135
+ package = Package.build_from_gem(gem_name, package_name: package_name, labels: method.labels, shallow: false, optional: true)
136
+ next unless package
137
+
138
+ package.handler_class = handler_class if handler_class
139
+ ClassTargetMethods.new(method.cls, TargetMethods.new(Array(method.method_names), package))
140
+ end.compact
77
141
  end
78
142
 
79
- def to_h
80
- {
81
- package: package.name,
82
- method_names: method_names
83
- }
143
+ def method_hook(cls, method_names, labels)
144
+ MethodHook.new(cls, method_names, labels)
84
145
  end
85
146
  end
86
147
 
87
- OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
148
+ # Hook well-known functions. When a function configured here is available in the bundle, it will be hooked with the
149
+ # predefined labels specified here. If any of these hooks are not desired, they can be disabled in the +exclude+ section
150
+ # of appmap.yml.
151
+ METHOD_HOOKS = [
152
+ package_hooks('actionview',
153
+ [
154
+ method_hook('ActionView::Renderer', :render, %w[mvc.view]),
155
+ method_hook('ActionView::TemplateRenderer', :render, %w[mvc.view]),
156
+ method_hook('ActionView::PartialRenderer', :render, %w[mvc.view])
157
+ ],
158
+ handler_class: AppMap::Handler::Rails::Template::RenderHandler,
159
+ package_name: 'action_view'
160
+ ),
161
+ package_hooks('actionview',
162
+ [
163
+ method_hook('ActionView::Resolver', %i[find_all find_all_anywhere], %w[mvc.template.resolver])
164
+ ],
165
+ handler_class: AppMap::Handler::Rails::Template::ResolverHandler,
166
+ package_name: 'action_view'
167
+ ),
168
+ package_hooks('actionpack',
169
+ [
170
+ method_hook('ActionDispatch::Request::Session', %i[destroy [] dig values []= clear update delete fetch merge], %w[http.session]),
171
+ method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session]),
172
+ method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
173
+ ],
174
+ package_name: 'action_dispatch'
175
+ ),
176
+ package_hooks('cancancan',
177
+ [
178
+ method_hook('CanCan::ControllerAdditions', %i[authorize! can? cannot?], %w[security.authorization]),
179
+ method_hook('CanCan::Ability', %i[authorize?], %w[security.authorization])
180
+ ]
181
+ ),
182
+ package_hooks('actionpack',
183
+ [
184
+ method_hook('ActionController::Instrumentation', %i[process_action send_file send_data redirect_to], %w[mvc.controller])
185
+ ],
186
+ package_name: 'action_controller'
187
+ )
188
+ ].flatten.freeze
88
189
 
89
- # Methods that should always be hooked, with their containing
90
- # package and labels that should be applied to them.
91
- HOOKED_METHODS = {
92
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[crypto.secure_compare])),
93
- 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', labels: %w[mvc.view])),
94
- 'ActionDispatch::Request::Session' => Hook.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_path('action_pack', labels: %w[http.session])),
95
- 'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[http.cookie])),
96
- 'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[http.cookie crypto.encrypt])),
97
- 'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[security.authorization])),
98
- 'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[security.authorization])),
99
- 'ActionController::Instrumentation' => [
100
- Hook.new(%i[process_action send_file send_data redirect_to], Package.build_from_path('action_view', labels: %w[mvc.controller])),
101
- Hook.new(%i[render], Package.build_from_path('action_view', labels: %w[mvc.view])),
102
- ]
103
- }.freeze
190
+ OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
104
191
 
105
- BUILTIN_METHODS = {
106
- 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
107
- 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
108
- 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
192
+ # Hook functions which are builtin to Ruby. Because they are builtins, they may be loaded before appmap.
193
+ # Therefore, we can't rely on TracePoint to report the loading of this code.
194
+ BUILTIN_HOOKS = {
195
+ 'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
196
+ 'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
197
+ 'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
109
198
  'OpenSSL::Cipher' => [
110
- Hook.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
111
- Hook.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
199
+ TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
200
+ TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
112
201
  ],
113
202
  'ActiveSupport::Callbacks::CallbackSequence' => [
114
- Hook.new(:invoke_before, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.before_action])),
115
- Hook.new(:invoke_after, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.after_action])),
203
+ TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.before_action])),
204
+ TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.after_action])),
116
205
  ],
117
- 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
118
- 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
206
+ 'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, package_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
207
+ 'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
208
+ 'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
119
209
  package.handler_class = AppMap::Handler::NetHTTP
120
210
  end),
121
- 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
122
- 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
123
- 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
124
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
125
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
126
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
127
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
211
+ 'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
212
+ 'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
213
+ # This is happening: Method send_command not found on Net::IMAP
214
+ # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
215
+ # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
216
+ 'Psych' => TargetMethods.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
217
+ 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
218
+ 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
128
219
  }.freeze
129
220
 
130
- attr_reader :name, :packages, :exclude, :builtin_methods
221
+ attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_hooks
131
222
 
132
223
  def initialize(name, packages, exclude: [], functions: [])
133
224
  @name = name
134
225
  @packages = packages
226
+ @hook_paths = Set.new(packages.map(&:path))
135
227
  @exclude = exclude
136
- @builtin_methods = BUILTIN_METHODS
228
+ @builtin_hooks = BUILTIN_HOOKS
137
229
  @functions = functions
138
- @hooked_methods = HOOKED_METHODS.dup
230
+
231
+ @hooked_methods = METHOD_HOOKS.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
232
+ hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
233
+ end
234
+
139
235
  functions.each do |func|
140
236
  package_options = {}
141
237
  package_options[:labels] = func.labels if func.labels
142
- @hooked_methods[func.cls] ||= []
143
- @hooked_methods[func.cls] << Hook.new(func.function_names, Package.build_from_path(func.package, package_options))
238
+ @hooked_methods[func.cls] << TargetMethods.new(func.function_names, Package.build_from_path(func.package, package_options))
239
+ end
240
+
241
+ @hooked_methods.each_value do |hooks|
242
+ Array(hooks).each do |hook|
243
+ @hook_paths << hook.package.path
244
+ end
144
245
  end
145
246
  end
146
247
 
@@ -191,57 +292,54 @@ module AppMap
191
292
  }
192
293
  end
193
294
 
194
- # package_for_method finds the Package, if any, which configures the hook
195
- # for a method.
196
- def package_for_method(method)
197
- package_hooked_by_class(method) || package_hooked_by_source_location(method)
198
- end
199
-
200
- def package_hooked_by_class(method)
201
- defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
202
- return find_package(defined_class, method_name)
295
+ # Determines if methods defined in a file path should possibly be hooked.
296
+ def path_enabled?(path)
297
+ path = AppMap::Util.normalize_path(path)
298
+ @hook_paths.find { |hook_path| path.index(hook_path) == 0 }
203
299
  end
204
300
 
205
- def package_hooked_by_source_location(method)
206
- location = method.source_location
207
- location_file, = location
208
- return unless location_file
301
+ # Looks up a class and method in the config, to find the matching Package configuration.
302
+ # This class is only used after +path_enabled?+ has returned `true`.
303
+ LookupPackage = Struct.new(:config, :cls, :method) do
304
+ def package
305
+ # Global "excludes" configuration can be used to ignore any class/method.
306
+ return if config.never_hook?(cls, method)
209
307
 
210
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
211
- packages.select { |pkg| pkg.path }.find do |pkg|
212
- (location_file.index(pkg.path) == 0) &&
213
- !pkg.exclude.find { |p| location_file.index(p) }
308
+ package_for_code_object || package_for_location
214
309
  end
215
- end
216
-
217
- def never_hook?(method)
218
- defined_class, separator, method_name = ::AppMap::Hook.qualify_method_name(method)
219
- return true if exclude.member?(defined_class) || exclude.member?([ defined_class, separator, method_name ].join)
220
- end
221
310
 
222
- # always_hook? indicates a method that should always be hooked.
223
- def always_hook?(defined_class, method_name)
224
- !!find_package(defined_class, method_name)
225
- end
311
+ # Hook a method which is specified by class and method name.
312
+ def package_for_code_object
313
+ Array(config.hooked_methods[cls.name])
314
+ .compact
315
+ .find { |hook| hook.include_method?(method.name) }
316
+ &.package
317
+ end
226
318
 
227
- # included_by_location? indicates a method whose source location matches a method definition that has been
228
- # configured for inclusion.
229
- def included_by_location?(method)
230
- !!package_for_method(method)
319
+ # Hook a method which is specified by code location (i.e. path).
320
+ def package_for_location
321
+ location = method.source_location
322
+ location_file, = location
323
+ return unless location_file
324
+
325
+ location_file = AppMap::Util.normalize_path(location_file)
326
+ config
327
+ .packages
328
+ .select { |pkg| pkg.path }
329
+ .find do |pkg|
330
+ (location_file.index(pkg.path) == 0) &&
331
+ !pkg.exclude.find { |p| location_file.index(p) }
332
+ end
333
+ end
231
334
  end
232
335
 
233
- def find_package(defined_class, method_name)
234
- hooks = find_hooks(defined_class)
235
- return nil unless hooks
236
-
237
- hook = Array(hooks).find do |hook|
238
- Array(hook.method_names).include?(method_name)
239
- end
240
- hook ? hook.package : nil
336
+ def lookup_package(cls, method)
337
+ LookupPackage.new(self, cls, method).package
241
338
  end
242
339
 
243
- def find_hooks(defined_class)
244
- Array(@hooked_methods[defined_class] || @builtin_methods[defined_class])
340
+ def never_hook?(cls, method)
341
+ _, separator, = ::AppMap::Hook.qualify_method_name(method)
342
+ return true if exclude.member?(cls.name) || exclude.member?([ cls.name, separator, method.name ].join)
245
343
  end
246
344
  end
247
345
  end