appmap 0.34.2 → 0.35.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81605d2c95140e963991e04af89235ee903cce0e3a86b2477f0f80961704e2d6
4
- data.tar.gz: 7e526d9ffbc559bb3968b8a973dcd757147676d17a5028f737192fd8956d1b43
3
+ metadata.gz: 9e7d5c58bd3addf395c32591fe468c4a0b0e886d715cb711ffb6c898049455e5
4
+ data.tar.gz: 271ac50ee1ebe139ed8e52262a98a13e6b3d471ddcab49316669e66d0b7be57a
5
5
  SHA512:
6
- metadata.gz: 6a34ba2662d48a7e0083e16d6abe4693b57711f9ac0020da6631ca6c40172aeb8f3ef734edd504cec00f2f437742e45fc0cf631b425cb960c9d51239556bc461
7
- data.tar.gz: 156f126aac6f63e819c175d6b4d481679cbced9184007871c12fb174f187198afd8e7788befc4ab0ff35b10293e1e163eee25a985b35bd93853c03aa8268d192
6
+ metadata.gz: b84b1d76d8890a72fb376e95c7ab3811814b75e99304d2f1c15bb091cba1aef1e81f7688fd69c35b8c7735fd72167f89c67eeb25f76e54253503be610fff52cc
7
+ data.tar.gz: 12b27887ae25d8f91fa6321ea59c222c7ec7d1e2d6a198f6672fa166fcd3c32456fe0d3969dda729d4c4817129a3eb874f8bae0afb5dca6b74253ccf457bd51b
@@ -1,3 +1,32 @@
1
+ # v0.35.2
2
+ * Make sure `MethodEvent#display_string` works when the value's `#to_s` and/or `#inspect`
3
+ methods have problems.
4
+
5
+ # v0.35.1
6
+ * Take out hooking of `IO` and `Logger` methods.
7
+ * Enable logging if either `APPMAP_DEBUG` or `DEBUG` is `true`.
8
+
9
+ # v0.35.0
10
+ * Provide a custom display string for files and HTTP requests.
11
+ * Report `mime_type` on HTTP response.
12
+
13
+ # v0.34.6
14
+ * Only warn once about problems determining database version for an ActiveRecord
15
+ connection.
16
+
17
+ # v0.34.5
18
+ * Ensure that hooking a method doesn't change its arity.
19
+
20
+ # v0.34.4
21
+ * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
22
+ `ActiveRecord::Base.connection.database_version` if it's available.
23
+ * Fix `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.database_type` returns `:postgres`
24
+ in all supported versions of Rails.
25
+
26
+ # v0.34.3
27
+ * Fix a crash in `singleton_method_owner_name` that occurred if `__attached__.class` returned
28
+ something other than a `Module` or a `Class`.
29
+
1
30
  # v0.34.2
2
31
  * Add an extension that gets the name of the owner of a singleton method without calling
3
32
  any methods that may have been redefined (e.g. `#to_s` or `.name`).
data/README.md CHANGED
@@ -57,6 +57,15 @@ end
57
57
 
58
58
  Then install with `bundle`.
59
59
 
60
+ **Railtie**
61
+
62
+ If you are using Ruby on Rails, require the railtie after Rails is loaded.
63
+
64
+ ```
65
+ # application.rb is a good place to do this, along with all the other railties.
66
+ require 'appmap/railtie'
67
+ ```
68
+
60
69
  # Configuration
61
70
 
62
71
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -46,4 +46,5 @@ Gem::Specification.new do |spec|
46
46
  spec.add_development_dependency 'selenium-webdriver'
47
47
  spec.add_development_dependency 'webdrivers', '~> 4.0'
48
48
  spec.add_development_dependency 'timecop'
49
+ spec.add_development_dependency 'hashie'
49
50
  end
@@ -8,19 +8,88 @@
8
8
  (!SPECIAL_CONST_P(obj) && \
9
9
  (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE))
10
10
 
11
- static VALUE singleton_method_owner_name(VALUE klass, VALUE method)
11
+ #define ARITIES_KEY "__arities__"
12
+
13
+ VALUE am_AppMapHook;
14
+
15
+ static VALUE
16
+ singleton_method_owner_name(VALUE klass, VALUE method)
12
17
  {
13
18
  VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
14
19
  VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
15
20
  if (!CLASS_OR_MODULE_P(attached)) {
16
21
  attached = rb_funcall(attached, rb_intern("class"), 0);
17
22
  }
18
- return rb_mod_name(attached);
23
+
24
+ // Did __attached__.class return an object that's a Module or a
25
+ // Class?
26
+ if (CLASS_OR_MODULE_P(attached)) {
27
+ // Yup, get it's name
28
+ return rb_mod_name(attached);
29
+ }
30
+
31
+ // Nope (which seems weird, but whatever). Fall back to calling
32
+ // #to_s on the method's owner and hope for the best.
33
+ return rb_funcall(owner, rb_intern("to_s"), 0);
34
+ }
35
+
36
+
37
+ static VALUE
38
+ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
+ {
40
+ VALUE arities_key = rb_intern(ARITIES_KEY);
41
+ VALUE arities = rb_ivar_get(mod, arities_key);
42
+
43
+ if (arities == Qundef || NIL_P(arities)) {
44
+ arities = rb_hash_new();
45
+ rb_ivar_set(mod, arities_key, arities);
46
+ }
47
+ rb_hash_aset(arities, name, arity);
48
+
49
+ return rb_funcall(mod, rb_intern("define_method"), 2, name, proc);
50
+ }
51
+
52
+ static VALUE
53
+ am_get_method_arity(VALUE method, VALUE orig_arity_method)
54
+ {
55
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
56
+ VALUE arities = rb_ivar_get(owner, rb_intern(ARITIES_KEY));
57
+ VALUE name = rb_funcall(method, rb_intern("name"), 0);
58
+ VALUE arity = Qnil;
59
+ // See if we saved an arity for the method.
60
+ if (!NIL_P(arities)) {
61
+ arity = rb_hash_aref(arities, name);
62
+ }
63
+ // Didn't find one, call the original method.
64
+ if (NIL_P(arity)) {
65
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
+ }
68
+
69
+ return arity;
19
70
  }
20
-
71
+
72
+ static VALUE
73
+ am_unbound_method_arity(VALUE method)
74
+ {
75
+ VALUE orig_unbound_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@unbound_method_arity"));
76
+ return am_get_method_arity(method, orig_unbound_method_arity);
77
+ }
78
+
79
+ static VALUE
80
+ am_method_arity(VALUE method)
81
+ {
82
+ VALUE orig_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@method_arity"));
83
+ return am_get_method_arity(method, orig_method_arity);
84
+ }
85
+
21
86
  void Init_appmap() {
22
87
  VALUE appmap = rb_define_module("AppMap");
23
- VALUE hook = rb_define_class_under(appmap, "Hook", rb_cObject);
88
+ am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
+
90
+ rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
24
91
 
25
- rb_define_singleton_method(hook, "singleton_method_owner_name", singleton_method_owner_name, 1);
92
+ rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
+ rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
94
+ rb_define_method(rb_cMethod, "arity", am_method_arity, 0);
26
95
  }
@@ -84,7 +84,7 @@ module AppMap
84
84
 
85
85
  # Builds a class map from a config and a list of Ruby methods.
86
86
  def class_map(methods)
87
- ClassMap.build_from_methods(configuration, methods)
87
+ ClassMap.build_from_methods(methods)
88
88
  end
89
89
 
90
90
  # Returns default metadata detected from the Ruby system and from the
@@ -61,25 +61,24 @@ module AppMap
61
61
  location: location,
62
62
  static: static,
63
63
  labels: labels
64
- }.delete_if { |k,v| v.nil? || v == [] }
64
+ }.delete_if { |_, v| v.nil? || v == [] }
65
65
  end
66
66
  end
67
67
  end
68
68
 
69
69
  class << self
70
- def build_from_methods(config, methods)
70
+ def build_from_methods(methods)
71
71
  root = Types::Root.new
72
72
  methods.each do |method|
73
- package = config.package_for_method(method) \
74
- or raise "No package found for method #{method}"
75
- add_function root, package, method
73
+ add_function root, method
76
74
  end
77
75
  root.children.map(&:to_h)
78
76
  end
79
77
 
80
78
  protected
81
79
 
82
- def add_function(root, package, method)
80
+ def add_function(root, method)
81
+ package = method.package
83
82
  static = method.static
84
83
 
85
84
  object_infos = [
@@ -6,7 +6,7 @@ module AppMap
6
6
  def initialize(path, package_name: nil, exclude: [], labels: [])
7
7
  super path, package_name, exclude, labels
8
8
  end
9
-
9
+
10
10
  def to_h
11
11
  {
12
12
  path: path,
@@ -35,12 +35,10 @@ module AppMap
35
35
  'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGE),
36
36
  'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGE),
37
37
  'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGE),
38
- 'Logger' => Hook.new(:add, Package.new('logger', labels: %w[log io])),
39
38
  'Net::HTTP' => Hook.new(:request, Package.new('net/http', package_name: 'net/http', labels: %w[http io])),
40
39
  'Net::SMTP' => Hook.new(:send, Package.new('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
41
40
  'Net::POP3' => Hook.new(:mails, Package.new('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
42
41
  'Net::IMAP' => Hook.new(:send_command, Package.new('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
43
- 'IO' => Hook.new(%i[read write open close], Package.new('io', labels: %w[io])),
44
42
  'Marshal' => Hook.new(%i[dump load], Package.new('marshal', labels: %w[serialization marshal])),
45
43
  'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.new('yaml', package_name: 'psych', labels: %w[serialization yaml])),
46
44
  'JSON::Ext::Parser' => Hook.new(:parse, Package.new('json', package_name: 'json', labels: %w[serialization json])),
@@ -105,7 +103,7 @@ module AppMap
105
103
  hook = find_hook(defined_class)
106
104
  return nil unless hook
107
105
 
108
- Array(hook.method_names).include?(method_name) ? hook.package : nil
106
+ Array(hook.method_names).include?(method_name) ? hook.package : nil
109
107
  end
110
108
 
111
109
  def find_hook(defined_class)
@@ -31,25 +31,43 @@ module AppMap
31
31
  def display_string(value)
32
32
  return nil unless value
33
33
 
34
+ value_string = custom_display_string(value) || default_display_string(value)
35
+
36
+ (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
+ end
38
+
39
+ protected
40
+
41
+ def custom_display_string(value)
42
+ case value
43
+ when File
44
+ "#{value.class}[path=#{value.path}]"
45
+ when Net::HTTP
46
+ "#{value.class}[#{value.address}:#{value.port}]"
47
+ when Net::HTTPGenericRequest
48
+ "#{value.class}[#{value.method} #{value.path}]"
49
+ end
50
+ rescue StandardError
51
+ nil
52
+ end
53
+
54
+ def default_display_string(value)
34
55
  last_resort_string = lambda do
35
56
  warn "AppMap encountered an error inspecting a #{value.class.name}: #{$!.message}"
36
57
  '*Error inspecting variable*'
37
58
  end
38
59
 
39
- value_string = \
60
+ begin
61
+ value.to_s
62
+ rescue NoMethodError
40
63
  begin
41
- value.to_s
42
- rescue NoMethodError
43
- begin
44
- value.inspect
45
- rescue StandardError
46
- last_resort_string.call
47
- end
64
+ value.inspect
48
65
  rescue StandardError
49
66
  last_resort_string.call
50
67
  end
51
-
52
- (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
68
+ rescue StandardError
69
+ last_resort_string.call
70
+ end
53
71
  end
54
72
  end
55
73
  end
@@ -4,7 +4,10 @@ require 'English'
4
4
 
5
5
  module AppMap
6
6
  class Hook
7
- LOG = (ENV['DEBUG'] == 'true')
7
+ LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
+
9
+ @unbound_method_arity = ::UnboundMethod.instance_method(:arity)
10
+ @method_arity = ::Method.instance_method(:arity)
8
11
 
9
12
  class << self
10
13
  def lock_builtins
@@ -45,7 +48,6 @@ module AppMap
45
48
  hook = lambda do |hook_cls|
46
49
  lambda do |method_id|
47
50
  method = hook_cls.public_instance_method(method_id)
48
- hook_method = Hook::Method.new(hook_cls, method)
49
51
 
50
52
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
51
53
 
@@ -53,14 +55,16 @@ module AppMap
53
55
  # Skip methods that have no instruction sequence, as they are obviously trivial.
54
56
  next unless disasm
55
57
 
56
- # Don't try and trace the AppMap methods or there will be
57
- # a stack overflow in the defined hook method.
58
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
59
-
60
58
  next unless \
61
59
  config.always_hook?(hook_cls, method.name) ||
62
60
  config.included_by_location?(method)
63
61
 
62
+ hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
63
+
64
+ # Don't try and trace the AppMap methods or there will be
65
+ # a stack overflow in the defined hook method.
66
+ next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
67
+
64
68
  hook_method.activate
65
69
  end
66
70
  end
@@ -94,9 +98,9 @@ module AppMap
94
98
  end
95
99
 
96
100
  if method
97
- Hook::Method.new(cls, method).activate
101
+ Hook::Method.new(hook.package, cls, method).activate
98
102
  else
99
- warn "Method #{method_name} not found on #{cls.name}"
103
+ warn "Method #{method_name} not found on #{cls.name}"
100
104
  end
101
105
  end
102
106
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppMap
2
4
  class Hook
3
5
  class Method
4
- attr_reader :hook_class, :hook_method
6
+ attr_reader :hook_package, :hook_class, :hook_method
5
7
 
6
8
  # +method_display_name+ may be nil if name resolution gets
7
9
  # deferred until runtime (e.g. for a singleton method on an
@@ -15,8 +17,9 @@ module AppMap
15
17
  # with the method we're hooking.
16
18
  TIME_NOW = Time.method(:now)
17
19
  private_constant :TIME_NOW
18
-
19
- def initialize(hook_class, hook_method)
20
+
21
+ def initialize(hook_package, hook_class, hook_method)
22
+ @hook_package = hook_package
20
23
  @hook_class = hook_class
21
24
  @hook_method = hook_method
22
25
 
@@ -30,7 +33,7 @@ module AppMap
30
33
  msg = if method_display_name
31
34
  "#{method_display_name}"
32
35
  else
33
- "#{hook_method.name} (class resolution deferrred)"
36
+ "#{hook_method.name} (class resolution deferred)"
34
37
  end
35
38
  warn "AppMap: Hooking " + msg
36
39
  end
@@ -41,34 +44,38 @@ module AppMap
41
44
  after_hook = self.method(:after_hook)
42
45
  with_disabled_hook = self.method(:with_disabled_hook)
43
46
 
44
- hook_class.define_method hook_method.name do |*args, &block|
45
- instance_method = hook_method.bind(self).to_proc
47
+ hook_method_def = nil
48
+ hook_class.instance_eval do
49
+ hook_method_def = Proc.new do |*args, &block|
50
+ instance_method = hook_method.bind(self).to_proc
46
51
 
47
- # We may not have gotten the class for the method during
48
- # initialization (e.g. for a singleton method on an embedded
49
- # struct), so make sure we have it now.
50
- defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
52
+ # We may not have gotten the class for the method during
53
+ # initialization (e.g. for a singleton method on an embedded
54
+ # struct), so make sure we have it now.
55
+ defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
51
56
 
52
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
53
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
54
- return instance_method.call(*args, &block) unless enabled
57
+ hook_disabled = Thread.current[HOOK_DISABLE_KEY]
58
+ enabled = true if !hook_disabled && AppMap.tracing.enabled?
59
+ return instance_method.call(*args, &block) unless enabled
55
60
 
56
- call_event, start_time = with_disabled_hook.() do
57
- before_hook.(self, defined_class, args)
58
- end
59
- return_value = nil
60
- exception = nil
61
- begin
62
- return_value = instance_method.(*args, &block)
63
- rescue
64
- exception = $ERROR_INFO
65
- raise
66
- ensure
67
- with_disabled_hook.() do
68
- after_hook.(call_event, start_time, return_value, exception)
61
+ call_event, start_time = with_disabled_hook.() do
62
+ before_hook.(self, defined_class, args)
63
+ end
64
+ return_value = nil
65
+ exception = nil
66
+ begin
67
+ return_value = instance_method.(*args, &block)
68
+ rescue
69
+ exception = $ERROR_INFO
70
+ raise
71
+ ensure
72
+ with_disabled_hook.() do
73
+ after_hook.(self, call_event, start_time, return_value, exception)
74
+ end
69
75
  end
70
76
  end
71
77
  end
78
+ hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
72
79
  end
73
80
 
74
81
  protected
@@ -76,11 +83,11 @@ module AppMap
76
83
  def before_hook(receiver, defined_class, args)
77
84
  require 'appmap/event'
78
85
  call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
79
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
86
+ AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
80
87
  [ call_event, TIME_NOW.call ]
81
88
  end
82
89
 
83
- def after_hook(call_event, start_time, return_value, exception)
90
+ def after_hook(receiver, call_event, start_time, return_value, exception)
84
91
  require 'appmap/event'
85
92
  elapsed = TIME_NOW.call - start_time
86
93
  return_event = \
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+ require 'appmap/hook'
5
+
6
+ module AppMap
7
+ module Rails
8
+ module RequestHandler
9
+ class HTTPServerRequest < AppMap::Event::MethodEvent
10
+ attr_accessor :request_method, :path_info, :params
11
+
12
+ def initialize(request)
13
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
+
15
+ @request_method = request.request_method
16
+ @path_info = request.path_info.split('?')[0]
17
+ @params = ActionDispatch::Http::ParameterFilter.new(::Rails.application.config.filter_parameters).filter(request.params)
18
+ end
19
+
20
+ def to_h
21
+ super.tap do |h|
22
+ h[:http_server_request] = {
23
+ request_method: request_method,
24
+ path_info: path_info
25
+ }
26
+
27
+ h[:message] = params.keys.map do |key|
28
+ val = params[key]
29
+ {
30
+ name: key,
31
+ class: val.class.name,
32
+ value: self.class.display_string(val),
33
+ object_id: val.__id__
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
41
+ attr_accessor :status, :mime_type
42
+
43
+ def initialize(response, parent_id, elapsed)
44
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
45
+
46
+ self.status = response.status
47
+ self.mime_type = response.headers['Content-Type']
48
+ self.parent_id = parent_id
49
+ self.elapsed = elapsed
50
+ end
51
+
52
+ def to_h
53
+ super.tap do |h|
54
+ h[:http_server_response] = {
55
+ status: status,
56
+ mime_type: mime_type
57
+ }.compact
58
+ end
59
+ end
60
+ end
61
+
62
+ class HookMethod < AppMap::Hook::Method
63
+ def initialize
64
+ # ActionController::Instrumentation has issued start_processing.action_controller and
65
+ # process_action.action_controller since Rails 3. Therefore it's a stable place to hook
66
+ # the request. Rails controller notifications can't be used directly because they don't
67
+ # provide response headers, and we want the Content-Type.
68
+ super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
69
+ end
70
+
71
+ protected
72
+
73
+ def before_hook(receiver, defined_class, _) # args
74
+ call_event = HTTPServerRequest.new(receiver.request)
75
+ # http_server_request events are i/o and do not require a package name.
76
+ AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
77
+ [ call_event, TIME_NOW.call ]
78
+ end
79
+
80
+ def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
81
+ elapsed = TIME_NOW.call - start_time
82
+ return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
83
+ AppMap.tracing.record_event return_event
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -72,21 +72,24 @@ module AppMap
72
72
  end
73
73
 
74
74
  class ActiveRecordExaminer
75
+ @@db_version_warning_issued = {}
76
+
77
+ def issue_warning
78
+ db_type = database_type
79
+ return if @@db_version_warning_issued[db_type]
80
+ warn("AppMap: Unable to determine database version for #{db_type.inspect}")
81
+ @@db_version_warning_issued[db_type] = true
82
+ end
83
+
75
84
  def server_version
76
- case database_type
77
- when :postgres
78
- ActiveRecord::Base.connection.postgresql_version
79
- when :sqlite
80
- ActiveRecord::Base.connection.database_version.to_s
81
- else
82
- warn "Unable to determine database version for #{database_type.inspect}"
83
- end
85
+ ActiveRecord::Base.connection.try(:database_version) || issue_warning
84
86
  end
85
87
 
86
88
  def database_type
87
- return :postgres if ActiveRecord::Base.connection.respond_to?(:postgresql_version)
89
+ type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
90
+ type = :postgres if type == :postgresql
88
91
 
89
- ActiveRecord::Base.connection.adapter_name.downcase.to_sym
92
+ type
90
93
  end
91
94
 
92
95
  def execute_query(sql)
@@ -13,13 +13,11 @@ module AppMap
13
13
  # AppMap events.
14
14
  initializer 'appmap.subscribe', after: 'appmap.init' do |_| # params: app
15
15
  require 'appmap/rails/sql_handler'
16
- require 'appmap/rails/action_handler'
16
+ require 'appmap/rails/request_handler'
17
17
  ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
18
18
  ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
19
- ActiveSupport::Notifications.subscribe \
20
- 'start_processing.action_controller', AppMap::Rails::ActionHandler::HTTPServerRequest.new
21
- ActiveSupport::Notifications.subscribe \
22
- 'process_action.action_controller', AppMap::Rails::ActionHandler::HTTPServerResponse.new
19
+
20
+ AppMap::Rails::RequestHandler::HookMethod.new.activate
23
21
  end
24
22
 
25
23
  # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
@@ -124,8 +124,18 @@ module AppMap
124
124
  def initialize(example)
125
125
  super
126
126
 
127
+ webdriver_port = lambda do
128
+ return unless defined?(page) && page&.driver
129
+
130
+ # This is the ugliest thing ever but I don't want to lose it.
131
+ # All the WebDriver calls are getting app-mapped and it's really unclear
132
+ # what they are.
133
+ page.driver.options[:http_client].instance_variable_get('@server_url').port
134
+ end
135
+
127
136
  warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
128
137
  @trace = AppMap.tracing.trace
138
+ @webdriver_port = webdriver_port.()
129
139
  end
130
140
 
131
141
  def finish
@@ -3,9 +3,10 @@
3
3
  module AppMap
4
4
  module Trace
5
5
  class ScopedMethod < SimpleDelegator
6
- attr_reader :defined_class, :static
7
-
8
- def initialize(defined_class, method, static)
6
+ attr_reader :package, :defined_class, :static
7
+
8
+ def initialize(package, defined_class, method, static)
9
+ @package = package
9
10
  @defined_class = defined_class
10
11
  @static = static
11
12
  super(method)
@@ -32,9 +33,9 @@ module AppMap
32
33
  @tracing.any?(&:enabled?)
33
34
  end
34
35
 
35
- def record_event(event, defined_class: nil, method: nil)
36
+ def record_event(event, package: nil, defined_class: nil, method: nil)
36
37
  @tracing.each do |tracer|
37
- tracer.record_event(event, defined_class: defined_class, method: method)
38
+ tracer.record_event(event, package: package, defined_class: defined_class, method: method)
38
39
  end
39
40
  end
40
41
 
@@ -71,11 +72,12 @@ module AppMap
71
72
  # Record a program execution event.
72
73
  #
73
74
  # The event should be one of the MethodEvent subclasses.
74
- def record_event(event, defined_class: nil, method: nil)
75
+ def record_event(event, package: nil, defined_class: nil, method: nil)
75
76
  return unless @enabled
76
77
 
77
78
  @events << event
78
- @methods << Trace::ScopedMethod.new(defined_class, method, event.static) if (defined_class && method && event.event == :call)
79
+ @methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
80
+ if package && defined_class && method && (event.event == :call)
79
81
  end
80
82
 
81
83
  # Gets a unique list of the methods that were invoked by the program.
@@ -36,6 +36,23 @@ module AppMap
36
36
  [ fname, extension ].join
37
37
  end
38
38
 
39
+ # sanitize_paths removes ephemeral values from objects with
40
+ # embedded paths (e.g. an event or a classmap), making events
41
+ # easier to compare across runs.
42
+ def sanitize_paths(h)
43
+ require 'hashie'
44
+ h.extend(Hashie::Extensions::DeepLocate)
45
+ keys = %i(path location)
46
+ h.deep_locate ->(k,v,o) {
47
+ next unless keys.include?(k)
48
+
49
+ fix = ->(v) {v.gsub(%r{#{Gem.dir}/gems/.*(?=lib)}, '')}
50
+ keys.each {|k| o[k] = fix.(o[k]) if o[k] }
51
+ }
52
+
53
+ h
54
+ end
55
+
39
56
  # sanitize_event removes ephemeral values from an event, making
40
57
  # events easier to compare across runs.
41
58
  def sanitize_event(event, &block)
@@ -49,7 +66,7 @@ module AppMap
49
66
 
50
67
  case event[:event]
51
68
  when :call
52
- event[:path] = event[:path].gsub(Gem.dir + '/', '')
69
+ sanitize_paths(event)
53
70
  end
54
71
 
55
72
  event
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.34.2'
6
+ VERSION = '0.35.2'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -39,7 +39,7 @@ describe 'AbstractControllerBase' do
39
39
  expect(appmap).to include(<<-SERVER_REQUEST.strip)
40
40
  http_server_request:
41
41
  request_method: POST
42
- path_info: "/api/users?login=alice&password=foobar
42
+ path_info: "/api/users"
43
43
  SERVER_REQUEST
44
44
  end
45
45
  it 'Properly captures method parameters in the appmap' do
@@ -45,6 +45,12 @@ describe 'AbstractControllerBase' do
45
45
  request_method: POST
46
46
  path_info: "/api/users"
47
47
  SERVER_REQUEST
48
+
49
+ expect(appmap).to include(<<-SERVER_RESPONSE.strip)
50
+ http_server_response:
51
+ status: 201
52
+ mime_type: application/json; charset=utf-8
53
+ SERVER_RESPONSE
48
54
  end
49
55
 
50
56
  it 'properly captures method parameters in the appmap' do
@@ -9,3 +9,47 @@ class ExceptionMethod
9
9
  raise 'Exception occurred in raise_exception'
10
10
  end
11
11
  end
12
+
13
+ # subclass from BasicObject so we don't get #to_s. Requires some
14
+ # hackery to implement the other methods normally provided by Object.
15
+ class NoToSMethod < BasicObject
16
+ def is_a?(*args)
17
+ return false
18
+ end
19
+
20
+ def class
21
+ return ::Class
22
+ end
23
+
24
+ def respond_to?(*args)
25
+ return false
26
+ end
27
+
28
+ def inspect
29
+ "NoToSMethod"
30
+ end
31
+
32
+ def say_hello
33
+ "hello"
34
+ end
35
+ end
36
+
37
+ class InspectRaises < NoToSMethod
38
+ def inspect
39
+ ::Kernel.raise "#to_s missing, #inspect raises"
40
+ end
41
+
42
+ def say_hello
43
+ "hello"
44
+ end
45
+ end
46
+
47
+ class ToSRaises
48
+ def to_s
49
+ raise "#to_s raises"
50
+ end
51
+
52
+ def say_hello
53
+ "hello"
54
+ end
55
+ end
@@ -22,7 +22,7 @@ describe 'AppMap class Hooking', docker: false do
22
22
  while tracer.event?
23
23
  events << tracer.next_event.to_h
24
24
  end
25
- end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
25
+ end.map(&AppMap::Util.method(:sanitize_event))
26
26
  end
27
27
 
28
28
  def invoke_test_file(file, setup: nil, &block)
@@ -50,7 +50,7 @@ describe 'AppMap class Hooking', docker: false do
50
50
  def test_hook_behavior(file, events_yaml, setup: nil, &block)
51
51
  config, tracer = invoke_test_file(file, setup: setup, &block)
52
52
 
53
- events = collect_events(tracer)
53
+ events = collect_events(tracer).to_yaml
54
54
 
55
55
  expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('')
56
56
 
@@ -465,6 +465,132 @@ describe 'AppMap class Hooking', docker: false do
465
465
  end
466
466
  end
467
467
 
468
+ context 'string conversions works for the receiver when' do
469
+
470
+ it 'is missing #to_s' do
471
+ events_yaml = <<~YAML
472
+ ---
473
+ - :id: 1
474
+ :event: :call
475
+ :defined_class: NoToSMethod
476
+ :method_id: respond_to?
477
+ :path: spec/fixtures/hook/exception_method.rb
478
+ :lineno: 24
479
+ :static: false
480
+ :parameters:
481
+ - :name: :args
482
+ :class: Symbol
483
+ :value: to_s
484
+ :kind: :rest
485
+ :receiver:
486
+ :class: Class
487
+ :value: NoToSMethod
488
+ - :id: 2
489
+ :event: :return
490
+ :parent_id: 1
491
+ - :id: 3
492
+ :event: :call
493
+ :defined_class: NoToSMethod
494
+ :method_id: say_hello
495
+ :path: spec/fixtures/hook/exception_method.rb
496
+ :lineno: 32
497
+ :static: false
498
+ :parameters: []
499
+ :receiver:
500
+ :class: Class
501
+ :value: NoToSMethod
502
+ - :id: 4
503
+ :event: :return
504
+ :parent_id: 3
505
+ :return_value:
506
+ :class: String
507
+ :value: hello
508
+ YAML
509
+
510
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
511
+ inst = NoToSMethod.new
512
+ # sanity check
513
+ expect(inst).not_to respond_to(:to_s)
514
+ inst.say_hello
515
+ end
516
+ end
517
+
518
+ it 'it is missing #to_s and it raises an exception in #inspect' do
519
+ events_yaml = <<~YAML
520
+ ---
521
+ - :id: 1
522
+ :event: :call
523
+ :defined_class: NoToSMethod
524
+ :method_id: respond_to?
525
+ :path: spec/fixtures/hook/exception_method.rb
526
+ :lineno: 24
527
+ :static: false
528
+ :parameters:
529
+ - :name: :args
530
+ :class: Symbol
531
+ :value: to_s
532
+ :kind: :rest
533
+ :receiver:
534
+ :class: Class
535
+ :value: "*Error inspecting variable*"
536
+ - :id: 2
537
+ :event: :return
538
+ :parent_id: 1
539
+ - :id: 3
540
+ :event: :call
541
+ :defined_class: InspectRaises
542
+ :method_id: say_hello
543
+ :path: spec/fixtures/hook/exception_method.rb
544
+ :lineno: 42
545
+ :static: false
546
+ :parameters: []
547
+ :receiver:
548
+ :class: Class
549
+ :value: "*Error inspecting variable*"
550
+ - :id: 4
551
+ :event: :return
552
+ :parent_id: 3
553
+ :return_value:
554
+ :class: String
555
+ :value: hello
556
+ YAML
557
+
558
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
559
+ inst = InspectRaises.new
560
+ # sanity check
561
+ expect(inst).not_to respond_to(:to_s)
562
+ inst.say_hello
563
+ end
564
+ end
565
+
566
+ it 'it raises an exception in #to_s' do
567
+ events_yaml = <<~YAML
568
+ ---
569
+ - :id: 1
570
+ :event: :call
571
+ :defined_class: ToSRaises
572
+ :method_id: say_hello
573
+ :path: spec/fixtures/hook/exception_method.rb
574
+ :lineno: 52
575
+ :static: false
576
+ :parameters: []
577
+ :receiver:
578
+ :class: ToSRaises
579
+ :value: "*Error inspecting variable*"
580
+ - :id: 2
581
+ :event: :return
582
+ :parent_id: 1
583
+ :return_value:
584
+ :class: String
585
+ :value: hello
586
+ YAML
587
+
588
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
589
+ ToSRaises.new.say_hello
590
+ end
591
+ end
592
+ end
593
+
468
594
  it 're-raises exceptions' do
469
595
  RSpec::Expectations.configuration.on_potential_false_positives = :nothing
470
596
 
@@ -500,7 +626,7 @@ describe 'AppMap class Hooking', docker: false do
500
626
  :event: :call
501
627
  :defined_class: ActiveSupport::SecurityUtils
502
628
  :method_id: secure_compare
503
- :path: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb
629
+ :path: lib/active_support/security_utils.rb
504
630
  :lineno: 26
505
631
  :static: true
506
632
  :parameters:
@@ -598,7 +724,7 @@ describe 'AppMap class Hooking', docker: false do
598
724
  :children:
599
725
  - :name: secure_compare
600
726
  :type: function
601
- :location: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb:26
727
+ :location: lib/active_support/security_utils.rb:26
602
728
  :static: true
603
729
  :labels:
604
730
  - security
@@ -624,7 +750,7 @@ describe 'AppMap class Hooking', docker: false do
624
750
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
625
751
  expect(Compare.compare('string', 'string')).to be_truthy
626
752
  end
627
- cm = AppMap::ClassMap.build_from_methods(config, tracer.event_methods)
753
+ cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(tracer.event_methods))
628
754
  entry = cm[1][:children][0][:children][0][:children][0]
629
755
  # Sanity check, make sure we got the right one
630
756
  expect(entry[:name]).to eq('secure_compare')
@@ -669,4 +795,11 @@ describe 'AppMap class Hooking', docker: false do
669
795
  end
670
796
  end
671
797
  end
798
+
799
+ it "preserves the arity of hooked methods" do
800
+ invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
801
+ expect(InstanceMethod.instance_method(:say_echo).arity).to be(1)
802
+ expect(InstanceMethod.new.method(:say_echo).arity).to be(1)
803
+ end
804
+ end
672
805
  end
@@ -56,12 +56,10 @@ class CLITest < Minitest::Test
56
56
  Class frequency:
57
57
  ----------------
58
58
  1 Main
59
- 1 IO
60
59
 
61
60
  Method frequency:
62
61
  ----------------
63
62
  1 Main.say_hello
64
- 1 IO#write
65
63
  OUTPUT
66
64
  end
67
65
 
@@ -82,20 +80,12 @@ class CLITest < Minitest::Test
82
80
  {
83
81
  "name": "Main",
84
82
  "count": 1
85
- },
86
- {
87
- "name": "IO",
88
- "count": 1
89
83
  }
90
84
  ],
91
85
  "method_frequency": [
92
86
  {
93
87
  "name": "Main.say_hello",
94
88
  "count": 1
95
- },
96
- {
97
- "name": "IO#write",
98
- "count": 1
99
89
  }
100
90
  ]
101
91
  }
@@ -79,27 +79,6 @@ class OpenSSLTest < Minitest::Test
79
79
  ]
80
80
  }
81
81
  ]
82
- },
83
- {
84
- "name": "io",
85
- "type": "package",
86
- "children": [
87
- {
88
- "name": "IO",
89
- "type": "class",
90
- "children": [
91
- {
92
- "name": "write",
93
- "type": "function",
94
- "location": "IO#write",
95
- "static": false,
96
- "labels": [
97
- "io"
98
- ]
99
- }
100
- ]
101
- }
102
- ]
103
82
  }
104
83
  ]
105
84
  JSON
@@ -167,33 +146,6 @@ class OpenSSLTest < Minitest::Test
167
146
  "return_value": {
168
147
  "class": "String"
169
148
  }
170
- },
171
- {
172
- "id": 5,
173
- "event": "call",
174
- "defined_class": "IO",
175
- "method_id": "write",
176
- "path": "IO#write",
177
- "static": false,
178
- "parameters": [
179
- {
180
- "name": "arg",
181
- "class": "String",
182
- "value": "Computed signature",
183
- "kind": "rest"
184
- }
185
- ],
186
- "receiver": {
187
- "class": "IO"
188
- }
189
- },
190
- {
191
- "id": 6,
192
- "event": "return",
193
- "parent_id": 5,
194
- "return_value": {
195
- "class": "Integer"
196
- }
197
149
  }
198
150
  ]
199
151
  JSON
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.34.2
4
+ version: 0.35.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-04 00:00:00.000000000 Z
11
+ date: 2020-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -276,6 +276,20 @@ dependencies:
276
276
  - - ">="
277
277
  - !ruby/object:Gem::Version
278
278
  version: '0'
279
+ - !ruby/object:Gem::Dependency
280
+ name: hashie
281
+ requirement: !ruby/object:Gem::Requirement
282
+ requirements:
283
+ - - ">="
284
+ - !ruby/object:Gem::Version
285
+ version: '0'
286
+ type: :development
287
+ prerelease: false
288
+ version_requirements: !ruby/object:Gem::Requirement
289
+ requirements:
290
+ - - ">="
291
+ - !ruby/object:Gem::Version
292
+ version: '0'
279
293
  description:
280
294
  email:
281
295
  - kgilpin@gmail.com
@@ -324,7 +338,7 @@ files:
324
338
  - lib/appmap/middleware/remote_recording.rb
325
339
  - lib/appmap/minitest.rb
326
340
  - lib/appmap/open.rb
327
- - lib/appmap/rails/action_handler.rb
341
+ - lib/appmap/rails/request_handler.rb
328
342
  - lib/appmap/rails/sql_handler.rb
329
343
  - lib/appmap/railtie.rb
330
344
  - lib/appmap/record.rb
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'appmap/event'
4
-
5
- module AppMap
6
- module Rails
7
- module ActionHandler
8
- Context = Struct.new(:id, :start_time)
9
-
10
- module ContextKey
11
- def context_key
12
- "#{HTTPServerRequest.name}#call"
13
- end
14
- end
15
-
16
- class HTTPServerRequest
17
- include ContextKey
18
-
19
- class Call < AppMap::Event::MethodCall
20
- attr_accessor :payload
21
-
22
- def initialize(payload)
23
- super AppMap::Event.next_id_counter, :call, Thread.current.object_id
24
-
25
- self.payload = payload
26
- end
27
-
28
- def to_h
29
- super.tap do |h|
30
- h[:http_server_request] = {
31
- request_method: payload[:method],
32
- path_info: payload[:path]
33
- }
34
-
35
- params = payload[:params]
36
- h[:message] = params.keys.map do |key|
37
- val = params[key]
38
- {
39
- name: key,
40
- class: val.class.name,
41
- value: self.class.display_string(val),
42
- object_id: val.__id__
43
- }
44
- end
45
- end
46
- end
47
- end
48
-
49
- def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
50
- event = Call.new(payload)
51
- Thread.current[context_key] = Context.new(event.id, Time.now)
52
- AppMap.tracing.record_event(event)
53
- end
54
- end
55
-
56
- class HTTPServerResponse
57
- include ContextKey
58
-
59
- class Call < AppMap::Event::MethodReturnIgnoreValue
60
- attr_accessor :payload
61
-
62
- def initialize(payload, parent_id, elapsed)
63
- super AppMap::Event.next_id_counter, :return, Thread.current.object_id
64
-
65
- self.payload = payload
66
- self.parent_id = parent_id
67
- self.elapsed = elapsed
68
- end
69
-
70
- def to_h
71
- super.tap do |h|
72
- h[:http_server_response] = {
73
- status: payload[:status]
74
- }
75
- end
76
- end
77
- end
78
-
79
- def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
80
- return unless Thread.current[context_key]
81
-
82
- context = Thread.current[context_key]
83
- Thread.current[context_key] = nil
84
-
85
- event = Call.new(payload, context.id, Time.now - context.start_time)
86
- AppMap.tracing.record_event(event)
87
- end
88
- end
89
- end
90
- end
91
- end