appmap 0.41.0 → 0.43.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +32 -6
  4. data/lib/appmap.rb +5 -0
  5. data/lib/appmap/command/record.rb +1 -1
  6. data/lib/appmap/config.rb +16 -12
  7. data/lib/appmap/event.rb +32 -4
  8. data/lib/appmap/hook.rb +17 -2
  9. data/lib/appmap/hook/method.rb +1 -1
  10. data/lib/appmap/middleware/remote_recording.rb +1 -1
  11. data/lib/appmap/minitest.rb +17 -14
  12. data/lib/appmap/rails/request_handler.rb +41 -10
  13. data/lib/appmap/rspec.rb +13 -78
  14. data/lib/appmap/version.rb +1 -1
  15. data/spec/abstract_controller_base_spec.rb +67 -22
  16. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  17. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  18. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  19. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  20. data/spec/fixtures/rails5_users_app/create_app +8 -2
  21. data/spec/fixtures/rails5_users_app/docker-compose.yml +3 -0
  22. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  23. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  24. data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
  25. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  26. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  27. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  28. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  29. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  30. data/spec/fixtures/rails6_users_app/create_app +8 -2
  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 +16 -3
  33. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  34. data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
  35. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  36. data/spec/record_sql_rails_pg_spec.rb +1 -1
  37. data/spec/spec_helper.rb +6 -0
  38. data/test/fixtures/gem_test/appmap.yml +1 -1
  39. data/test/fixtures/gem_test/test/parser_test.rb +12 -0
  40. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
  41. data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
  42. data/test/gem_test.rb +4 -4
  43. data/test/minitest_test.rb +1 -2
  44. data/test/rspec_test.rb +1 -7
  45. metadata +3 -4
  46. data/spec/rspec_feature_metadata_spec.rb +0 -31
  47. 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: d747c0e65dc1efb45769ae9118a7b97015b0378d79e20eec855e74c20b622af2
4
- data.tar.gz: dcc3b91f32246aaefd02fbcf47672bfdf13acf6cadb715fbc5ff0040dbcfbf72
3
+ metadata.gz: 4007f0eb67a3a25088e8b3677de8d0388fd1bfec4b326c43a51c503f640325b5
4
+ data.tar.gz: 1b593251a7a072b7a1483e7af172b9157d6913208658fb324630bc52b0b4fe73
5
5
  SHA512:
6
- metadata.gz: 5f82a6a0cf8075537b0835e783779c827c5afed81e83e242bc97cbfd5b6ef0320fe352235436f57009a63db321223b9f7f82e8bcdced558d9b935a2ce4e4cbf3
7
- data.tar.gz: aaa4676d24b6bb1bcbd34fb7f0e3a0244d005c36dd89782b774b342b7e30e1c9637795d0d492522b2a5e525de413e3adb9da54bd5ace2699373ffdfadf3a8157
6
+ metadata.gz: 6af64a17bf69655ae8bdc9b4b67b7e2c9bd1b87d07a23e266dcabd2a927311224a4b27a77355674559828a08ae1df2e7231e2a68a2734c0f620de8efc48ec6d4
7
+ data.tar.gz: 7725cb0c79d8be13fddf7a3b688113f4fd31941c4f08f64ee44d4fbb3f5255dd3a308b08ef578413938052ee3b9eeecedc3e58bc092b12e23bfa872eac119eb7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # v0.43.0
2
+
3
+ * Record `name` and `class` of each entry in Hash-like parameters, messages, and return values.
4
+ * Record client-sent headers in HTTP server request and response.
5
+ * Record HTTP server request `mime_type`.
6
+ * Record HTTP server request `authorization`.
7
+
8
+ # v0.42.1
9
+
10
+ * Add missing require `set`.
11
+ * Check `cls.respond_to?(:singleton_class)`, since it oddly, may not.
12
+
13
+ # v0.42.0
14
+
15
+ * Remove `feature_group` and `feature` metadata from minitest and RSpec AppMaps.
16
+ * Add `metadata.source_location`.
17
+
18
+ # v0.41.2
19
+
20
+ * Don't rely on `gemspec.source_paths` to list all the source locations in a gem. Hook any code that's loaded
21
+ from inside the `gem_dir`.
22
+
23
+ # v0.41.1
24
+
25
+ * Make best effort to ensure that class name is not `null` in the appmap.json.
26
+ * Don't try and instrument gems which are a dependency of the this gem.
27
+ * Fix a nil exception when applying the exclude list to builtins.
28
+
1
29
  # v0.41.0
2
30
 
3
31
  * Adjust some label names to match `provider.*`, `format.*`.
data/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - [Minitest](#minitest)
10
10
  - [Cucumber](#cucumber)
11
11
  - [Remote recording](#remote-recording)
12
+ - [Server process recording](#server-process-recording)
12
13
  - [AppMap for VSCode](#appmap-for-vscode)
13
14
  - [Uploading AppMaps](#uploading-appmaps)
14
15
  - [Development](#development)
@@ -81,6 +82,22 @@ If you are using Ruby on Rails, require the railtie after Rails is loaded.
81
82
  require 'appmap/railtie' if defined?(AppMap).
82
83
  ```
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
99
+ ```
100
+
84
101
  # Configuration
85
102
 
86
103
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -306,6 +323,12 @@ $ bundle exec rails server
306
323
 
307
324
  6. Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
308
325
 
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.
329
+
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.
331
+
309
332
  # AppMap for VSCode
310
333
 
311
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.
@@ -317,7 +340,7 @@ The [AppMap extension for VSCode](https://marketplace.visualstudio.com/items?ite
317
340
  For instructions on uploading, see the documentation of the [AppLand CLI](https://github.com/applandinc/appland-cli).
318
341
 
319
342
  # Development
320
- [![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)
321
344
 
322
345
  ## Running tests
323
346
 
@@ -346,7 +369,7 @@ The fixture apps in `test/fixtures` are plain Ruby projects that exercise the ba
346
369
 
347
370
  ### `spec/fixtures`
348
371
 
349
- The fixture apps in `spec/fixtures` are simple Rack, Rails4, and Rails5 apps.
372
+ The fixture apps in `spec/fixtures` are simple Rack, Rails5, and Rails6 apps.
350
373
  You can use them to interactively develop and test the recording features of the `appmap` gem.
351
374
  These fixture apps are more sophisticated than `test/fixtures`, because they include additional
352
375
  resources such as a PostgreSQL database.
@@ -372,11 +395,15 @@ $ docker-compose run --rm app ./create_app
372
395
  Now you can start a development container.
373
396
 
374
397
  ```sh-session
375
- $ docker-compose run --rm -v $PWD/../../..:/src/appmap-ruby app bash
398
+ $ docker-compose run --rm -v $PWD:/app -v $PWD/../../..:/src/appmap-ruby app bash
376
399
  Starting rails_users_app_pg_1 ... done
377
- root@6fab5f89125f:/app# cd /src/app
400
+ root@6fab5f89125f:/app# cd /src/appmap-ruby
401
+ root@6fab5f89125f:/src/appmap-ruby# rm ext/appmap/*.so ext/appmap/*.o
402
+ root@6fab5f89125f:/src/appmap-ruby# bundle
403
+ root@6fab5f89125f:/src/appmap-ruby# bundle exec rake compile
404
+ root@6fab5f89125f:/src/appmap-ruby# cd /src/app
378
405
  root@6fab5f89125f:/src/app# bundle config local.appmap /src/appmap-ruby
379
- root@6fab5f89125f:/src/app# bundle update appmap
406
+ root@6fab5f89125f:/src/app# bundle
380
407
  ```
381
408
 
382
409
  At this point, the bundle is built with the `appmap` gem located in `/src/appmap`, which is volume-mounted from the host.
@@ -391,4 +418,3 @@ Configuring AppMap from path appmap.yml
391
418
  Finished in 0.07357 seconds (files took 2.1 seconds to load)
392
419
  4 examples, 0 failures
393
420
  ```
394
-
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
@@ -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,8 +57,12 @@ 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[provider.secure_compare])),
61
- 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', package_name: 'action_view', labels: %w[mvc.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 = {
@@ -71,7 +75,7 @@ module AppMap
71
75
  'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.smtp protocol.email io])),
72
76
  'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.pop protocol.email io])),
73
77
  'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.imap protocol.email io])),
74
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization marshal])),
78
+ 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization])),
75
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])),
76
80
  'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
77
81
  'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
@@ -105,9 +109,9 @@ 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
114
+ end.compact
111
115
  Config.new config_data['name'], packages, config_data['exclude'] || []
112
116
  end
113
117
  end
@@ -137,7 +141,7 @@ module AppMap
137
141
  return unless location_file
138
142
 
139
143
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
140
- packages.find do |pkg|
144
+ packages.select { |pkg| pkg.path }.find do |pkg|
141
145
  (location_file.index(pkg.path) == 0) &&
142
146
  !pkg.exclude.find { |p| location_file.index(p) }
143
147
  end
data/lib/appmap/event.rb CHANGED
@@ -36,8 +36,29 @@ module AppMap
36
36
  (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
37
  end
38
38
 
39
+ def object_properties(hash_like)
40
+ hash = hash_like.to_h
41
+ hash.keys.map do |key|
42
+ {
43
+ name: key,
44
+ class: hash[key].class.name,
45
+ }
46
+ end
47
+ rescue
48
+ nil
49
+ end
50
+
39
51
  protected
40
52
 
53
+ # Heuristic for dynamically defined class whose name can be nil
54
+ def best_class_name(value)
55
+ value_cls = value.class
56
+ while value_cls.name.nil?
57
+ value_cls = value_cls.superclass
58
+ end
59
+ value_cls.name
60
+ end
61
+
41
62
  def custom_display_string(value)
42
63
  case value
43
64
  when File
@@ -70,6 +91,12 @@ module AppMap
70
91
  end
71
92
  end
72
93
  end
94
+
95
+ protected
96
+
97
+ def object_properties(hash_like)
98
+ self.class.object_properties(hash_like)
99
+ end
73
100
  end
74
101
 
75
102
  class MethodCall < MethodEvent
@@ -77,6 +104,7 @@ module AppMap
77
104
 
78
105
  class << self
79
106
  def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
107
+ defined_class ||= 'Class'
80
108
  mc.tap do
81
109
  static = receiver.is_a?(Module)
82
110
  mc.defined_class = defined_class
@@ -110,14 +138,14 @@ module AppMap
110
138
  end
111
139
  {
112
140
  name: param_name,
113
- class: value.class.name,
141
+ class: best_class_name(value),
114
142
  object_id: value.__id__,
115
143
  value: display_string(value),
116
144
  kind: param_type
117
145
  }
118
146
  end
119
147
  mc.receiver = {
120
- class: receiver.class.name,
148
+ class: best_class_name(receiver),
121
149
  object_id: receiver.__id__,
122
150
  value: display_string(receiver)
123
151
  }
@@ -172,7 +200,7 @@ module AppMap
172
200
  mr.tap do |_|
173
201
  if return_value
174
202
  mr.return_value = {
175
- class: return_value.class.name,
203
+ class: best_class_name(return_value),
176
204
  value: display_string(return_value),
177
205
  object_id: return_value.__id__
178
206
  }
@@ -183,7 +211,7 @@ module AppMap
183
211
  while next_exception
184
212
  exception_backtrace = next_exception.backtrace_locations.try(:[], 0)
185
213
  exceptions << {
186
- class: next_exception.class.name,
214
+ class: best_class_name(next_exception),
187
215
  message: next_exception.message,
188
216
  object_id: next_exception.__id__,
189
217
  path: exception_backtrace&.path,
data/lib/appmap/hook.rb CHANGED
@@ -46,7 +46,16 @@ module AppMap
46
46
  cls = trace_point.self
47
47
 
48
48
  instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
49
- class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
49
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
50
+ class_methods = begin
51
+ if cls.respond_to?(:singleton_class)
52
+ cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
53
+ else
54
+ []
55
+ end
56
+ rescue NameError
57
+ []
58
+ end
50
59
 
51
60
  hook = lambda do |hook_cls|
52
61
  lambda do |method_id|
@@ -80,7 +89,13 @@ module AppMap
80
89
  end
81
90
 
82
91
  instance_methods.each(&hook.(cls))
83
- class_methods.each(&hook.(cls.singleton_class))
92
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
93
+ begin
94
+ class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
95
+ rescue NameError
96
+ # NameError:
97
+ # uninitialized constant Faraday::Connection
98
+ end
84
99
  end
85
100
 
86
101
  tp.enable(&block)
@@ -35,7 +35,7 @@ module AppMap
35
35
  else
36
36
  "#{hook_method.name} (class resolution deferred)"
37
37
  end
38
- warn "AppMap: Hooking " + msg
38
+ warn "AppMap: Hooking #{msg} at line #{(hook_method.source_location || []).join(':')}"
39
39
  end
40
40
 
41
41
  defined_class = @defined_class
@@ -67,7 +67,7 @@ module AppMap
67
67
 
68
68
  response = JSON.generate \
69
69
  version: AppMap::APPMAP_FORMAT_VERSION,
70
- classMap: AppMap.class_map(tracer.event_methods),
70
+ classMap: AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
71
71
  metadata: metadata,
72
72
  events: @events
73
73
 
@@ -7,20 +7,25 @@ module AppMap
7
7
  # be activated around each test.
8
8
  module Minitest
9
9
  APPMAP_OUTPUT_DIR = 'tmp/appmap/minitest'
10
- LOG = false
10
+ LOG = ( ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true' )
11
11
 
12
12
  def self.metadata
13
13
  AppMap.detect_metadata
14
14
  end
15
15
 
16
- Recording = Struct.new(:test) do
17
- def initialize(test)
16
+ Recording = Struct.new(:test, :test_name) do
17
+ def initialize(test, test_name)
18
18
  super
19
19
 
20
- warn "Starting recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
20
+ warn "Starting recording of test #{test.class}.#{test.name}@#{source_location}" if AppMap::Minitest::LOG
21
21
  @trace = AppMap.tracing.trace
22
22
  end
23
23
 
24
+ def source_location
25
+ test.method(test_name).source_location.join(':')
26
+ end
27
+
28
+
24
29
  def finish
25
30
  warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
26
31
 
@@ -31,7 +36,7 @@ module AppMap
31
36
 
32
37
  AppMap::Minitest.add_event_methods @trace.event_methods
33
38
 
34
- class_map = AppMap.class_map(@trace.event_methods)
39
+ class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
35
40
 
36
41
  feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
37
42
  feature_name = test.name.split('_')[1..-1].join(' ')
@@ -39,9 +44,8 @@ module AppMap
39
44
 
40
45
  AppMap::Minitest.save scenario_name,
41
46
  class_map,
42
- events: events,
43
- feature_name: feature_name,
44
- feature_group_name: feature_group
47
+ source_location,
48
+ events: events
45
49
  end
46
50
  end
47
51
 
@@ -55,8 +59,8 @@ module AppMap
55
59
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
56
60
  end
57
61
 
58
- def begin_test(test)
59
- @recordings_by_test[test.object_id] = Recording.new(test)
62
+ def begin_test(test, name)
63
+ @recordings_by_test[test.object_id] = Recording.new(test, name)
60
64
  end
61
65
 
62
66
  def end_test(test)
@@ -74,12 +78,11 @@ module AppMap
74
78
  @event_methods += event_methods
75
79
  end
76
80
 
77
- def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
81
+ def save(example_name, class_map, source_location, events: nil, labels: nil)
78
82
  metadata = AppMap::Minitest.metadata.tap do |m|
79
83
  m[:name] = example_name
84
+ m[:source_location] = source_location
80
85
  m[:app] = AppMap.configuration.name
81
- m[:feature] = feature_name if feature_name
82
- m[:feature_group] = feature_group_name if feature_group_name
83
86
  m[:frameworks] ||= []
84
87
  m[:frameworks] << {
85
88
  name: 'minitest',
@@ -128,7 +131,7 @@ if AppMap::Minitest.enabled?
128
131
  alias run_without_hook run
129
132
 
130
133
  def run
131
- AppMap::Minitest.begin_test self
134
+ AppMap::Minitest.begin_test self, name
132
135
  begin
133
136
  run_without_hook
134
137
  ensure
@@ -6,15 +6,38 @@ require 'appmap/hook'
6
6
  module AppMap
7
7
  module Rails
8
8
  module RequestHandler
9
+ # Host and User-Agent will just introduce needless variation.
10
+ # Content-Type and Authorization get their own fields in the request.
11
+ IGNORE_HEADERS = %w[host user_agent content_type authorization].map(&:upcase).map {|h| "HTTP_#{h}"}.freeze
12
+
13
+ class << self
14
+ def selected_headers(env)
15
+ # Rack prepends HTTP_ to all client-sent headers.
16
+ matching_headers = env
17
+ .select { |k,v| k.start_with? 'HTTP_'}
18
+ .reject { |k,v| IGNORE_HEADERS.member?(k) }
19
+ .reject { |k,v| v.blank? }
20
+ .each_with_object({}) do |kv, memo|
21
+ key = kv[0].sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
22
+ value = kv[1]
23
+ memo[key] = value
24
+ end
25
+ matching_headers.blank? ? nil : matching_headers
26
+ end
27
+ end
28
+
9
29
  class HTTPServerRequest < AppMap::Event::MethodEvent
10
- attr_accessor :normalized_path_info, :request_method, :path_info, :params
30
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
11
31
 
12
32
  def initialize(request)
13
33
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
34
 
15
- @request_method = request.request_method
16
- @normalized_path_info = normalized_path request
17
- @path_info = request.path_info.split('?')[0]
35
+ self.request_method = request.request_method
36
+ self.normalized_path_info = normalized_path(request)
37
+ self.mime_type = request.headers['Content-Type']
38
+ self.headers = RequestHandler.selected_headers(request.env)
39
+ self.authorization = request.headers['Authorization']
40
+ self.path_info = request.path_info.split('?')[0]
18
41
  # ActionDispatch::Http::ParameterFilter is deprecated
19
42
  parameter_filter_cls = \
20
43
  if defined?(ActiveSupport::ParameterFilter)
@@ -22,7 +45,7 @@ module AppMap
22
45
  else
23
46
  ActionDispatch::Http::ParameterFilter
24
47
  end
25
- @params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
48
+ self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
26
49
  end
27
50
 
28
51
  def to_h
@@ -30,7 +53,10 @@ module AppMap
30
53
  h[:http_server_request] = {
31
54
  request_method: request_method,
32
55
  path_info: path_info,
33
- normalized_path_info: normalized_path_info
56
+ mime_type: mime_type,
57
+ normalized_path_info: normalized_path_info,
58
+ authorization: authorization,
59
+ headers: headers,
34
60
  }.compact
35
61
 
36
62
  h[:message] = params.keys.map do |key|
@@ -39,8 +65,11 @@ module AppMap
39
65
  name: key,
40
66
  class: val.class.name,
41
67
  value: self.class.display_string(val),
42
- object_id: val.__id__
43
- }
68
+ object_id: val.__id__,
69
+ }.tap do |message|
70
+ properties = object_properties(val)
71
+ message[:properties] = properties if properties
72
+ end
44
73
  end
45
74
  end
46
75
  end
@@ -59,7 +88,7 @@ module AppMap
59
88
  end
60
89
 
61
90
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
62
- attr_accessor :status, :mime_type
91
+ attr_accessor :status, :mime_type, :headers
63
92
 
64
93
  def initialize(response, parent_id, elapsed)
65
94
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
@@ -68,13 +97,15 @@ module AppMap
68
97
  self.mime_type = response.headers['Content-Type']
69
98
  self.parent_id = parent_id
70
99
  self.elapsed = elapsed
100
+ self.headers = RequestHandler.selected_headers(response.headers)
71
101
  end
72
102
 
73
103
  def to_h
74
104
  super.tap do |h|
75
105
  h[:http_server_response] = {
76
106
  status: status,
77
- mime_type: mime_type
107
+ mime_type: mime_type,
108
+ headers: headers
78
109
  }.compact
79
110
  end
80
111
  end