appmap 0.34.5 → 0.37.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.
@@ -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
 
@@ -67,23 +70,24 @@ module AppMap
67
70
  raise
68
71
  ensure
69
72
  with_disabled_hook.() do
70
- after_hook.(call_event, start_time, return_value, exception)
73
+ after_hook.(self, call_event, start_time, return_value, exception)
71
74
  end
72
75
  end
73
76
  end
74
77
  end
75
78
  hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
76
79
  end
80
+
77
81
  protected
78
82
 
79
83
  def before_hook(receiver, defined_class, args)
80
84
  require 'appmap/event'
81
85
  call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
82
- 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
83
87
  [ call_event, TIME_NOW.call ]
84
88
  end
85
89
 
86
- def after_hook(call_event, start_time, return_value, exception)
90
+ def after_hook(receiver, call_event, start_time, return_value, exception)
87
91
  require 'appmap/event'
88
92
  elapsed = TIME_NOW.call - start_time
89
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,9 +72,17 @@ 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
- ActiveRecord::Base.connection.try(:database_version) ||\
77
- warn("Unable to determine database version for #{database_type.inspect}")
85
+ ActiveRecord::Base.connection.try(:database_version) || issue_warning
78
86
  end
79
87
 
80
88
  def database_type
@@ -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.
@@ -42,4 +40,4 @@ module AppMap
42
40
  end.call
43
41
  end
44
42
  end
45
- end
43
+ end unless ENV['APPMAP_INITIALIZE'] == 'false'
@@ -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
@@ -138,7 +148,7 @@ module AppMap
138
148
 
139
149
  AppMap::RSpec.add_event_methods @trace.event_methods
140
150
 
141
- class_map = AppMap.class_map(@trace.event_methods)
151
+ class_map = AppMap.class_map(@trace.event_methods, include_source: false)
142
152
 
143
153
  description = []
144
154
  scope = ScopeExample.new(example)
@@ -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.
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.34.5'
6
+ VERSION = '0.37.0'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.2'
8
+ APPMAP_FORMAT_VERSION = '1.3'
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
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'AppMap::ClassMap' do
6
+ describe '.build_from_methods' do
7
+ it 'includes source code if available' do
8
+ map = AppMap.class_map([scoped_method(method(:test_method))])
9
+ function = dig_map(map, 5)[0]
10
+ expect(function[:source]).to include 'test method body'
11
+ expect(function[:comment]).to include 'test method comment'
12
+ end
13
+
14
+ it 'can omit source code even if available' do
15
+ map = AppMap.class_map([scoped_method((method :test_method))], include_source: false)
16
+ function = dig_map(map, 5)[0]
17
+ expect(function).to_not include(:source)
18
+ expect(function).to_not include(:comment)
19
+ end
20
+
21
+ # test method comment
22
+ def test_method
23
+ 'test method body'
24
+ end
25
+
26
+ def scoped_method(method)
27
+ AppMap::Trace::ScopedMethod.new AppMap::Config::Package.new, method.receiver.class.name, method, false
28
+ end
29
+
30
+ def dig_map(map, depth)
31
+ return map if depth == 0
32
+
33
+ dig_map map[0][:children], depth - 1
34
+ end
35
+ end
36
+ end
@@ -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
@@ -27,7 +27,7 @@ describe 'AppMap class Hooking', docker: false do
27
27
 
28
28
  def invoke_test_file(file, setup: nil, &block)
29
29
  AppMap.configuration = nil
30
- package = AppMap::Config::Package.new(file)
30
+ package = AppMap::Config::Package.build_from_path(file)
31
31
  config = AppMap::Config.new('hook_spec', [ package ])
32
32
  AppMap.configuration = config
33
33
  tracer = nil
@@ -112,6 +112,10 @@ describe 'AppMap class Hooking', docker: false do
112
112
  :type: function
113
113
  :location: spec/fixtures/hook/instance_method.rb:8
114
114
  :static: false
115
+ :source: |2
116
+ def say_default
117
+ 'default'
118
+ end
115
119
  YAML
116
120
  end
117
121
 
@@ -465,6 +469,132 @@ describe 'AppMap class Hooking', docker: false do
465
469
  end
466
470
  end
467
471
 
472
+ context 'string conversions works for the receiver when' do
473
+
474
+ it 'is missing #to_s' do
475
+ events_yaml = <<~YAML
476
+ ---
477
+ - :id: 1
478
+ :event: :call
479
+ :defined_class: NoToSMethod
480
+ :method_id: respond_to?
481
+ :path: spec/fixtures/hook/exception_method.rb
482
+ :lineno: 24
483
+ :static: false
484
+ :parameters:
485
+ - :name: :args
486
+ :class: Symbol
487
+ :value: to_s
488
+ :kind: :rest
489
+ :receiver:
490
+ :class: Class
491
+ :value: NoToSMethod
492
+ - :id: 2
493
+ :event: :return
494
+ :parent_id: 1
495
+ - :id: 3
496
+ :event: :call
497
+ :defined_class: NoToSMethod
498
+ :method_id: say_hello
499
+ :path: spec/fixtures/hook/exception_method.rb
500
+ :lineno: 32
501
+ :static: false
502
+ :parameters: []
503
+ :receiver:
504
+ :class: Class
505
+ :value: NoToSMethod
506
+ - :id: 4
507
+ :event: :return
508
+ :parent_id: 3
509
+ :return_value:
510
+ :class: String
511
+ :value: hello
512
+ YAML
513
+
514
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
515
+ inst = NoToSMethod.new
516
+ # sanity check
517
+ expect(inst).not_to respond_to(:to_s)
518
+ inst.say_hello
519
+ end
520
+ end
521
+
522
+ it 'it is missing #to_s and it raises an exception in #inspect' do
523
+ events_yaml = <<~YAML
524
+ ---
525
+ - :id: 1
526
+ :event: :call
527
+ :defined_class: NoToSMethod
528
+ :method_id: respond_to?
529
+ :path: spec/fixtures/hook/exception_method.rb
530
+ :lineno: 24
531
+ :static: false
532
+ :parameters:
533
+ - :name: :args
534
+ :class: Symbol
535
+ :value: to_s
536
+ :kind: :rest
537
+ :receiver:
538
+ :class: Class
539
+ :value: "*Error inspecting variable*"
540
+ - :id: 2
541
+ :event: :return
542
+ :parent_id: 1
543
+ - :id: 3
544
+ :event: :call
545
+ :defined_class: InspectRaises
546
+ :method_id: say_hello
547
+ :path: spec/fixtures/hook/exception_method.rb
548
+ :lineno: 42
549
+ :static: false
550
+ :parameters: []
551
+ :receiver:
552
+ :class: Class
553
+ :value: "*Error inspecting variable*"
554
+ - :id: 4
555
+ :event: :return
556
+ :parent_id: 3
557
+ :return_value:
558
+ :class: String
559
+ :value: hello
560
+ YAML
561
+
562
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
563
+ inst = InspectRaises.new
564
+ # sanity check
565
+ expect(inst).not_to respond_to(:to_s)
566
+ inst.say_hello
567
+ end
568
+ end
569
+
570
+ it 'it raises an exception in #to_s' do
571
+ events_yaml = <<~YAML
572
+ ---
573
+ - :id: 1
574
+ :event: :call
575
+ :defined_class: ToSRaises
576
+ :method_id: say_hello
577
+ :path: spec/fixtures/hook/exception_method.rb
578
+ :lineno: 52
579
+ :static: false
580
+ :parameters: []
581
+ :receiver:
582
+ :class: ToSRaises
583
+ :value: "*Error inspecting variable*"
584
+ - :id: 2
585
+ :event: :return
586
+ :parent_id: 1
587
+ :return_value:
588
+ :class: String
589
+ :value: hello
590
+ YAML
591
+
592
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
593
+ ToSRaises.new.say_hello
594
+ end
595
+ end
596
+ end
597
+
468
598
  it 're-raises exceptions' do
469
599
  RSpec::Expectations.configuration.on_potential_false_positives = :nothing
470
600
 
@@ -587,6 +717,10 @@ describe 'AppMap class Hooking', docker: false do
587
717
  :type: function
588
718
  :location: spec/fixtures/hook/compare.rb:4
589
719
  :static: true
720
+ :source: |2
721
+ def self.compare(s1, s2)
722
+ ActiveSupport::SecurityUtils.secure_compare(s1, s2)
723
+ end
590
724
  - :name: active_support
591
725
  :type: package
592
726
  :children:
@@ -603,6 +737,15 @@ describe 'AppMap class Hooking', docker: false do
603
737
  :labels:
604
738
  - security
605
739
  - crypto
740
+ :comment: |
741
+ # Constant time string comparison, for variable length strings.
742
+ #
743
+ # The values are first processed by SHA256, so that we don't leak length info
744
+ # via timing attacks.
745
+ :source: |2
746
+ def secure_compare(a, b)
747
+ fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b
748
+ end
606
749
  - :name: openssl
607
750
  :type: package
608
751
  :children:
@@ -624,7 +767,7 @@ describe 'AppMap class Hooking', docker: false do
624
767
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
625
768
  expect(Compare.compare('string', 'string')).to be_truthy
626
769
  end
627
- cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(config, tracer.event_methods))
770
+ cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(tracer.event_methods))
628
771
  entry = cm[1][:children][0][:children][0][:children][0]
629
772
  # Sanity check, make sure we got the right one
630
773
  expect(entry[:name]).to eq('secure_compare')