appmap 0.41.2 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +23 -2
  4. data/CHANGELOG.md +40 -0
  5. data/README.md +36 -6
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +4 -2
  10. data/lib/appmap/class_map.rb +7 -10
  11. data/lib/appmap/config.rb +98 -28
  12. data/lib/appmap/cucumber.rb +1 -1
  13. data/lib/appmap/event.rb +18 -0
  14. data/lib/appmap/handler/function.rb +19 -0
  15. data/lib/appmap/handler/net_http.rb +107 -0
  16. data/lib/appmap/hook.rb +42 -22
  17. data/lib/appmap/hook/method.rb +5 -7
  18. data/lib/appmap/minitest.rb +35 -30
  19. data/lib/appmap/rails/request_handler.rb +30 -17
  20. data/lib/appmap/record.rb +1 -1
  21. data/lib/appmap/rspec.rb +32 -96
  22. data/lib/appmap/trace.rb +2 -1
  23. data/lib/appmap/util.rb +39 -2
  24. data/lib/appmap/version.rb +2 -2
  25. data/release.sh +17 -0
  26. data/spec/abstract_controller_base_spec.rb +76 -29
  27. data/spec/class_map_spec.rb +3 -11
  28. data/spec/config_spec.rb +33 -1
  29. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  30. data/spec/fixtures/hook/method_named_call.rb +11 -0
  31. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  32. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  33. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  34. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  35. data/spec/fixtures/rails5_users_app/create_app +8 -2
  36. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  37. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  38. data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
  39. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  40. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  41. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  42. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  43. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  44. data/spec/fixtures/rails6_users_app/create_app +8 -2
  45. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  46. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  47. data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
  48. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  49. data/spec/hook_spec.rb +135 -18
  50. data/spec/record_net_http_spec.rb +160 -0
  51. data/spec/record_sql_rails_pg_spec.rb +1 -1
  52. data/spec/spec_helper.rb +16 -0
  53. data/test/expectations/openssl_test_key_sign1.json +2 -4
  54. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
  55. data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
  56. data/test/gem_test.rb +1 -1
  57. data/test/minitest_test.rb +1 -2
  58. data/test/rspec_test.rb +1 -20
  59. metadata +17 -13
  60. data/exe/appmap +0 -154
  61. data/spec/rspec_feature_metadata_spec.rb +0 -31
  62. data/test/cli_test.rb +0 -116
@@ -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
@@ -36,6 +36,18 @@ module AppMap
36
36
  (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
37
  end
38
38
 
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
50
+
39
51
  protected
40
52
 
41
53
  # Heuristic for dynamically defined class whose name can be nil
@@ -79,6 +91,12 @@ module AppMap
79
91
  end
80
92
  end
81
93
  end
94
+
95
+ protected
96
+
97
+ def object_properties(hash_like)
98
+ self.class.object_properties(hash_like)
99
+ end
82
100
  end
83
101
 
84
102
  class MethodCall < MethodEvent
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+
5
+ module AppMap
6
+ module Handler
7
+ module Function
8
+ class << self
9
+ def handle_call(defined_class, hook_method, receiver, args)
10
+ AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
11
+ end
12
+
13
+ def handle_return(call_event_id, elapsed, return_value, exception)
14
+ AppMap::Event::MethodReturn.build_from_invocation(call_event_id, elapsed, return_value, exception)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+
5
+ module AppMap
6
+ module Handler
7
+ class HTTPClientRequest < AppMap::Event::MethodEvent
8
+ attr_accessor :request_method, :url, :params, :headers
9
+
10
+ def initialize(http, request)
11
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
12
+
13
+ path, query = request.path.split('?')
14
+ query ||= ''
15
+
16
+ protocol = http.use_ssl? ? 'https' : 'http'
17
+ port = if http.use_ssl? && http.port == 443
18
+ nil
19
+ elsif !http.use_ssl? && http.port == 80
20
+ nil
21
+ else
22
+ ":#{http.port}"
23
+ end
24
+
25
+ url = [ protocol, '://', http.address, port, path ].compact.join
26
+
27
+ self.request_method = request.method
28
+ self.url = url
29
+ self.headers = AppMap::Util.select_headers(NetHTTP.request_headers(request))
30
+ self.params = Rack::Utils.parse_nested_query(query)
31
+ end
32
+
33
+ def to_h
34
+ super.tap do |h|
35
+ h[:http_client_request] = {
36
+ request_method: request_method,
37
+ url: url,
38
+ headers: headers
39
+ }.compact
40
+
41
+ unless params.blank?
42
+ h[:message] = params.keys.map do |key|
43
+ val = params[key]
44
+ {
45
+ name: key,
46
+ class: val.class.name,
47
+ value: self.class.display_string(val),
48
+ object_id: val.__id__,
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ class HTTPClientResponse < AppMap::Event::MethodReturnIgnoreValue
57
+ attr_accessor :status, :mime_type, :headers
58
+
59
+ def initialize(response, parent_id, elapsed)
60
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
61
+
62
+ self.status = response.code.to_i
63
+ self.parent_id = parent_id
64
+ self.elapsed = elapsed
65
+ self.headers = AppMap::Util.select_headers(NetHTTP.response_headers(response))
66
+ end
67
+
68
+ def to_h
69
+ super.tap do |h|
70
+ h[:http_client_response] = {
71
+ status_code: status,
72
+ mime_type: mime_type,
73
+ headers: headers
74
+ }.compact
75
+ end
76
+ end
77
+ end
78
+
79
+ class NetHTTP
80
+ class << self
81
+ def request_headers(request)
82
+ {}.tap do |headers|
83
+ request.each_header do |k,v|
84
+ key = [ 'HTTP', k.underscore.upcase ].join('_')
85
+ headers[key] = v
86
+ end
87
+ end
88
+ end
89
+
90
+ alias response_headers request_headers
91
+
92
+ def handle_call(defined_class, hook_method, receiver, args)
93
+ # request will call itself again in a start block if it's not already started.
94
+ return unless receiver.started?
95
+
96
+ http = receiver
97
+ request = args.first
98
+ HTTPClientRequest.new(http, request)
99
+ end
100
+
101
+ def handle_return(call_event_id, elapsed, return_value, exception)
102
+ HTTPClientResponse.new(return_value, call_event_id, elapsed)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/appmap/hook.rb CHANGED
@@ -32,6 +32,7 @@ module AppMap
32
32
  end
33
33
 
34
34
  attr_reader :config
35
+
35
36
  def initialize(config)
36
37
  @config = config
37
38
  end
@@ -46,10 +47,23 @@ module AppMap
46
47
  cls = trace_point.self
47
48
 
48
49
  instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
49
- class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
50
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
51
+ class_methods = begin
52
+ if cls.respond_to?(:singleton_class)
53
+ cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
54
+ else
55
+ []
56
+ end
57
+ rescue NameError
58
+ []
59
+ end
50
60
 
51
61
  hook = lambda do |hook_cls|
52
62
  lambda do |method_id|
63
+ # Don't try and trace the AppMap methods or there will be
64
+ # a stack overflow in the defined hook method.
65
+ return if (hook_cls&.name || '').split('::')[0] == AppMap.name
66
+
53
67
  method = begin
54
68
  hook_cls.public_instance_method(method_id)
55
69
  rescue NameError
@@ -69,18 +83,22 @@ module AppMap
69
83
  config.always_hook?(hook_cls, method.name) ||
70
84
  config.included_by_location?(method)
71
85
 
72
- hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
86
+ package = config.package_for_method(method)
73
87
 
74
- # Don't try and trace the AppMap methods or there will be
75
- # a stack overflow in the defined hook method.
76
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
88
+ hook_method = Hook::Method.new(package, hook_cls, method)
77
89
 
78
90
  hook_method.activate
79
91
  end
80
92
  end
81
93
 
82
94
  instance_methods.each(&hook.(cls))
83
- class_methods.each(&hook.(cls.singleton_class))
95
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
96
+ begin
97
+ class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
98
+ rescue NameError
99
+ # NameError:
100
+ # uninitialized constant Faraday::Connection
101
+ end
84
102
  end
85
103
 
86
104
  tp.enable(&block)
@@ -97,25 +115,27 @@ module AppMap
97
115
  end
98
116
  end
99
117
 
100
- Config::BUILTIN_METHODS.each do |class_name, hook|
101
- require hook.package.package_name if hook.package.package_name
102
- Array(hook.method_names).each do |method_name|
103
- method_name = method_name.to_sym
118
+ config.builtin_methods.each do |class_name, hooks|
119
+ Array(hooks).each do |hook|
120
+ require hook.package.package_name if hook.package.package_name
121
+ Array(hook.method_names).each do |method_name|
122
+ method_name = method_name.to_sym
104
123
 
105
- cls = class_from_string.(class_name)
106
- method = \
107
- begin
108
- cls.instance_method(method_name)
109
- rescue NameError
110
- cls.method(method_name) rescue nil
111
- end
124
+ cls = class_from_string.(class_name)
125
+ method = \
126
+ begin
127
+ cls.instance_method(method_name)
128
+ rescue NameError
129
+ cls.method(method_name) rescue nil
130
+ end
112
131
 
113
- next if config.never_hook?(method)
132
+ next if config.never_hook?(method)
114
133
 
115
- if method
116
- Hook::Method.new(hook.package, cls, method).activate
117
- else
118
- warn "Method #{method_name} not found on #{cls.name}"
134
+ if method
135
+ Hook::Method.new(hook.package, cls, method).activate
136
+ else
137
+ warn "Method #{method_name} not found on #{cls.name}"
138
+ end
119
139
  end
120
140
  end
121
141
  end
@@ -76,7 +76,7 @@ module AppMap
76
76
  raise
77
77
  ensure
78
78
  with_disabled_hook.call do
79
- after_hook.call(self, call_event, start_time, return_value, exception)
79
+ after_hook.call(self, call_event, start_time, return_value, exception) if call_event
80
80
  end
81
81
  end
82
82
  end
@@ -87,18 +87,16 @@ module AppMap
87
87
  protected
88
88
 
89
89
  def before_hook(receiver, defined_class, args)
90
- require 'appmap/event'
91
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
92
- AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
90
+ call_event = hook_package.handler_class.handle_call(defined_class, hook_method, receiver, args)
91
+ AppMap.tracing.record_event(call_event, package: hook_package, defined_class: defined_class, method: hook_method) if call_event
93
92
  [ call_event, TIME_NOW.call ]
94
93
  end
95
94
 
96
95
  def after_hook(_receiver, call_event, start_time, return_value, exception)
97
- require 'appmap/event'
98
96
  elapsed = TIME_NOW.call - start_time
99
- return_event = \
100
- AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
97
+ return_event = hook_package.handler_class.handle_return(call_event.id, elapsed, return_value, exception)
101
98
  AppMap.tracing.record_event return_event
99
+ nil
102
100
  end
103
101
 
104
102
  def with_disabled_hook(&function)
@@ -7,22 +7,28 @@ module AppMap
7
7
  # be activated around each test.
8
8
  module Minitest
9
9
  APPMAP_OUTPUT_DIR = 'tmp/appmap/minitest'
10
- LOG = false
10
+ LOG = ( ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true' )
11
11
 
12
12
  def self.metadata
13
13
  AppMap.detect_metadata
14
14
  end
15
15
 
16
- Recording = Struct.new(:test) do
17
- def initialize(test)
16
+ Recording = Struct.new(:test, :test_name) do
17
+ def initialize(test, test_name)
18
18
  super
19
19
 
20
- warn "Starting recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
20
+ warn "Starting recording of test #{test.class}.#{test.name}@#{source_location}" if AppMap::Minitest::LOG
21
21
  @trace = AppMap.tracing.trace
22
22
  end
23
23
 
24
- def finish
24
+ def source_location
25
+ test.method(test_name).source_location.join(':')
26
+ end
27
+
28
+
29
+ def finish(exception)
25
30
  warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
31
+ warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
26
32
 
27
33
  events = []
28
34
  AppMap.tracing.delete @trace
@@ -37,11 +43,12 @@ module AppMap
37
43
  feature_name = test.name.split('_')[1..-1].join(' ')
38
44
  scenario_name = [ feature_group, feature_name ].join(' ')
39
45
 
40
- AppMap::Minitest.save scenario_name,
41
- class_map,
42
- events: events,
43
- feature_name: feature_name,
44
- feature_group_name: feature_group
46
+ AppMap::Minitest.save name: scenario_name,
47
+ class_map: class_map,
48
+ source_location: source_location,
49
+ test_status: exception ? 'failed' : 'succeeded',
50
+ exception: exception,
51
+ events: events
45
52
  end
46
53
  end
47
54
 
@@ -55,15 +62,15 @@ module AppMap
55
62
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
56
63
  end
57
64
 
58
- def begin_test(test)
59
- @recordings_by_test[test.object_id] = Recording.new(test)
65
+ def begin_test(test, name)
66
+ @recordings_by_test[test.object_id] = Recording.new(test, name)
60
67
  end
61
68
 
62
- def end_test(test)
69
+ def end_test(test, exception:)
63
70
  recording = @recordings_by_test.delete(test.object_id)
64
71
  return warn "No recording found for #{test}" unless recording
65
72
 
66
- recording.finish
73
+ recording.finish exception
67
74
  end
68
75
 
69
76
  def config
@@ -74,12 +81,11 @@ module AppMap
74
81
  @event_methods += event_methods
75
82
  end
76
83
 
77
- def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
84
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
78
85
  metadata = AppMap::Minitest.metadata.tap do |m|
79
- m[:name] = example_name
86
+ m[:name] = name
87
+ m[:source_location] = source_location
80
88
  m[:app] = AppMap.configuration.name
81
- m[:feature] = feature_name if feature_name
82
- m[:feature_group] = feature_group_name if feature_group_name
83
89
  m[:frameworks] ||= []
84
90
  m[:frameworks] << {
85
91
  name: 'minitest',
@@ -88,6 +94,13 @@ module AppMap
88
94
  m[:recorder] = {
89
95
  name: 'minitest'
90
96
  }
97
+ m[:test_status] = test_status
98
+ if exception
99
+ m[:exception] = {
100
+ class: exception.class.name,
101
+ message: exception.to_s
102
+ }
103
+ end
91
104
  end
92
105
 
93
106
  appmap = {
@@ -96,14 +109,9 @@ module AppMap
96
109
  classMap: class_map,
97
110
  events: events
98
111
  }.compact
99
- fname = AppMap::Util.scenario_filename(example_name)
112
+ fname = AppMap::Util.scenario_filename(name)
100
113
 
101
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
102
- end
103
-
104
- def print_inventory
105
- class_map = AppMap.class_map(@event_methods)
106
- save 'Inventory', class_map, labels: %w[inventory]
114
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
107
115
  end
108
116
 
109
117
  def enabled?
@@ -112,9 +120,6 @@ module AppMap
112
120
 
113
121
  def run
114
122
  init
115
- at_exit do
116
- print_inventory
117
- end
118
123
  end
119
124
  end
120
125
  end
@@ -128,11 +133,11 @@ if AppMap::Minitest.enabled?
128
133
  alias run_without_hook run
129
134
 
130
135
  def run
131
- AppMap::Minitest.begin_test self
136
+ AppMap::Minitest.begin_test self, name
132
137
  begin
133
138
  run_without_hook
134
139
  ensure
135
- AppMap::Minitest.end_test self
140
+ AppMap::Minitest.end_test self, exception: $!
136
141
  end
137
142
  end
138
143
  end
@@ -7,14 +7,17 @@ module AppMap
7
7
  module Rails
8
8
  module RequestHandler
9
9
  class HTTPServerRequest < AppMap::Event::MethodEvent
10
- attr_accessor :normalized_path_info, :request_method, :path_info, :params
10
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
11
11
 
12
12
  def initialize(request)
13
13
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
14
 
15
- @request_method = request.request_method
16
- @normalized_path_info = normalized_path request
17
- @path_info = request.path_info.split('?')[0]
15
+ self.request_method = request.request_method
16
+ self.normalized_path_info = normalized_path(request)
17
+ self.mime_type = request.headers['Content-Type']
18
+ self.headers = AppMap::Util.select_headers(request.env)
19
+ self.authorization = request.headers['Authorization']
20
+ self.path_info = request.path_info.split('?')[0]
18
21
  # ActionDispatch::Http::ParameterFilter is deprecated
19
22
  parameter_filter_cls = \
20
23
  if defined?(ActiveSupport::ParameterFilter)
@@ -22,7 +25,7 @@ module AppMap
22
25
  else
23
26
  ActionDispatch::Http::ParameterFilter
24
27
  end
25
- @params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
28
+ self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
26
29
  end
27
30
 
28
31
  def to_h
@@ -30,17 +33,25 @@ module AppMap
30
33
  h[:http_server_request] = {
31
34
  request_method: request_method,
32
35
  path_info: path_info,
33
- normalized_path_info: normalized_path_info
36
+ mime_type: mime_type,
37
+ normalized_path_info: normalized_path_info,
38
+ authorization: authorization,
39
+ headers: headers,
34
40
  }.compact
35
41
 
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
- }
42
+ unless params.blank?
43
+ h[:message] = params.keys.map do |key|
44
+ val = params[key]
45
+ {
46
+ name: key,
47
+ class: val.class.name,
48
+ value: self.class.display_string(val),
49
+ object_id: val.__id__,
50
+ }.tap do |message|
51
+ properties = object_properties(val)
52
+ message[:properties] = properties if properties
53
+ end
54
+ end
44
55
  end
45
56
  end
46
57
  end
@@ -59,7 +70,7 @@ module AppMap
59
70
  end
60
71
 
61
72
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
62
- attr_accessor :status, :mime_type
73
+ attr_accessor :status, :mime_type, :headers
63
74
 
64
75
  def initialize(response, parent_id, elapsed)
65
76
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
@@ -68,13 +79,15 @@ module AppMap
68
79
  self.mime_type = response.headers['Content-Type']
69
80
  self.parent_id = parent_id
70
81
  self.elapsed = elapsed
82
+ self.headers = AppMap::Util.select_headers(response.headers)
71
83
  end
72
84
 
73
85
  def to_h
74
86
  super.tap do |h|
75
87
  h[:http_server_response] = {
76
- status: status,
77
- mime_type: mime_type
88
+ status_code: status,
89
+ mime_type: mime_type,
90
+ headers: headers
78
91
  }.compact
79
92
  end
80
93
  end