appmap 0.39.1 → 0.42.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/CONTRIBUTING.md +22 -0
  4. data/README.md +105 -50
  5. data/lib/appmap.rb +5 -0
  6. data/lib/appmap/class_map.rb +25 -8
  7. data/lib/appmap/command/record.rb +1 -1
  8. data/lib/appmap/config.rb +48 -28
  9. data/lib/appmap/event.rb +14 -4
  10. data/lib/appmap/hook.rb +7 -0
  11. data/lib/appmap/hook/method.rb +1 -1
  12. data/lib/appmap/middleware/remote_recording.rb +1 -1
  13. data/lib/appmap/minitest.rb +17 -14
  14. data/lib/appmap/rails/request_handler.rb +8 -3
  15. data/lib/appmap/railtie.rb +1 -5
  16. data/lib/appmap/rspec.rb +12 -78
  17. data/lib/appmap/version.rb +1 -1
  18. data/spec/abstract_controller_base_spec.rb +2 -2
  19. data/spec/config_spec.rb +1 -0
  20. data/spec/fixtures/hook/exclude.rb +15 -0
  21. data/spec/fixtures/hook/labels.rb +6 -0
  22. data/spec/fixtures/rails5_users_app/Gemfile +2 -3
  23. data/spec/fixtures/rails5_users_app/appmap.yml +4 -1
  24. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  25. data/spec/fixtures/rails5_users_app/docker-compose.yml +3 -0
  26. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +3 -3
  27. data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
  28. data/spec/fixtures/rails6_users_app/Gemfile +2 -3
  29. data/spec/fixtures/rails6_users_app/appmap.yml +4 -1
  30. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  31. data/spec/fixtures/rails6_users_app/docker-compose.yml +3 -0
  32. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +3 -3
  33. data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
  34. data/spec/hook_spec.rb +37 -1
  35. data/spec/record_sql_rails_pg_spec.rb +1 -1
  36. data/spec/spec_helper.rb +1 -0
  37. data/test/fixtures/gem_test/appmap.yml +1 -1
  38. data/test/fixtures/gem_test/test/parser_test.rb +12 -0
  39. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
  40. data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
  41. data/test/gem_test.rb +4 -4
  42. data/test/minitest_test.rb +1 -2
  43. data/test/rspec_test.rb +1 -7
  44. metadata +6 -4
  45. data/spec/rspec_feature_metadata_spec.rb +0 -31
  46. data/test/fixtures/gem_test/test/to_param_test.rb +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 384456ad51727b3c819e8307de92b2785da152a46c5fa60d0a4e6ff690dbb596
4
- data.tar.gz: e0d5a984bc74ee91b0f9428d83cbaac70c387b442cc55666f36b75907080e620
3
+ metadata.gz: 50826fcf91733d3bb29aad0b6c77af2516ad08aa87557c3675d4be896f7790db
4
+ data.tar.gz: 4e5ee3f431b68a69de5ff6b080eeae861f3f7de6fe952ea1e71b5c30d4777bc1
5
5
  SHA512:
6
- metadata.gz: 2f0670d632a370241168ad76ad30dd663568556ea3e524c48aafdbf86ae9d0c8e8f76f48a1c34a7a830457cff66c4417bbf19acb7ac952daeaaec65121e7a0d5
7
- data.tar.gz: 3f1bec26161a40c75487e7f98f039df0c135f7de77ee089d72dde29c1a5d5fd19b43661c6aff3c3de58e945b87d330b96a66738a95a208ec7a4e75db1bd85363
6
+ metadata.gz: fcd01c7d1a3d70c29e33d8890e8f1ea8d7dc5cd2fbca7724618fcc76190c2d0217b89ba69e4de5003eb80147d44c15335cfdff60e67cdaf3eeaa18655fdf2b4f
7
+ data.tar.gz: 32c714968c8669f3a9dceb20543a0f09856948f4b4d6b8584ad06d6347cf10f28a4d0386e4f3af1aaccb1709ec2a88fefe88da88104ffbe18dfd0fd81fb5cb95
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # v0.42.0
2
+
3
+ * Remove `feature_group` and `feature` metadata from minitest and RSpec AppMaps.
4
+ * Add `metadata.source_location`.
5
+
6
+ # v0.41.2
7
+
8
+ * Don't rely on `gemspec.source_paths` to list all the source locations in a gem. Hook any code that's loaded
9
+ from inside the `gem_dir`.
10
+
11
+ # v0.41.1
12
+
13
+ * Make best effort to ensure that class name is not `null` in the appmap.json.
14
+ * Don't try and instrument gems which are a dependency of the this gem.
15
+ * Fix a nil exception when applying the exclude list to builtins.
16
+
17
+ # v0.41.0
18
+
19
+ * Adjust some label names to match `provider.*`, `format.*`.
20
+ * Add global `exclude` list to *appmap.yml* which can be used to definitively exclude specific classes and methods.
21
+
22
+ # v0.40.0
23
+
24
+ * Parse source code comments into function labels.
25
+
26
+ # v0.39.2
27
+ * Correctly recognize normalized path info for subengines.
28
+
1
29
  # v0.39.1
2
30
  * Support Ruby 2.7.
3
31
  * Remove support for Rails 4.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,22 @@
1
+ # Contributing to appmap-ruby
2
+
3
+ We are incredibly thankful for the contributions we receive from the community. Before contributing, please take a moment to read our [Contributor License Agreement](https://github.com/applandorg/community/blob/master/docs/CLA%20Instructions.pdf) and our [Code of Conduct](https://github.com/applandorg/community/blob/master/docs/Code%20of%20Conduct%20for%20Contributors.pdf).
4
+
5
+ ## Contributor License Agreement
6
+ We require our external contributors to sign a Contributor License Agreement ("CLA") in order to ensure that
7
+ our projects remain licensed under Free and Open Source licenses such as while allowing
8
+ Appland to build a sustainable business.
9
+
10
+ AppLand is committed to having a true Free and Open Source Software license for our
11
+ non-commercial software. A CLA enables AppLand to safely commercialize our products while
12
+ keeping a standard FOSS license with all the rights that license grants to users.
13
+
14
+ * [Contributor License Agreement](https://github.com/applandorg/community/blob/master/docs/CLA%20Instructions.pdf)
15
+
16
+
17
+ ## Code of Conduct
18
+
19
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and
20
+ healthy community.
21
+
22
+ * [Code of Conduct](https://github.com/applandorg/community/blob/master/docs/Code%20of%20Conduct%20for%20Contributors.pdf)
data/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
 
2
2
  - [About](#about)
3
+ - [Supported versions](#supported-versions)
3
4
  - [Installation](#installation)
4
5
  - [Configuration](#configuration)
6
+ - [Labels](#labels)
5
7
  - [Running](#running)
6
8
  - [RSpec](#rspec)
7
9
  - [Minitest](#minitest)
8
10
  - [Cucumber](#cucumber)
9
11
  - [Remote recording](#remote-recording)
10
- - [Ruby on Rails](#ruby-on-rails)
12
+ - [Server process recording](#server-process-recording)
13
+ - [AppMap for VSCode](#appmap-for-vscode)
11
14
  - [Uploading AppMaps](#uploading-appmaps)
12
15
  - [Development](#development)
13
16
  - [Running tests](#running-tests)
@@ -23,21 +26,21 @@
23
26
  "AppMap" is a data format which records code structure (modules, classes, and methods), code execution events
24
27
  (function calls and returns), and code metadata (repo name, repo URL, commit
25
28
  SHA, labels, etc). It's more granular than a performance profile, but it's less
26
- granular than a full debug trace. It's designed to be optimal for understanding the design intent and behavior of code.
29
+ granular than a full debug trace. It's designed to be optimal for understanding the design intent and structure of code and key data flows.
27
30
 
28
31
  There are several ways to record AppMaps of your Ruby program using the `appmap` gem:
29
32
 
30
- * Run your RSpec tests with the environment variable `APPMAP=true`. An AppMap will be generated for each spec.
33
+ * Run your tests (RSpec, Minitest, Cucumber) with the environment variable `APPMAP=true`. An AppMap will be generated for each spec.
31
34
  * Run your application server with AppMap remote recording enabled, and use the [AppLand
32
35
  browser extension](https://github.com/applandinc/appland-browser-extension) to start,
33
36
  stop, and upload recordings.
34
- * Run the command `appmap record <program>` to record the entire execution of a program.
37
+ * Wrap some code in an `AppMap.record` block, which returns JSON containing the code execution trace.
35
38
 
36
- Once you have recorded some AppMaps (for example, by running RSpec tests), you use the `appland upload` command
37
- to upload them to the AppLand server. This command, and some others, is provided
38
- by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases).
39
- Then, on the [AppLand website](https://app.land), you can
40
- visualize the design of your code and share links with collaborators.
39
+ Once you have made a recording, there are two ways to view automatically generated diagrams of the AppMaps.
40
+
41
+ The first option is to load the diagrams directly in your IDE, using the [AppMap extension for VSCode](https://marketplace.visualstudio.com/items?itemName=appland.appmap).
42
+
43
+ The second option is to upload them to the [AppLand server](https://app.land) using the [AppLand CLI](https://github.com/applandinc/appland-cli/releases).
41
44
 
42
45
  ### Supported versions
43
46
 
@@ -48,23 +51,26 @@ Support for new versions is added frequently, please check back regularly for up
48
51
 
49
52
  # Installation
50
53
 
51
- Add `gem 'appmap'` to your Gemfile just as you would any other dependency.
54
+ <a href="https://www.loom.com/share/78ab32a312ff4b85aa8827a37f1cb655"> <p>Quick and easy setup of the AppMap gem for Rails - Watch Video</p> <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/78ab32a312ff4b85aa8827a37f1cb655-with-play.gif"> </a>
52
55
 
53
- **Global installation**
56
+
57
+ Add `gem 'appmap'` to **beginning** of your Gemfile. We recommend that you add the `appmap` gem to the `:development, :test` group. Your Gemfile should look something like this:
54
58
 
55
59
  ```
56
- gem 'appmap'
57
- ```
60
+ source 'https://rubygems.org'
61
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
58
62
 
59
- **Install in test, development groups**
63
+ # Optional rubRuby version
64
+ # ruby '2.7.2'
60
65
 
61
- ```
62
66
  group :development, :test do
63
67
  gem 'appmap'
64
68
  end
65
69
  ```
66
70
 
67
- Then install with `bundle`.
71
+ Install with `bundle install`, as usual.
72
+
73
+ It's important to add the `appmap` gem before any other gems that you may want to instrument. There is more about this in the section on adding gems to the *appmap.yml*.
68
74
 
69
75
  **Railtie**
70
76
 
@@ -72,7 +78,24 @@ If you are using Ruby on Rails, require the railtie after Rails is loaded.
72
78
 
73
79
  ```
74
80
  # application.rb is a good place to do this, along with all the other railties.
75
- require 'appmap/railtie'
81
+ # Don't require the railtie in environments that don't bundle the appmap gem.
82
+ require 'appmap/railtie' if defined?(AppMap).
83
+ ```
84
+
85
+ **application.rb**
86
+
87
+ Add this line to *application.rb*, to enable server recording with `APPMAP_RECORD=true`:
88
+
89
+ ```ruby
90
+ module MyApp
91
+ class Application < Rails::Application
92
+ ...
93
+
94
+ config.appmap.enabled = true if ENV['APPMAP_RECORD']
95
+
96
+ ...
97
+ end
98
+ end
76
99
  ```
77
100
 
78
101
  # Configuration
@@ -81,15 +104,28 @@ When you run your program, the `appmap` gem reads configuration settings from `a
81
104
  file for a typical Rails project:
82
105
 
83
106
  ```yaml
84
- name: MyProject
107
+ # 'name' should generally be the same as the code repo name.
108
+ name: my_project
85
109
  packages:
86
110
  - path: app/controllers
87
111
  - path: app/models
112
+ - path: app/jobs
113
+ - path: app/helpers
114
+ # Include the gems that you want to see in the dependency maps.
115
+ # These are just examples.
88
116
  - gem: activerecord
117
+ - gem: devise
118
+ - gem: aws-sdk
119
+ - gem: will_paginate
120
+ exclude:
121
+ - MyClass
122
+ - MyClass#my_instance_method
123
+ - MyClass.my_class_method
89
124
  ```
90
125
 
91
126
  * **name** Provides the project name (required)
92
- * **packages** A list of source code directories which should be instrumented.
127
+ * **packages** A list of source code directories which should be recorded.
128
+ * **exclude** A list of classes and/or methods to definitively exclude from recording.
93
129
 
94
130
  **packages**
95
131
 
@@ -97,13 +133,46 @@ Each entry in the `packages` list is a YAML object which has the following keys:
97
133
 
98
134
  * **path** The path to the source code directory. The path may be relative to the current working directory, or it may
99
135
  be an absolute path.
100
- * **gem** As an alternative to specifying the path, specify the name of a dependency gem. When using `gem`, don't specify `path`.
136
+ * **gem** As an alternative to specifying the path, specify the name of a dependency gem. When using `gem`, don't specify `path`. In your `Gemfile`, the `appmap` gem **must** be listed **before** any gem that you specify in your *appmap.yml*.
101
137
  * **exclude** A list of files and directories which will be ignored. By default, all modules, classes and public
102
- functions are inspected.
138
+ functions are inspected. See also: global `exclude` list.
103
139
  * **shallow** When set to `true`, only the first function call entry into a package will be recorded. Subsequent function calls within
104
140
  the same package are not recorded unless code execution leaves the package and re-enters it. Default: `true` when using `gem`,
105
141
  `false` when using `path`.
106
142
 
143
+ **exclude**
144
+
145
+ Optional list of fully qualified class and method names. Separate class and method names with period (`.`) for class methods and hash (`#`) for instance methods.
146
+
147
+
148
+ # Labels
149
+
150
+ The [AppMap data format](https://github.com/applandinc/appmap) provides for class and function `labels`, which can be used to enhance the AppMap visualizations, and to programatically analyze the data.
151
+
152
+ You can apply function labels using source code comments in your Ruby code. To apply a labels to a function, add a `@label` or `@labels` line to the comment which immediately precedes a function.
153
+
154
+ For example, if you add this comment to your source code:
155
+
156
+ ```ruby
157
+ class ApiKey
158
+ # @labels provider.authentication security
159
+ def authenticate(key)
160
+ # logic to verify the key here...
161
+ end
162
+ end
163
+ ```
164
+
165
+ Then the AppMap metadata section for this function will include:
166
+
167
+ ```json
168
+ {
169
+ "name": "authenticate",
170
+ "type": "function",
171
+ "labels": [ "provider.authentication", "security" ]
172
+ }
173
+ ```
174
+
175
+
107
176
  # Running
108
177
 
109
178
  ## RSpec
@@ -129,10 +198,7 @@ require 'appmap/rspec'
129
198
  require File.expand_path("../../config/environment", __FILE__)
130
199
  ```
131
200
 
132
- 2) *Optional* Add `feature: '<feature name>'` and `feature_group: '<feature group name>'` annotations to your
133
- examples.
134
-
135
- 3) Run the tests with the environment variable `APPMAP=true`:
201
+ 2) Run the tests with the environment variable `APPMAP=true`:
136
202
 
137
203
  ```sh-session
138
204
  $ APPMAP=true bundle exec rspec
@@ -145,23 +211,6 @@ $ find tmp/appmap/rspec
145
211
  Hello_says_hello_when_prompted.appmap.json
146
212
  ```
147
213
 
148
- If you include the `feature` and `feature_group` metadata, these attributes will be exported to the AppMap file in the
149
- `metadata` section. It will look something like this:
150
-
151
- ```json
152
- {
153
- ...
154
- "metadata": {
155
- "name": "Hello app says hello when prompted",
156
- "feature": "Hello app says hello",
157
- "feature_group": "Hello"
158
- },
159
- ...
160
- }
161
- ```
162
-
163
- If you don't explicitly declare `feature` and `feature_group`, then they will be inferred from the spec name and example descriptions.
164
-
165
214
  ## Minitest
166
215
 
167
216
  To record Minitest tests, follow these additional steps:
@@ -188,13 +237,13 @@ require_relative '../config/environment'
188
237
  2) Run your tests as you normally would with the environment variable `APPMAP=true`. For example:
189
238
 
190
239
  ```
191
- $ APPMAP=true bundle exec rake
240
+ $ APPMAP=true bundle exec rake test
192
241
  ```
193
242
 
194
243
  or
195
244
 
196
245
  ```
197
- $ APPMAP=true bundle exec -Ilib -Itest test/*
246
+ $ APPMAP=true bundle exec ruby -Ilib -Itest test/*_test.rb
198
247
  ```
199
248
 
200
249
  Each Minitest test will output an AppMap file into the directory `tmp/appmap/minitest`. For example:
@@ -251,9 +300,9 @@ To manually record ad-hoc AppMaps of your Ruby app, use AppMap remote recording.
251
300
  1. Add the AppMap remote recording middleware. For example, in `config/initializers/appmap_remote_recording.rb`:
252
301
 
253
302
  ```ruby
254
- require 'appmap/middleware/remote_recording'
303
+ if defined?(AppMap)
304
+ require 'appmap/middleware/remote_recording'
255
305
 
256
- unless Rails.env.test?
257
306
  Rails.application.config.middleware.insert_after \
258
307
  Rails::Rack::Logger,
259
308
  AppMap::Middleware::RemoteRecording
@@ -274,18 +323,24 @@ $ bundle exec rails server
274
323
 
275
324
  6. Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
276
325
 
277
- ## Ruby on Rails
326
+ ## Server process recording
327
+
328
+ 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.
278
329
 
279
- If your app uses Ruby on Rails, the AppMap Railtie will be automatically enabled. Set the Rails config flag `app.config.appmap.enabled = true` to record the entire execution of your Rails app.
330
+ 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.
280
331
 
281
- Note that using this method is kind of a blunt instrument. Recording RSpecs and using Remote Recording are usually better options.
332
+ # AppMap for VSCode
333
+
334
+ 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.
282
335
 
283
336
  # Uploading AppMaps
284
337
 
338
+ [https://app.land](https://app.land) can be used to store, analyze, and share AppMaps.
339
+
285
340
  For instructions on uploading, see the documentation of the [AppLand CLI](https://github.com/applandinc/appland-cli).
286
341
 
287
342
  # Development
288
- [![Build Status](https://travis-ci.org/applandinc/appmap-ruby.svg?branch=master)](https://travis-ci.org/applandinc/appmap-ruby)
343
+ [![Build Status](https://travis-ci.com/applandinc/appmap-ruby.svg?branch=master)](https://travis-ci.com/applandinc/appmap-ruby)
289
344
 
290
345
  ## Running tests
291
346
 
data/lib/appmap.rb CHANGED
@@ -50,6 +50,11 @@ module AppMap
50
50
  end
51
51
  end
52
52
 
53
+ # Whether to include source and comments in all class maps.
54
+ def include_source?
55
+ ENV['APPMAP_SOURCE'] == 'true'
56
+ end
57
+
53
58
  # Used to start tracing, stop tracing, and record events.
54
59
  def tracing
55
60
  @tracing ||= Trace::Tracing.new
@@ -113,17 +113,18 @@ 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 ]
118
+ rescue MethodSource::SourceNotFoundError
119
+ [ nil, nil, ]
120
+ end
121
+
116
122
  if include_source
117
- begin
118
- function_info[:source] = method.source
119
- comment = method.comment || ''
120
- function_info[:comment] = comment unless comment.empty?
121
- rescue MethodSource::SourceNotFoundError
122
- # pass
123
- end
123
+ function_info[:source] = source unless source.blank?
124
+ function_info[:comment] = comment unless comment.blank?
124
125
  end
125
126
 
126
- function_info[:labels] = package.labels if package.labels
127
+ function_info[:labels] = parse_labels(comment) + (package.labels || [])
127
128
  object_infos << function_info
128
129
 
129
130
  parent = root
@@ -141,6 +142,22 @@ module AppMap
141
142
  end
142
143
  end
143
144
 
145
+ # Labels can be embedded in the function comment. Label format is similar to YARD and JavaDoc.
146
+ # The keyword is @labels or @label. The keyword is followed by space-separated labels.
147
+ # For example:
148
+ # @label provider.authentication security
149
+ def parse_labels(comment)
150
+ return [] unless comment
151
+
152
+ comment
153
+ .split("\n")
154
+ .map { |line| line.match(/^\s*#\s*@labels?\s+(.*)/) }
155
+ .compact
156
+ .map { |match| match[1] }
157
+ .inject([]) { |accum, labels| accum += labels.split(/\s+/); accum }
158
+ .sort
159
+ end
160
+
144
161
  def find_or_create(list, info)
145
162
  obj = list.find { |item| item.type == info[:type] && item.name == info[:name] }
146
163
  return obj if obj
@@ -27,7 +27,7 @@ module AppMap
27
27
  event_thread.join
28
28
  yield AppMap::APPMAP_FORMAT_VERSION,
29
29
  AppMap.detect_metadata,
30
- AppMap.class_map(tracer.event_methods),
30
+ AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
31
31
  events
32
32
  end
33
33
 
data/lib/appmap/config.rb CHANGED
@@ -16,20 +16,20 @@ module AppMap
16
16
  end
17
17
 
18
18
  def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
19
- gem_paths(gem).map do |gem_path|
20
- Package.new(gem_path, gem, package_name, exclude, labels, shallow)
19
+ if %w[method_source activesupport].member?(gem)
20
+ warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
21
+ return
21
22
  end
23
+ Package.new(gem_path(gem), gem, package_name, exclude, labels, shallow)
22
24
  end
23
25
 
24
26
  private_class_method :new
25
27
 
26
28
  protected
27
29
 
28
- def gem_paths(gem)
30
+ def gem_path(gem)
29
31
  gemspec = Gem.loaded_specs[gem] or raise "Gem #{gem.inspect} not found"
30
- gemspec.source_paths.map do |path|
31
- File.join(gemspec.gem_dir, path)
32
- end
32
+ gemspec.gem_dir
33
33
  end
34
34
  end
35
35
 
@@ -57,32 +57,36 @@ module AppMap
57
57
  # Methods that should always be hooked, with their containing
58
58
  # package and labels that should be applied to them.
59
59
  HOOKED_METHODS = {
60
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[security crypto])),
61
- 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', package_name: 'action_view', labels: %w[view]))
60
+ 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[provider.secure_compare])),
61
+ 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', labels: %w[mvc.view])),
62
+ 'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[provider.http.cookie])),
63
+ 'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[provider.http.cookie crypto])),
64
+ 'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[provider.authorization])),
65
+ 'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[provider.authorization])),
62
66
  }.freeze
63
67
 
64
68
  BUILTIN_METHODS = {
65
69
  'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
66
- 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGES),
67
70
  'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
68
71
  'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
69
72
  'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
70
73
  'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
71
- 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[http io])),
72
- 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
73
- 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
74
- 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
75
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[serialization marshal])),
76
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[serialization yaml])),
77
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json])),
78
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json])),
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])),
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])),
79
82
  }.freeze
80
83
 
81
- attr_reader :name, :packages
84
+ attr_reader :name, :packages, :exclude
82
85
 
83
- def initialize(name, packages = [])
86
+ def initialize(name, packages = [], exclude = [])
84
87
  @name = name
85
88
  @packages = packages
89
+ @exclude = exclude
86
90
  end
87
91
 
88
92
  class << self
@@ -105,44 +109,60 @@ module AppMap
105
109
  shallow = true if shallow.nil?
106
110
  Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
107
111
  else
108
- [ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow']) ]
112
+ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
109
113
  end
110
- end.flatten
111
- Config.new config_data['name'], packages
114
+ end.compact
115
+ Config.new config_data['name'], packages, config_data['exclude'] || []
112
116
  end
113
117
  end
114
118
 
115
119
  def to_h
116
120
  {
117
121
  name: name,
118
- packages: packages.map(&:to_h)
122
+ packages: packages.map(&:to_h),
123
+ exclude: exclude
119
124
  }
120
125
  end
121
126
 
127
+ # package_for_method finds the Package, if any, which configures the hook
128
+ # for a method.
122
129
  def package_for_method(method)
130
+ package_hooked_by_class(method) || package_hooked_by_source_location(method)
131
+ end
132
+
133
+ def package_hooked_by_class(method)
123
134
  defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
124
- package = find_package(defined_class, method_name)
125
- return package if package
135
+ return find_package(defined_class, method_name)
136
+ end
126
137
 
138
+ def package_hooked_by_source_location(method)
127
139
  location = method.source_location
128
140
  location_file, = location
129
141
  return unless location_file
130
142
 
131
143
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
132
- packages.find do |pkg|
144
+ packages.select { |pkg| pkg.path }.find do |pkg|
133
145
  (location_file.index(pkg.path) == 0) &&
134
146
  !pkg.exclude.find { |p| location_file.index(p) }
135
147
  end
136
148
  end
137
149
 
138
- def included_by_location?(method)
139
- !!package_for_method(method)
150
+ def never_hook?(method)
151
+ defined_class, separator, method_name = ::AppMap::Hook.qualify_method_name(method)
152
+ return true if exclude.member?(defined_class) || exclude.member?([ defined_class, separator, method_name ].join)
140
153
  end
141
154
 
155
+ # always_hook? indicates a method that should always be hooked.
142
156
  def always_hook?(defined_class, method_name)
143
157
  !!find_package(defined_class, method_name)
144
158
  end
145
159
 
160
+ # included_by_location? indicates a method whose source location matches a method definition that has been
161
+ # configured for inclusion.
162
+ def included_by_location?(method)
163
+ !!package_for_method(method)
164
+ end
165
+
146
166
  def find_package(defined_class, method_name)
147
167
  hook = find_hook(defined_class)
148
168
  return nil unless hook