appmap 0.42.1 → 0.46.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +33 -2
  4. data/CHANGELOG.md +44 -0
  5. data/README.md +74 -16
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +3 -7
  10. data/lib/appmap/class_map.rb +11 -22
  11. data/lib/appmap/command/record.rb +1 -1
  12. data/lib/appmap/config.rb +180 -67
  13. data/lib/appmap/cucumber.rb +1 -1
  14. data/lib/appmap/event.rb +46 -27
  15. data/lib/appmap/handler/function.rb +19 -0
  16. data/lib/appmap/handler/net_http.rb +107 -0
  17. data/lib/appmap/handler/rails/request_handler.rb +124 -0
  18. data/lib/appmap/handler/rails/sql_handler.rb +152 -0
  19. data/lib/appmap/handler/rails/template.rb +149 -0
  20. data/lib/appmap/hook.rb +111 -70
  21. data/lib/appmap/hook/method.rb +6 -8
  22. data/lib/appmap/middleware/remote_recording.rb +1 -1
  23. data/lib/appmap/minitest.rb +22 -20
  24. data/lib/appmap/railtie.rb +5 -5
  25. data/lib/appmap/record.rb +1 -1
  26. data/lib/appmap/rspec.rb +22 -21
  27. data/lib/appmap/trace.rb +47 -6
  28. data/lib/appmap/util.rb +47 -2
  29. data/lib/appmap/version.rb +2 -2
  30. data/package-lock.json +3 -3
  31. data/release.sh +17 -0
  32. data/spec/abstract_controller_base_spec.rb +140 -34
  33. data/spec/class_map_spec.rb +5 -13
  34. data/spec/config_spec.rb +33 -1
  35. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  36. data/spec/fixtures/hook/method_named_call.rb +11 -0
  37. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  38. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  39. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  40. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  41. data/spec/fixtures/rails5_users_app/create_app +8 -2
  42. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  43. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  44. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  45. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  46. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  47. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  48. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  49. data/spec/fixtures/rails6_users_app/create_app +8 -2
  50. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  51. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  52. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  53. data/spec/hook_spec.rb +143 -22
  54. data/spec/record_net_http_spec.rb +160 -0
  55. data/spec/record_sql_rails_pg_spec.rb +1 -1
  56. data/spec/spec_helper.rb +16 -0
  57. data/test/expectations/openssl_test_key_sign1.json +2 -4
  58. data/test/gem_test.rb +1 -1
  59. data/test/rspec_test.rb +0 -13
  60. metadata +20 -14
  61. data/exe/appmap +0 -154
  62. data/lib/appmap/rails/request_handler.rb +0 -109
  63. data/lib/appmap/rails/sql_handler.rb +0 -150
  64. data/test/cli_test.rb +0 -116
@@ -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, include_source: AppMap.include_source?),
30
+ AppMap.class_map(tracer.event_methods),
31
31
  events
32
32
  end
33
33
 
data/lib/appmap/config.rb CHANGED
@@ -1,8 +1,32 @@
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.
23
+ attr_writer :handler_class
24
+
25
+ def handler_class
26
+ require 'appmap/handler/function'
27
+ @handler_class || AppMap::Handler::Function
28
+ end
29
+
6
30
  # Indicates that only the entry points to a package will be recorded.
7
31
  # Once the code has entered a package, subsequent calls within the package will not be
8
32
  # recorded unless the code leaves the package and re-enters it.
@@ -11,25 +35,36 @@ module AppMap
11
35
  end
12
36
 
13
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.
14
40
  def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
15
41
  Package.new(path, nil, package_name, exclude, labels, shallow)
16
42
  end
17
43
 
18
- def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
19
- 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)
20
48
  warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
21
49
  return
22
50
  end
23
- 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
24
57
  end
25
58
 
26
59
  private_class_method :new
27
60
 
28
61
  protected
29
62
 
30
- def gem_path(gem)
31
- gemspec = Gem.loaded_specs[gem] or raise "Gem #{gem.inspect} not found"
32
- 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
33
68
  end
34
69
  end
35
70
 
@@ -42,6 +77,7 @@ module AppMap
42
77
  path: path,
43
78
  package_name: package_name,
44
79
  gem: gem,
80
+ handler_class: handler_class.name,
45
81
  exclude: exclude.blank? ? nil : exclude,
46
82
  labels: labels.blank? ? nil : labels,
47
83
  shallow: shallow
@@ -49,44 +85,109 @@ module AppMap
49
85
  end
50
86
  end
51
87
 
52
- Hook = Struct.new(:method_names, :package) do
88
+ Function = Struct.new(:package, :cls, :labels, :function_names) do # :nodoc:
89
+ def to_h
90
+ {
91
+ package: package,
92
+ class: cls,
93
+ labels: labels,
94
+ functions: function_names.map(&:to_sym)
95
+ }.compact
96
+ end
53
97
  end
98
+ private_constant :Function
54
99
 
55
- OPENSSL_PACKAGES = Package.build_from_path('openssl', package_name: 'openssl', labels: %w[security crypto])
100
+ class TargetMethods # :nodoc:
101
+ attr_reader :method_names, :package
102
+
103
+ def initialize(method_names, package)
104
+ @method_names = method_names
105
+ @package = package
106
+ end
107
+
108
+ def include_method?(method_name)
109
+ Array(method_names).include?(method_name)
110
+ end
111
+
112
+ def to_h
113
+ {
114
+ package: package.name,
115
+ method_names: method_names
116
+ }
117
+ end
118
+ end
119
+ private_constant :TargetMethods
120
+
121
+ OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
56
122
 
57
123
  # Methods that should always be hooked, with their containing
58
124
  # package and labels that should be applied to them.
59
125
  HOOKED_METHODS = {
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])),
126
+ 'ActionView::Renderer' => TargetMethods.new(:render, Package.build_from_gem('actionview', shallow: false, package_name: 'action_view', labels: %w[mvc.view], optional: true).tap do |package|
127
+ package.handler_class = AppMap::Handler::Rails::Template::RenderHandler if package
128
+ end),
129
+ 'ActionView::Resolver' => TargetMethods.new(%i[find_all find_all_anywhere], Package.build_from_gem('actionview', shallow: false, package_name: 'action_view', labels: %w[mvc.template.resolver], optional: true).tap do |package|
130
+ package.handler_class = AppMap::Handler::Rails::Template::ResolverHandler if package
131
+ end),
132
+ 'ActionDispatch::Request::Session' => TargetMethods.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.session], optional: true)),
133
+ 'ActionDispatch::Cookies::CookieJar' => TargetMethods.new(%i[[]= clear update delete recycle], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.cookie], optional: true)),
134
+ 'ActionDispatch::Cookies::EncryptedCookieJar' => TargetMethods.new(%i[[]=], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_dispatch', labels: %w[http.cookie crypto.encrypt], optional: true)),
135
+ 'CanCan::ControllerAdditions' => TargetMethods.new(%i[authorize! can? cannot?], Package.build_from_gem('cancancan', shallow: false, labels: %w[security.authorization], optional: true)),
136
+ 'CanCan::Ability' => TargetMethods.new(%i[authorize!], Package.build_from_gem('cancancan', shallow: false, labels: %w[security.authorization], optional: true)),
137
+ 'ActionController::Instrumentation' => [
138
+ TargetMethods.new(%i[process_action send_file send_data redirect_to], Package.build_from_gem('actionpack', shallow: false, package_name: 'action_controller', labels: %w[mvc.controller], optional: true))
139
+ ]
66
140
  }.freeze
67
141
 
68
142
  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])),
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])),
143
+ 'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
144
+ 'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
145
+ 'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
146
+ 'OpenSSL::Cipher' => [
147
+ TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
148
+ TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
149
+ ],
150
+ 'ActiveSupport::Callbacks::CallbackSequence' => [
151
+ TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.before_action])),
152
+ TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.after_action])),
153
+ ],
154
+ 'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, package_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
155
+ 'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
156
+ 'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
157
+ package.handler_class = AppMap::Handler::NetHTTP
158
+ end),
159
+ 'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
160
+ 'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
161
+ # This is happening: Method send_command not found on Net::IMAP
162
+ # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
163
+ # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
164
+ '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])),
165
+ 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
166
+ 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
82
167
  }.freeze
83
168
 
84
- attr_reader :name, :packages, :exclude
169
+ attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_methods
85
170
 
86
- def initialize(name, packages = [], exclude = [])
171
+ def initialize(name, packages, exclude: [], functions: [])
87
172
  @name = name
88
173
  @packages = packages
174
+ @hook_paths = packages.map(&:path)
89
175
  @exclude = exclude
176
+ @builtin_methods = BUILTIN_METHODS
177
+ @functions = functions
178
+ @hooked_methods = HOOKED_METHODS.dup
179
+ functions.each do |func|
180
+ package_options = {}
181
+ package_options[:labels] = func.labels if func.labels
182
+ @hooked_methods[func.cls] ||= []
183
+ @hooked_methods[func.cls] << TargetMethods.new(func.function_names, Package.build_from_path(func.package, package_options))
184
+ end
185
+
186
+ @hooked_methods.each_value do |hooks|
187
+ Array(hooks).each do |hook|
188
+ @hook_paths << hook.package.path if hook.package
189
+ end
190
+ end
90
191
  end
91
192
 
92
193
  class << self
@@ -98,6 +199,16 @@ module AppMap
98
199
 
99
200
  # Loads configuration from a Hash.
100
201
  def load(config_data)
202
+ functions = (config_data['functions'] || []).map do |function_data|
203
+ package = function_data['package']
204
+ cls = function_data['class']
205
+ functions = function_data['function'] || function_data['functions']
206
+ raise 'AppMap class configuration should specify package, class and function(s)' unless package && cls && functions
207
+ functions = Array(functions).map(&:to_sym)
208
+ labels = function_data['label'] || function_data['labels']
209
+ labels = Array(labels).map(&:to_s) if labels
210
+ Function.new(package, cls, labels, functions)
211
+ end
101
212
  packages = (config_data['packages'] || []).map do |package|
102
213
  gem = package['gem']
103
214
  path = package['path']
@@ -112,7 +223,8 @@ module AppMap
112
223
  Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
113
224
  end
114
225
  end.compact
115
- Config.new config_data['name'], packages, config_data['exclude'] || []
226
+ exclude = config_data['exclude'] || []
227
+ Config.new config_data['name'], packages, exclude: exclude, functions: functions
116
228
  end
117
229
  end
118
230
 
@@ -120,58 +232,59 @@ module AppMap
120
232
  {
121
233
  name: name,
122
234
  packages: packages.map(&:to_h),
235
+ functions: @functions.map(&:to_h),
123
236
  exclude: exclude
124
237
  }
125
238
  end
126
239
 
127
- # package_for_method finds the Package, if any, which configures the hook
128
- # for a method.
129
- def package_for_method(method)
130
- package_hooked_by_class(method) || package_hooked_by_source_location(method)
240
+ # Determines if methods defined in a file path should possibly be hooked.
241
+ def path_enabled?(path)
242
+ path = AppMap::Util.normalize_path(path)
243
+ @hook_paths.find { |hook_path| path.index(hook_path) == 0 }
131
244
  end
132
245
 
133
- def package_hooked_by_class(method)
134
- defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
135
- return find_package(defined_class, method_name)
136
- end
246
+ # Looks up a class and method in the config, to find the matching Package configuration.
247
+ # This class is only used after +path_enabled?+ has returned `true`.
248
+ LookupPackage = Struct.new(:config, :cls, :method) do
249
+ def package
250
+ # Global "excludes" configuration can be used to ignore any class/method.
251
+ return if config.never_hook?(cls, method)
137
252
 
138
- def package_hooked_by_source_location(method)
139
- location = method.source_location
140
- location_file, = location
141
- return unless location_file
142
-
143
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
144
- packages.select { |pkg| pkg.path }.find do |pkg|
145
- (location_file.index(pkg.path) == 0) &&
146
- !pkg.exclude.find { |p| location_file.index(p) }
253
+ package_for_code_object || package_for_location
147
254
  end
148
- end
149
255
 
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)
153
- end
256
+ # Hook a method which is specified by class and method name.
257
+ def package_for_code_object
258
+ Array(config.hooked_methods[cls.name])
259
+ .compact
260
+ .find { |hook| hook.include_method?(method.name) }
261
+ &.package
262
+ end
154
263
 
155
- # always_hook? indicates a method that should always be hooked.
156
- def always_hook?(defined_class, method_name)
157
- !!find_package(defined_class, method_name)
158
- end
264
+ # Hook a method which is specified by code location (i.e. path).
265
+ def package_for_location
266
+ location = method.source_location
267
+ location_file, = location
268
+ return unless location_file
159
269
 
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)
270
+ location_file = AppMap::Util.normalize_path(location_file)
271
+ config
272
+ .packages
273
+ .select { |pkg| pkg.path }
274
+ .find do |pkg|
275
+ (location_file.index(pkg.path) == 0) &&
276
+ !pkg.exclude.find { |p| location_file.index(p) }
277
+ end
278
+ end
164
279
  end
165
280
 
166
- def find_package(defined_class, method_name)
167
- hook = find_hook(defined_class)
168
- return nil unless hook
169
-
170
- Array(hook.method_names).include?(method_name) ? hook.package : nil
281
+ def lookup_package(cls, method)
282
+ LookupPackage.new(self, cls, method).package
171
283
  end
172
284
 
173
- def find_hook(defined_class)
174
- HOOKED_METHODS[defined_class] || BUILTIN_METHODS[defined_class]
285
+ def never_hook?(cls, method)
286
+ _, separator, = ::AppMap::Hook.qualify_method_name(method)
287
+ return true if exclude.member?(cls.name) || exclude.member?([ cls.name, separator, method.name ].join)
175
288
  end
176
289
  end
177
290
  end
@@ -50,7 +50,7 @@ module AppMap
50
50
  appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
51
51
  scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
52
52
 
53
- File.write(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
53
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
54
54
  end
55
55
 
56
56
  def enabled?
data/lib/appmap/event.rb CHANGED
@@ -21,10 +21,10 @@ module AppMap
21
21
  LIMIT = 100
22
22
 
23
23
  class << self
24
- def build_from_invocation(me, event_type)
25
- me.id = AppMap::Event.next_id_counter
26
- me.event = event_type
27
- me.thread_id = Thread.current.object_id
24
+ def build_from_invocation(event_type, event:)
25
+ event.id = AppMap::Event.next_id_counter
26
+ event.event = event_type
27
+ event.thread_id = Thread.current.object_id
28
28
  end
29
29
 
30
30
  # Gets a display string for a value. This is not meant to be a machine deserializable value.
@@ -36,7 +36,17 @@ module AppMap
36
36
  (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
37
  end
38
38
 
39
- protected
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
40
50
 
41
51
  # Heuristic for dynamically defined class whose name can be nil
42
52
  def best_class_name(value)
@@ -79,25 +89,32 @@ module AppMap
79
89
  end
80
90
  end
81
91
  end
92
+
93
+ protected
94
+
95
+ def object_properties(hash_like)
96
+ self.class.object_properties(hash_like)
97
+ end
82
98
  end
83
99
 
84
100
  class MethodCall < MethodEvent
85
101
  attr_accessor :defined_class, :method_id, :path, :lineno, :parameters, :receiver, :static
86
102
 
87
103
  class << self
88
- def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
104
+ def build_from_invocation(defined_class, method, receiver, arguments, event: MethodCall.new)
105
+ event ||= MethodCall.new
89
106
  defined_class ||= 'Class'
90
- mc.tap do
107
+ event.tap do
91
108
  static = receiver.is_a?(Module)
92
- mc.defined_class = defined_class
93
- mc.method_id = method.name.to_s
109
+ event.defined_class = defined_class
110
+ event.method_id = method.name.to_s
94
111
  if method.source_location
95
112
  path = method.source_location[0]
96
113
  path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
97
- mc.path = path
98
- mc.lineno = method.source_location[1]
114
+ event.path = path
115
+ event.lineno = method.source_location[1]
99
116
  else
100
- mc.path = [ defined_class, static ? '.' : '#', method.name ].join
117
+ event.path = [ defined_class, static ? '.' : '#', method.name ].join
101
118
  end
102
119
 
103
120
  # Check if the method has key parameters. If there are any they'll always be last.
@@ -105,7 +122,7 @@ module AppMap
105
122
  has_key = [[:dummy], *method.parameters].last.first.to_s.start_with?('key') && arguments[-1].is_a?(Hash)
106
123
  kwargs = has_key && arguments[-1].dup || {}
107
124
 
108
- mc.parameters = method.parameters.map.with_index do |method_param, idx|
125
+ event.parameters = method.parameters.map.with_index do |method_param, idx|
109
126
  param_type, param_name = method_param
110
127
  param_name ||= 'arg'
111
128
  value = case param_type
@@ -126,13 +143,13 @@ module AppMap
126
143
  kind: param_type
127
144
  }
128
145
  end
129
- mc.receiver = {
146
+ event.receiver = {
130
147
  class: best_class_name(receiver),
131
148
  object_id: receiver.__id__,
132
149
  value: display_string(receiver)
133
150
  }
134
- mc.static = static
135
- MethodEvent.build_from_invocation(mc, :call)
151
+ event.static = static
152
+ MethodEvent.build_from_invocation(:call, event: event)
136
153
  end
137
154
  end
138
155
  end
@@ -157,11 +174,12 @@ module AppMap
157
174
  attr_accessor :parent_id, :elapsed
158
175
 
159
176
  class << self
160
- def build_from_invocation(mr = MethodReturnIgnoreValue.new, parent_id, elapsed)
161
- mr.tap do |_|
162
- mr.parent_id = parent_id
163
- mr.elapsed = elapsed
164
- MethodEvent.build_from_invocation(mr, :return)
177
+ def build_from_invocation(parent_id, elapsed: nil, event: MethodReturnIgnoreValue.new)
178
+ event ||= MethodReturnIgnoreValue.new
179
+ event.tap do |_|
180
+ event.parent_id = parent_id
181
+ event.elapsed = elapsed
182
+ MethodEvent.build_from_invocation(:return, event: event)
165
183
  end
166
184
  end
167
185
  end
@@ -169,7 +187,7 @@ module AppMap
169
187
  def to_h
170
188
  super.tap do |h|
171
189
  h[:parent_id] = parent_id
172
- h[:elapsed] = elapsed
190
+ h[:elapsed] = elapsed if elapsed
173
191
  end
174
192
  end
175
193
  end
@@ -178,10 +196,11 @@ module AppMap
178
196
  attr_accessor :return_value, :exceptions
179
197
 
180
198
  class << self
181
- def build_from_invocation(mr = MethodReturn.new, parent_id, elapsed, return_value, exception)
182
- mr.tap do |_|
199
+ def build_from_invocation(parent_id, return_value, exception, elapsed: nil, event: MethodReturn.new)
200
+ event ||= MethodReturn.new
201
+ event.tap do |_|
183
202
  if return_value
184
- mr.return_value = {
203
+ event.return_value = {
185
204
  class: best_class_name(return_value),
186
205
  value: display_string(return_value),
187
206
  object_id: return_value.__id__
@@ -202,9 +221,9 @@ module AppMap
202
221
  next_exception = next_exception.cause
203
222
  end
204
223
 
205
- mr.exceptions = exceptions
224
+ event.exceptions = exceptions
206
225
  end
207
- MethodReturnIgnoreValue.build_from_invocation(mr, parent_id, elapsed)
226
+ MethodReturnIgnoreValue.build_from_invocation(parent_id, elapsed: elapsed, event: event)
208
227
  end
209
228
  end
210
229
  end