appmap 0.41.1 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +17 -2
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +54 -22
  5. data/appmap.gemspec +0 -2
  6. data/lib/appmap.rb +3 -2
  7. data/lib/appmap/class_map.rb +7 -10
  8. data/lib/appmap/config.rb +94 -34
  9. data/lib/appmap/cucumber.rb +1 -1
  10. data/lib/appmap/event.rb +18 -0
  11. data/lib/appmap/hook.rb +42 -22
  12. data/lib/appmap/hook/method.rb +1 -1
  13. data/lib/appmap/minitest.rb +35 -30
  14. data/lib/appmap/rails/request_handler.rb +41 -10
  15. data/lib/appmap/record.rb +1 -1
  16. data/lib/appmap/rspec.rb +32 -96
  17. data/lib/appmap/util.rb +16 -0
  18. data/lib/appmap/version.rb +1 -1
  19. data/patch +1447 -0
  20. data/spec/abstract_controller_base_spec.rb +69 -26
  21. data/spec/class_map_spec.rb +3 -11
  22. data/spec/config_spec.rb +31 -1
  23. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  24. data/spec/fixtures/hook/method_named_call.rb +11 -0
  25. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  26. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  27. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  28. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  29. data/spec/fixtures/rails5_users_app/create_app +8 -2
  30. data/spec/fixtures/rails5_users_app/docker-compose.yml +3 -0
  31. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  32. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  33. data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
  34. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  35. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  36. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  37. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  38. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  39. data/spec/fixtures/rails6_users_app/create_app +8 -2
  40. data/spec/fixtures/rails6_users_app/docker-compose.yml +3 -0
  41. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  42. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  43. data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
  44. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  45. data/spec/hook_spec.rb +134 -10
  46. data/spec/record_sql_rails_pg_spec.rb +1 -1
  47. data/spec/spec_helper.rb +6 -0
  48. data/test/expectations/openssl_test_key_sign1.json +2 -4
  49. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
  50. data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
  51. data/test/gem_test.rb +1 -1
  52. data/test/minitest_test.rb +1 -2
  53. data/test/rspec_test.rb +1 -20
  54. metadata +7 -8
  55. data/exe/appmap +0 -154
  56. data/spec/rspec_feature_metadata_spec.rb +0 -31
  57. data/test/cli_test.rb +0 -116
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75523ebe41aa8e327db7ff2acc4fa20bf1283474b97baf6610e11292bb1abb8b
4
- data.tar.gz: 1c0293cf928ff1f6615d7f0a72a6ba7a03acdee04d00065ffc4a0eb41e0192ad
3
+ metadata.gz: 02dfa68caaa5e5413c68ae06e89664a676624c9dbb301e2ebbc2a7ae26359afd
4
+ data.tar.gz: af97e3fb4bb428a238b854ef30969f232c48d4f3a59956865a4cc95788ac35e8
5
5
  SHA512:
6
- metadata.gz: c0a39d8067f455a1c0179c65a7f17ffd041de405bc3c48be567970e2b987765216a33801e7c486c6aeee66cae5c2291f91196c3132bd9ad65d147617f6282468
7
- data.tar.gz: bdc24d37f171945127c2ff67c834fa55932c019dbbe5e74a2f1625d11b12ca881e6a2d0c6419c0ffe625bba3295b5ef2ef1e96d6256f152ca7fbaa3d2a99b52d
6
+ metadata.gz: 9a0f2454acf5d10c48aaf1abb6e4b23d674acbbfe495770a25ff83093c38dd81cf0bb1c85dfbae4691773586ace8500c3f9b879b31749c4142851f460ed1a87c
7
+ data.tar.gz: a2860fb0258b96d95e5ca98f7d13b382c86b635ce1c2d97084c1f651a55973d2ffe31e360695fdec963292081e527b96876c6b42976a90573b9201e14032d1f5
data/.travis.yml CHANGED
@@ -13,11 +13,26 @@ services:
13
13
  # necessary.
14
14
  before_script:
15
15
  - unset RAILS_ENV
16
-
16
+
17
+ cache:
18
+ bundler: true
19
+ directories:
20
+ - $HOME/docker
21
+
22
+ # https://stackoverflow.com/a/41975912
23
+ before_cache:
24
+ # Save tagged docker images
25
+ - >
26
+ mkdir -p $HOME/docker && docker images -a --filter='dangling=false' --format '{{.Repository}}:{{.Tag}} {{.ID}}'
27
+ | xargs -n 2 -t sh -c 'test -e $HOME/docker/$1.tar.gz || docker save $0 | gzip -2 > $HOME/docker/$1.tar.gz'
28
+
29
+ before_install:
30
+ # Load cached docker images
31
+ - if [[ -d $HOME/docker ]]; then ls $HOME/docker/*.tar.gz | xargs -I {file} sh -c "zcat {file} | docker load"; fi
32
+
17
33
  jobs:
18
34
  include:
19
35
  - stage: test
20
36
  script:
21
37
  - mkdir tmp
22
38
  - bundle exec rake test
23
-
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ # v0.44.0
2
+
3
+ * Support recording and labeling of indivudal functions via `functions:` section in *appmap.yml*.
4
+ * Remove deprecated `exe/appmap`.
5
+ * Add `test_status` and `exception` fields to AppMap metadata.
6
+ * Write AppMap file atomically, by writing to a temp file first and then moving it into place.
7
+ * Remove printing of `Inventory.json` file.
8
+ * Remove source code from `classMap`.
9
+
10
+ # v0.43.0
11
+
12
+ * Record `name` and `class` of each entry in Hash-like parameters, messages, and return values.
13
+ * Record client-sent headers in HTTP server request and response.
14
+ * Record HTTP server request `mime_type`.
15
+ * Record HTTP server request `authorization`.
16
+
17
+ # v0.42.1
18
+
19
+ * Add missing require `set`.
20
+ * Check `cls.respond_to?(:singleton_class)`, since it oddly, may not.
21
+
22
+ # v0.42.0
23
+
24
+ * Remove `feature_group` and `feature` metadata from minitest and RSpec AppMaps.
25
+ * Add `metadata.source_location`.
26
+
27
+ # v0.41.2
28
+
29
+ * Don't rely on `gemspec.source_paths` to list all the source locations in a gem. Hook any code that's loaded
30
+ from inside the `gem_dir`.
31
+
1
32
  # v0.41.1
2
33
 
3
34
  * Make best effort to ensure that class name is not `null` in the appmap.json.
data/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - [Remote recording](#remote-recording)
12
12
  - [Server process recording](#server-process-recording)
13
13
  - [AppMap for VSCode](#appmap-for-vscode)
14
+ - [AppMap Swagger](#appmap-swagger)
14
15
  - [Uploading AppMaps](#uploading-appmaps)
15
16
  - [Development](#development)
16
17
  - [Running tests](#running-tests)
@@ -82,6 +83,22 @@ If you are using Ruby on Rails, require the railtie after Rails is loaded.
82
83
  require 'appmap/railtie' if defined?(AppMap).
83
84
  ```
84
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
+
85
102
  # Configuration
86
103
 
87
104
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -93,6 +110,9 @@ name: my_project
93
110
  packages:
94
111
  - path: app/controllers
95
112
  - path: app/models
113
+ # Exclude sub-paths within the package path
114
+ exclude:
115
+ - concerns/accessor
96
116
  - path: app/jobs
97
117
  - path: app/helpers
98
118
  # Include the gems that you want to see in the dependency maps.
@@ -101,15 +121,22 @@ packages:
101
121
  - gem: devise
102
122
  - gem: aws-sdk
103
123
  - gem: will_paginate
124
+ # Global exclusion of a class name
104
125
  exclude:
105
126
  - MyClass
106
127
  - MyClass#my_instance_method
107
128
  - MyClass.my_class_method
129
+ functions:
130
+ - packages: myapp
131
+ class: ControllerHelper
132
+ function: logged_in_user
133
+ labels: [ authentication ]
108
134
  ```
109
135
 
110
136
  * **name** Provides the project name (required)
111
137
  * **packages** A list of source code directories which should be recorded.
112
138
  * **exclude** A list of classes and/or methods to definitively exclude from recording.
139
+ * **functions** A list of specific functions, scoped by package and class, to record.
113
140
 
114
141
  **packages**
115
142
 
@@ -128,6 +155,11 @@ Each entry in the `packages` list is a YAML object which has the following keys:
128
155
 
129
156
  Optional list of fully qualified class and method names. Separate class and method names with period (`.`) for class methods and hash (`#`) for instance methods.
130
157
 
158
+ **functions**
159
+
160
+ Optional list of `class, function` pairs. The `package` name is used to place the function within the class map, and does not have to match
161
+ the folder or gem name. The primary use of `functions` is to apply specific labels to functions whose source code is not accessible (e.g., it's in a Gem).
162
+ For functions which are part of the application code, use `@label` or `@labels` in code comments to apply labels.
131
163
 
132
164
  # Labels
133
165
 
@@ -309,27 +341,24 @@ $ bundle exec rails server
309
341
 
310
342
  ## Server process recording
311
343
 
312
- Add this line to *configuration.rb*:
313
-
314
- ```ruby
315
- module MyApp
316
- class Application < Rails::Application
317
- ...
318
-
319
- config.appmap.enabled = true if ENV['APPMAP_RECORD']
320
-
321
- ...
322
- end
323
- end
324
- ```
325
-
326
- With this setting, you can 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.
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
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
347
 
348
+
330
349
  # AppMap for VSCode
331
350
 
332
- The [AppMap extension for VSCode](https://marketplace.visualstudio.com/items?itemName=appland.appmap) is a great way to onboard developers to new code, and troubleshoot hard-to-understand bugs with visuals.
351
+ The [AppMap extension for VSCode](https://marketplace.visualstudio.com/items?itemName=appland.appmap) helps you navigate your code more efficiently with interactive, accurate software architecture diagrams right in your IDE. In less than two minutes you can go from installing the AppMap extension to exploring maps of your code's architecture. AppMap helps you:
352
+
353
+ * Onboard to code architecture, with no extra work for the team
354
+ * Conduct code and design reviews using live and accurate data
355
+ * Troubleshoot hard-to-understand bugs using a "top-down" approach.
356
+
357
+ Each interactive diagram links directly to the source code, and the information is easy to share.
358
+
359
+ # AppMap Swagger
360
+
361
+ [appmap_swagger](https://github.com/applandinc/appmap_swagger-ruby) is a tool to generate Swagger files from AppMap data. With `appmap_swagger`, you can add Swagger to your Ruby or Ruby on Rails project, with no need to write or modify code. Use the Swagger UI to interact with your web services API as you build it, and use diffs of Swagger to perform code review of web service changes.
333
362
 
334
363
  # Uploading AppMaps
335
364
 
@@ -338,7 +367,7 @@ The [AppMap extension for VSCode](https://marketplace.visualstudio.com/items?ite
338
367
  For instructions on uploading, see the documentation of the [AppLand CLI](https://github.com/applandinc/appland-cli).
339
368
 
340
369
  # Development
341
- [![Build Status](https://travis-ci.org/applandinc/appmap-ruby.svg?branch=master)](https://travis-ci.org/applandinc/appmap-ruby)
370
+ [![Build Status](https://travis-ci.com/applandinc/appmap-ruby.svg?branch=master)](https://travis-ci.com/applandinc/appmap-ruby)
342
371
 
343
372
  ## Running tests
344
373
 
@@ -367,7 +396,7 @@ The fixture apps in `test/fixtures` are plain Ruby projects that exercise the ba
367
396
 
368
397
  ### `spec/fixtures`
369
398
 
370
- The fixture apps in `spec/fixtures` are simple Rack, Rails4, and Rails5 apps.
399
+ The fixture apps in `spec/fixtures` are simple Rack, Rails5, and Rails6 apps.
371
400
  You can use them to interactively develop and test the recording features of the `appmap` gem.
372
401
  These fixture apps are more sophisticated than `test/fixtures`, because they include additional
373
402
  resources such as a PostgreSQL database.
@@ -393,11 +422,15 @@ $ docker-compose run --rm app ./create_app
393
422
  Now you can start a development container.
394
423
 
395
424
  ```sh-session
396
- $ docker-compose run --rm -v $PWD/../../..:/src/appmap-ruby app bash
425
+ $ docker-compose run --rm -v $PWD:/app -v $PWD/../../..:/src/appmap-ruby app bash
397
426
  Starting rails_users_app_pg_1 ... done
398
- root@6fab5f89125f:/app# cd /src/app
427
+ root@6fab5f89125f:/app# cd /src/appmap-ruby
428
+ root@6fab5f89125f:/src/appmap-ruby# rm ext/appmap/*.so ext/appmap/*.o
429
+ root@6fab5f89125f:/src/appmap-ruby# bundle
430
+ root@6fab5f89125f:/src/appmap-ruby# bundle exec rake compile
431
+ root@6fab5f89125f:/src/appmap-ruby# cd /src/app
399
432
  root@6fab5f89125f:/src/app# bundle config local.appmap /src/appmap-ruby
400
- root@6fab5f89125f:/src/app# bundle update appmap
433
+ root@6fab5f89125f:/src/app# bundle
401
434
  ```
402
435
 
403
436
  At this point, the bundle is built with the `appmap` gem located in `/src/appmap`, which is volume-mounted from the host.
@@ -412,4 +445,3 @@ Configuring AppMap from path appmap.yml
412
445
  Finished in 0.07357 seconds (files took 2.1 seconds to load)
413
446
  4 examples, 0 failures
414
447
  ```
415
-
data/appmap.gemspec CHANGED
@@ -20,8 +20,6 @@ Gem::Specification.new do |spec|
20
20
  ")
21
21
  spec.extensions << "ext/appmap/extconf.rb"
22
22
 
23
- spec.bindir = 'exe'
24
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
23
  spec.require_paths = ['lib']
26
24
 
27
25
  spec.add_dependency 'activesupport'
data/lib/appmap.rb CHANGED
@@ -43,6 +43,7 @@ module AppMap
43
43
  # Call this function before the program code is loaded by the Ruby VM, otherwise
44
44
  # the load events won't be seen and the hooks won't activate.
45
45
  def initialize(config_file_path = 'appmap.yml')
46
+ raise "AppMap configuration file #{config_file_path} does not exist" unless ::File.exists?(config_file_path)
46
47
  warn "Configuring AppMap from path #{config_file_path}"
47
48
  Config.load_from_file(config_file_path).tap do |configuration|
48
49
  self.configuration = configuration
@@ -83,8 +84,8 @@ module AppMap
83
84
  end
84
85
 
85
86
  # Builds a class map from a config and a list of Ruby methods.
86
- def class_map(methods, options = {})
87
- ClassMap.build_from_methods(methods, options)
87
+ def class_map(methods)
88
+ ClassMap.build_from_methods(methods)
88
89
  end
89
90
 
90
91
  # Returns default metadata detected from the Ruby system and from the
@@ -71,17 +71,17 @@ module AppMap
71
71
  end
72
72
 
73
73
  class << self
74
- def build_from_methods(methods, options = {})
74
+ def build_from_methods(methods)
75
75
  root = Types::Root.new
76
76
  methods.each do |method|
77
- add_function root, method, options
77
+ add_function root, method
78
78
  end
79
79
  root.children.map(&:to_h)
80
80
  end
81
81
 
82
82
  protected
83
83
 
84
- def add_function(root, method, include_source: true)
84
+ def add_function(root, method)
85
85
  package = method.package
86
86
  static = method.static
87
87
 
@@ -113,16 +113,13 @@ module AppMap
113
113
  [ method.defined_class, static ? '.' : '#', method.name ].join
114
114
  end
115
115
 
116
- source, comment = begin
117
- [ method.source, method.comment ]
116
+ comment = begin
117
+ method.comment
118
118
  rescue MethodSource::SourceNotFoundError
119
- [ nil, nil, ]
119
+ nil
120
120
  end
121
121
 
122
- if include_source
123
- function_info[:source] = source unless source.blank?
124
- function_info[:comment] = comment unless comment.blank?
125
- end
122
+ function_info[:comment] = comment unless comment.blank?
126
123
 
127
124
  function_info[:labels] = parse_labels(comment) + (package.labels || [])
128
125
  object_infos << function_info
data/lib/appmap/config.rb CHANGED
@@ -20,20 +20,16 @@ module AppMap
20
20
  warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
21
21
  return
22
22
  end
23
- gem_paths(gem).map do |gem_path|
24
- Package.new(gem_path, gem, package_name, exclude, labels, shallow)
25
- end
23
+ Package.new(gem_path(gem), gem, package_name, exclude, labels, shallow)
26
24
  end
27
25
 
28
26
  private_class_method :new
29
27
 
30
28
  protected
31
29
 
32
- def gem_paths(gem)
30
+ def gem_path(gem)
33
31
  gemspec = Gem.loaded_specs[gem] or raise "Gem #{gem.inspect} not found"
34
- gemspec.source_paths.map do |path|
35
- File.join(gemspec.gem_dir, path)
36
- end
32
+ gemspec.gem_dir
37
33
  end
38
34
  end
39
35
 
@@ -53,40 +49,89 @@ module AppMap
53
49
  end
54
50
  end
55
51
 
56
- Hook = Struct.new(:method_names, :package) do
52
+ Function = Struct.new(:package, :cls, :labels, :function_names) do
53
+ def to_h
54
+ {
55
+ package: package,
56
+ class: cls,
57
+ labels: labels,
58
+ functions: function_names.map(&:to_sym)
59
+ }.compact
60
+ end
57
61
  end
58
62
 
59
- OPENSSL_PACKAGES = Package.build_from_path('openssl', package_name: 'openssl', labels: %w[security crypto])
63
+ class Hook
64
+ attr_reader :method_names, :package
65
+
66
+ def initialize(method_names, package)
67
+ @method_names = method_names
68
+ @package = package
69
+ end
70
+
71
+ def to_h
72
+ {
73
+ package: package.name,
74
+ method_names: method_names
75
+ }
76
+ end
77
+ end
78
+
79
+ OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
60
80
 
61
81
  # Methods that should always be hooked, with their containing
62
82
  # package and labels that should be applied to them.
63
83
  HOOKED_METHODS = {
64
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[provider.secure_compare])),
65
- 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', package_name: 'action_view', labels: %w[mvc.view]))
84
+ 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[crypto.secure_compare])),
85
+ 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', labels: %w[mvc.view])),
86
+ 'ActionDispatch::Request::Session' => Hook.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_path('action_pack', labels: %w[http.session])),
87
+ 'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[http.cookie])),
88
+ 'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[http.cookie crypto.encrypt])),
89
+ 'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[security.authorization])),
90
+ 'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[security.authorization])),
91
+ 'ActionController::Instrumentation' => [
92
+ Hook.new(%i[process_action send_file send_data redirect_to], Package.build_from_path('action_view', labels: %w[mvc.controller])),
93
+ Hook.new(%i[render], Package.build_from_path('action_view', labels: %w[mvc.view])),
94
+ ]
66
95
  }.freeze
67
96
 
68
97
  BUILTIN_METHODS = {
69
- 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
70
- 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
71
- 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
72
- 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
73
- 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
74
- 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http io])),
75
- 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.smtp protocol.email io])),
76
- 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.pop protocol.email io])),
77
- 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.imap protocol.email io])),
78
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization marshal])),
79
- '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 provider.serialization])),
80
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
81
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
98
+ 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
99
+ 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
100
+ 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
101
+ 'OpenSSL::Cipher' => [
102
+ Hook.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
103
+ Hook.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
104
+ ],
105
+ 'ActiveSupport::Callbacks::CallbackSequence' => [
106
+ Hook.new(:invoke_before, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.before_action])),
107
+ Hook.new(:invoke_after, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.after_action])),
108
+ ],
109
+ 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
110
+ 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http])),
111
+ 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
112
+ 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
113
+ 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
114
+ 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
115
+ '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])),
116
+ 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
117
+ 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
82
118
  }.freeze
83
119
 
84
- attr_reader :name, :packages, :exclude
120
+ attr_reader :name, :packages, :exclude, :builtin_methods
85
121
 
86
- def initialize(name, packages = [], exclude = [])
122
+ def initialize(name, packages, exclude: [], functions: [])
87
123
  @name = name
88
124
  @packages = packages
89
125
  @exclude = exclude
126
+ @builtin_methods = BUILTIN_METHODS
127
+ @functions = functions
128
+ @hooked_methods = HOOKED_METHODS.dup
129
+ functions.each do |func|
130
+ package_options = {}
131
+ package_options[:labels] = func.labels if func.labels
132
+ @hooked_methods[func.cls] ||= []
133
+ @hooked_methods[func.cls] << Hook.new(func.function_names, Package.build_from_path(func.package, package_options))
134
+ end
90
135
  end
91
136
 
92
137
  class << self
@@ -98,6 +143,16 @@ module AppMap
98
143
 
99
144
  # Loads configuration from a Hash.
100
145
  def load(config_data)
146
+ functions = (config_data['functions'] || []).map do |function_data|
147
+ package = function_data['package']
148
+ cls = function_data['class']
149
+ functions = function_data['function'] || function_data['functions']
150
+ raise 'AppMap class configuration should specify package, class and function(s)' unless package && cls && functions
151
+ functions = Array(functions).map(&:to_sym)
152
+ labels = function_data['label'] || function_data['labels']
153
+ labels = Array(labels).map(&:to_s) if labels
154
+ Function.new(package, cls, labels, functions)
155
+ end
101
156
  packages = (config_data['packages'] || []).map do |package|
102
157
  gem = package['gem']
103
158
  path = package['path']
@@ -109,10 +164,11 @@ module AppMap
109
164
  shallow = true if shallow.nil?
110
165
  Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
111
166
  else
112
- [ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow']) ]
167
+ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
113
168
  end
114
- end.flatten.compact
115
- Config.new config_data['name'], packages, config_data['exclude'] || []
169
+ end.compact
170
+ exclude = config_data['exclude'] || []
171
+ Config.new config_data['name'], packages, exclude: exclude, functions: functions
116
172
  end
117
173
  end
118
174
 
@@ -120,6 +176,7 @@ module AppMap
120
176
  {
121
177
  name: name,
122
178
  packages: packages.map(&:to_h),
179
+ functions: @functions.map(&:to_h),
123
180
  exclude: exclude
124
181
  }
125
182
  end
@@ -164,14 +221,17 @@ module AppMap
164
221
  end
165
222
 
166
223
  def find_package(defined_class, method_name)
167
- hook = find_hook(defined_class)
168
- return nil unless hook
224
+ hooks = find_hooks(defined_class)
225
+ return nil unless hooks
169
226
 
170
- Array(hook.method_names).include?(method_name) ? hook.package : nil
227
+ hook = Array(hooks).find do |hook|
228
+ Array(hook.method_names).include?(method_name)
229
+ end
230
+ hook ? hook.package : nil
171
231
  end
172
232
 
173
- def find_hook(defined_class)
174
- HOOKED_METHODS[defined_class] || BUILTIN_METHODS[defined_class]
233
+ def find_hooks(defined_class)
234
+ Array(@hooked_methods[defined_class] || @builtin_methods[defined_class])
175
235
  end
176
236
  end
177
237
  end