appmap 0.42.1 → 0.46.0

Sign up to get free protection for your applications and to get access to all the features.
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