appmap 0.34.0 → 0.35.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,63 +1,95 @@
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, :defined_class, :method_display_name
6
+ attr_reader :hook_package, :hook_class, :hook_method
7
+
8
+ # +method_display_name+ may be nil if name resolution gets
9
+ # deferred until runtime (e.g. for a singleton method on an
10
+ # embedded Struct).
11
+ attr_reader :method_display_name
5
12
 
6
13
  HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
7
14
  private_constant :HOOK_DISABLE_KEY
8
15
 
9
- def initialize(hook_class, hook_method)
16
+ # Grab the definition of Time.now here, to avoid interfering
17
+ # with the method we're hooking.
18
+ TIME_NOW = Time.method(:now)
19
+ private_constant :TIME_NOW
20
+
21
+ def initialize(hook_package, hook_class, hook_method)
22
+ @hook_package = hook_package
10
23
  @hook_class = hook_class
11
24
  @hook_method = hook_method
25
+
26
+ # Get the class for the method, if it's known.
12
27
  @defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
13
- @method_display_name = [@defined_class, method_symbol, @hook_method.name].join
28
+ @method_display_name = [@defined_class, method_symbol, @hook_method.name].join if @defined_class
14
29
  end
15
30
 
16
31
  def activate
17
- warn "AppMap: Hooking #{method_display_name}" if Hook::LOG
32
+ if Hook::LOG
33
+ msg = if method_display_name
34
+ "#{method_display_name}"
35
+ else
36
+ "#{hook_method.name} (class resolution deferred)"
37
+ end
38
+ warn "AppMap: Hooking " + msg
39
+ end
18
40
 
41
+ defined_class = @defined_class
19
42
  hook_method = self.hook_method
20
43
  before_hook = self.method(:before_hook)
21
44
  after_hook = self.method(:after_hook)
22
45
  with_disabled_hook = self.method(:with_disabled_hook)
23
46
 
24
- hook_class.define_method hook_method.name do |*args, &block|
25
- 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
26
51
 
27
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
28
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
29
- return instance_method.call(*args, &block) unless enabled
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
30
56
 
31
- call_event, start_time = with_disabled_hook.() do
32
- before_hook.(self, args)
33
- end
34
- return_value = nil
35
- exception = nil
36
- begin
37
- return_value = instance_method.(*args, &block)
38
- rescue
39
- exception = $ERROR_INFO
40
- raise
41
- ensure
42
- with_disabled_hook.() do
43
- after_hook.(call_event, start_time, return_value, exception)
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
60
+
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
44
75
  end
45
76
  end
46
77
  end
78
+ hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
47
79
  end
48
80
 
49
81
  protected
50
82
 
51
- def before_hook(receiver, args)
83
+ def before_hook(receiver, defined_class, args)
52
84
  require 'appmap/event'
53
85
  call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
54
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
55
- [ call_event, Time.now ]
86
+ AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
87
+ [ call_event, TIME_NOW.call ]
56
88
  end
57
89
 
58
- def after_hook(call_event, start_time, return_value, exception)
90
+ def after_hook(receiver, call_event, start_time, return_value, exception)
59
91
  require 'appmap/event'
60
- elapsed = Time.now - start_time
92
+ elapsed = TIME_NOW.call - start_time
61
93
  return_event = \
62
94
  AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
63
95
  AppMap.tracing.record_event 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.0'
6
+ VERSION = '0.35.0'
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
@@ -20,4 +20,8 @@ class InstanceMethod
20
20
  def say_block(&block)
21
21
  yield
22
22
  end
23
+
24
+ def say_the_time
25
+ Time.now.to_s
26
+ end
23
27
  end
@@ -15,6 +15,20 @@ class SingletonMethod
15
15
  'defined with self class scope'
16
16
  end
17
17
 
18
+ module AddMethod
19
+ def self.included(base)
20
+ base.module_eval do
21
+ define_method "added_method" do
22
+ _added_method
23
+ end
24
+ end
25
+ end
26
+
27
+ def _added_method
28
+ 'defined by including a module'
29
+ end
30
+ end
31
+
18
32
  # When called, do_include calls +include+ to bring in the module
19
33
  # AddMethod. AddMethod defines a new instance method, which gets
20
34
  # added to the singleton class of SingletonMethod.
@@ -32,23 +46,18 @@ class SingletonMethod
32
46
  end
33
47
  end
34
48
  end
35
-
36
- def to_s
37
- 'Singleton Method fixture'
38
- end
39
- end
40
49
 
41
- module AddMethod
42
- def self.included(base)
43
- base.module_eval do
44
- define_method "added_method" do
45
- _added_method
50
+ STRUCT_TEST = Struct.new(:attr) do
51
+ class << self
52
+ def say_struct_singleton
53
+ 'singleton for a struct'
46
54
  end
47
55
  end
48
56
  end
49
57
 
50
- def _added_method
51
- 'defined by including a module'
58
+ def to_s
59
+ 'Singleton Method fixture'
52
60
  end
53
61
  end
54
62
 
63
+