appmap 0.66.1 → 0.68.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +45 -0
  4. data/lib/appmap/builtin_hooks/json.yml +4 -0
  5. data/lib/appmap/builtin_hooks/net/http.yml +3 -0
  6. data/lib/appmap/builtin_hooks/openssl.yml +16 -0
  7. data/lib/appmap/builtin_hooks/yaml.yml +10 -0
  8. data/lib/appmap/command/agent_setup/validate.rb +8 -1
  9. data/lib/appmap/command/inspect.rb +0 -1
  10. data/lib/appmap/config.rb +157 -137
  11. data/lib/appmap/depends/api.rb +1 -1
  12. data/lib/appmap/depends/configuration.rb +0 -2
  13. data/lib/appmap/depends/util.rb +1 -1
  14. data/lib/appmap/event.rb +6 -2
  15. data/lib/appmap/gem_hooks/actionpack.yml +40 -0
  16. data/lib/appmap/gem_hooks/actionview.yml +13 -0
  17. data/lib/appmap/gem_hooks/activesupport.yml +12 -0
  18. data/lib/appmap/gem_hooks/cancancan.yml +6 -0
  19. data/lib/appmap/handler/net_http.rb +14 -12
  20. data/lib/appmap/handler/rails/request_handler.rb +4 -10
  21. data/lib/appmap/hook.rb +46 -30
  22. data/lib/appmap/minitest.rb +1 -0
  23. data/lib/appmap/rspec.rb +1 -0
  24. data/lib/appmap/swagger/configuration.rb +2 -0
  25. data/lib/appmap/util.rb +20 -2
  26. data/lib/appmap/version.rb +1 -1
  27. data/lib/appmap.rb +2 -2
  28. data/spec/config_spec.rb +207 -65
  29. data/spec/depends/api_spec.rb +3 -3
  30. data/spec/fixtures/config/missing_path_or_gem.yml +3 -0
  31. data/spec/rails_recording_spec.rb +1 -1
  32. data/spec/rails_spec_helper.rb +6 -1
  33. data/test/agent_setup_validate_test.rb +35 -8
  34. data/test/fixtures/mocha_mock_app/Gemfile +5 -0
  35. data/test/fixtures/mocha_mock_app/appmap.yml +5 -0
  36. data/test/fixtures/mocha_mock_app/lib/sheep.rb +5 -0
  37. data/test/fixtures/mocha_mock_app/test/sheep_test.rb +18 -0
  38. data/test/mock_compatibility_test.rb +45 -0
  39. metadata +17 -3
@@ -0,0 +1,40 @@
1
+ - methods:
2
+ - ActionDispatch::Request::Session#[]
3
+ - ActionDispatch::Request::Session#dig
4
+ - ActionDispatch::Request::Session#values
5
+ - ActionDispatch::Request::Session#fetch
6
+ - ActionDispatch::Cookies::CookieJar#[]
7
+ - ActionDispatch::Cookies::CookieJar#fetch
8
+ label: http.session.read
9
+ require_name: action_dispatch
10
+ - methods:
11
+ - ActionDispatch::Request::Session#destroy
12
+ - ActionDispatch::Request::Session#[]=
13
+ - ActionDispatch::Request::Session#clear
14
+ - ActionDispatch::Request::Session#update
15
+ - ActionDispatch::Request::Session#delete
16
+ - ActionDispatch::Request::Session#merge
17
+ - ActionDispatch::Cookies::CookieJar#[]=
18
+ - ActionDispatch::Cookies::CookieJar#clear
19
+ - ActionDispatch::Cookies::CookieJar#update
20
+ - ActionDispatch::Cookies::CookieJar#delete
21
+ - ActionDispatch::Cookies::CookieJar#recycle!
22
+ label: http.session.write
23
+ require_name: action_dispatch
24
+ - methods:
25
+ - ActionDispatch::Cookies::EncryptedCookieJar#[]=
26
+ - ActionDispatch::Cookies::EncryptedCookieJar#clear
27
+ - ActionDispatch::Cookies::EncryptedCookieJar#update
28
+ - ActionDispatch::Cookies::EncryptedCookieJar#delete
29
+ - ActionDispatch::Cookies::EncryptedCookieJar#recycle
30
+ labels:
31
+ - http.cookie
32
+ - crypto.encrypt
33
+ require_name: action_dispatch
34
+ - methods:
35
+ - ActionController::Instrumentation#process_action
36
+ - ActionController::Instrumentation#send_file
37
+ - ActionController::Instrumentation#send_data
38
+ - ActionController::Instrumentation#redirect_to
39
+ label: mvc.controller
40
+ require_name: action_controller
@@ -0,0 +1,13 @@
1
+ - methods:
2
+ - ActionView::Renderer#render
3
+ - ActionView::TemplateRenderer#render
4
+ - ActionView::PartialRenderer#render
5
+ label: mvc.view
6
+ handler_class: AppMap::Handler::Rails::Template::RenderHandler
7
+ require_name: action_view
8
+ - methods:
9
+ - ActionView::Resolver#find_all
10
+ - ActionView::Resolver#find_all_anywhere
11
+ label: mvc.template.resolver
12
+ handler_class: AppMap::Handler::Rails::Template::ResolverHandler
13
+ require_name: action_view
@@ -0,0 +1,12 @@
1
+ - method: ActiveSupport::Callbacks::CallbackSequence#invoke_before
2
+ label: mvc.before_action
3
+ require_name: active_support
4
+ force: true
5
+ - method: ActiveSupport::Callbacks::CallbackSequence#invoke_after
6
+ label: mvc.after_action
7
+ require_name: active_support
8
+ force: true
9
+ - method: ActiveSupport::SecurityUtils#secure_compare
10
+ label: crypto.secure_compare
11
+ require_name: active_support/security_utils
12
+ force: true
@@ -0,0 +1,6 @@
1
+ - methods:
2
+ - CanCan::ControllerAdditions#authorize!
3
+ - CanCan::ControllerAdditions#can?
4
+ - CanCan::ControllerAdditions#cannot?
5
+ - CanCan::Ability#authorize?
6
+ label: security.authorization
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'appmap/event'
4
4
  require 'appmap/util'
5
+ require 'rack'
5
6
 
6
7
  module AppMap
7
8
  module Handler
@@ -27,7 +28,7 @@ module AppMap
27
28
 
28
29
  self.request_method = request.method
29
30
  self.url = url
30
- self.headers = AppMap::Util.select_headers(NetHTTP.request_headers(request))
31
+ self.headers = NetHTTP.copy_headers(request)
31
32
  self.params = Rack::Utils.parse_nested_query(query)
32
33
  end
33
34
 
@@ -55,22 +56,25 @@ module AppMap
55
56
  end
56
57
 
57
58
  class HTTPClientResponse < AppMap::Event::MethodReturnIgnoreValue
58
- attr_accessor :status, :mime_type, :headers
59
+ attr_accessor :status, :headers
59
60
 
60
61
  def initialize(response, parent_id, elapsed)
61
62
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
62
63
 
63
- self.status = response.code.to_i
64
+ if response
65
+ self.status = response.code.to_i
66
+ self.headers = NetHTTP.copy_headers(response)
67
+ else
68
+ self.headers = {}
69
+ end
64
70
  self.parent_id = parent_id
65
71
  self.elapsed = elapsed
66
- self.headers = AppMap::Util.select_headers(NetHTTP.response_headers(response))
67
72
  end
68
73
 
69
74
  def to_h
70
75
  super.tap do |h|
71
76
  h[:http_client_response] = {
72
77
  status_code: status,
73
- mime_type: mime_type,
74
78
  headers: headers
75
79
  }.compact
76
80
  end
@@ -79,17 +83,15 @@ module AppMap
79
83
 
80
84
  class NetHTTP
81
85
  class << self
82
- def request_headers(request)
86
+ def copy_headers(obj)
83
87
  {}.tap do |headers|
84
- request.each_header do |k,v|
85
- key = [ 'HTTP', Util.underscore(k).upcase ].join('_')
86
- headers[key] = v
88
+ obj.each_header do |key, value|
89
+ key = key.split('-').map(&:capitalize).join('-')
90
+ headers[key] = value
87
91
  end
88
92
  end
89
93
  end
90
-
91
- alias response_headers request_headers
92
-
94
+
93
95
  def handle_call(defined_class, hook_method, receiver, args)
94
96
  # request will call itself again in a start block if it's not already started.
95
97
  return unless receiver.started?
@@ -9,16 +9,14 @@ module AppMap
9
9
  module Rails
10
10
  module RequestHandler
11
11
  class HTTPServerRequest < AppMap::Event::MethodEvent
12
- attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
12
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params, :headers
13
13
 
14
14
  def initialize(request)
15
15
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
16
16
 
17
17
  self.request_method = request.request_method
18
18
  self.normalized_path_info = normalized_path(request)
19
- self.mime_type = request.headers['Content-Type']
20
- self.headers = AppMap::Util.select_headers(request.env)
21
- self.authorization = request.headers['Authorization']
19
+ self.headers = AppMap::Util.select_rack_headers(request.env)
22
20
  self.path_info = request.path_info.split('?')[0]
23
21
  # ActionDispatch::Http::ParameterFilter is deprecated
24
22
  parameter_filter_cls = \
@@ -35,9 +33,7 @@ module AppMap
35
33
  h[:http_server_request] = {
36
34
  request_method: request_method,
37
35
  path_info: path_info,
38
- mime_type: mime_type,
39
36
  normalized_path_info: normalized_path_info,
40
- authorization: authorization,
41
37
  headers: headers,
42
38
  }.compact
43
39
 
@@ -72,23 +68,21 @@ module AppMap
72
68
  end
73
69
 
74
70
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
75
- attr_accessor :status, :mime_type, :headers
71
+ attr_accessor :status, :headers
76
72
 
77
73
  def initialize(response, parent_id, elapsed)
78
74
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
79
75
 
80
76
  self.status = response.status
81
- self.mime_type = response.headers['Content-Type']
82
77
  self.parent_id = parent_id
83
78
  self.elapsed = elapsed
84
- self.headers = AppMap::Util.select_headers(response.headers)
79
+ self.headers = response.headers.dup
85
80
  end
86
81
 
87
82
  def to_h
88
83
  super.tap do |h|
89
84
  h[:http_server_response] = {
90
85
  status_code: status,
91
- mime_type: mime_type,
92
86
  headers: headers
93
87
  }.compact
94
88
  end
data/lib/appmap/hook.rb CHANGED
@@ -15,10 +15,23 @@ module AppMap
15
15
  @method_arity = ::Method.instance_method(:arity)
16
16
 
17
17
  class << self
18
- def lock_builtins
19
- return if @builtins_hooked
18
+ def hook_builtins?
19
+ Mutex.new.synchronize do
20
+ @hook_builtins = true if @hook_builtins.nil?
20
21
 
21
- @builtins_hooked = true
22
+ return false unless @hook_builtins
23
+
24
+ @hook_builtins = false
25
+ true
26
+ end
27
+ end
28
+
29
+ def already_hooked?(method)
30
+ # After a method is defined, the statement "module_function <the-method>" can convert that method
31
+ # into a module (class) method. The method is hooked first when it's defined, then AppMap will attempt to
32
+ # hook it again when it's redefined as a module method. So we check the method source location - if it's
33
+ # part of the AppMap source tree, we ignore it.
34
+ method.source_location && method.source_location[0].index(__dir__) == 0
22
35
  end
23
36
 
24
37
  # Return the class, separator ('.' or '#'), and method name for
@@ -79,42 +92,43 @@ module AppMap
79
92
  # hook_builtins builds hooks for code that is built in to the Ruby standard library.
80
93
  # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
81
94
  def hook_builtins
82
- return unless self.class.lock_builtins
95
+ return unless self.class.hook_builtins?
83
96
 
84
- class_from_string = lambda do |fq_class|
85
- fq_class.split('::').inject(Object) do |mod, class_name|
86
- mod.const_get(class_name)
87
- end
88
- end
97
+ hook_loaded_code = lambda do |hooks_by_class, builtin|
98
+ hooks_by_class.each do |class_name, hooks|
99
+ Array(hooks).each do |hook|
100
+ require hook.package.require_name if builtin && hook.package.require_name && hook.package.require_name != 'ruby'
89
101
 
90
- config.builtin_hooks.each do |class_name, hooks|
91
- Array(hooks).each do |hook|
92
- require hook.package.package_name if hook.package.package_name && hook.package.package_name != 'ruby'
93
- Array(hook.method_names).each do |method_name|
94
- method_name = method_name.to_sym
95
- base_cls = class_from_string.(class_name)
102
+ Array(hook.method_names).each do |method_name|
103
+ method_name = method_name.to_sym
104
+ base_cls = Util::class_from_string(class_name, must: false)
105
+ next unless base_cls
96
106
 
97
- hook_method = lambda do |entry|
98
- cls, method = entry
99
- return false if config.never_hook?(cls, method)
107
+ hook_method = lambda do |entry|
108
+ cls, method = entry
109
+ return false if config.never_hook?(cls, method)
100
110
 
101
- Hook::Method.new(hook.package, cls, method).activate
102
- end
111
+ Hook::Method.new(hook.package, cls, method).activate
112
+ end
103
113
 
104
- methods = []
105
- methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
106
- if base_cls.respond_to?(:singleton_class)
107
- methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
108
- end
109
- methods.compact!
110
- if methods.empty?
111
- warn "Method #{method_name} not found on #{base_cls.name}"
112
- else
113
- methods.each(&hook_method)
114
+ methods = []
115
+ methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
116
+ if base_cls.respond_to?(:singleton_class)
117
+ methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
118
+ end
119
+ methods.compact!
120
+ if methods.empty?
121
+ warn "Method #{method_name} not found on #{base_cls.name}" if LOG
122
+ else
123
+ methods.each(&hook_method)
124
+ end
114
125
  end
115
126
  end
116
127
  end
117
128
  end
129
+
130
+ hook_loaded_code.(config.builtin_hooks, true)
131
+ hook_loaded_code.(config.gem_hooks, false)
118
132
  end
119
133
 
120
134
  protected
@@ -165,6 +179,8 @@ module AppMap
165
179
  next
166
180
  end
167
181
 
182
+ next if self.class.already_hooked?(method)
183
+
168
184
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
169
185
 
170
186
  disasm = RubyVM::InstructionSequence.disasm(method)
@@ -142,6 +142,7 @@ if AppMap::Minitest.enabled?
142
142
  alias run_without_hook run
143
143
 
144
144
  def run
145
+ GC.start
145
146
  AppMap::Minitest.begin_test self, name
146
147
  begin
147
148
  run_without_hook
data/lib/appmap/rspec.rb CHANGED
@@ -87,6 +87,7 @@ module AppMap
87
87
  end
88
88
 
89
89
  warn "Starting recording of example #{example}@#{source_location}" if AppMap::RSpec::LOG
90
+ GC.start
90
91
  @trace = AppMap.tracing.trace
91
92
  @webdriver_port = webdriver_port.()
92
93
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module AppMap
data/lib/appmap/util.rb CHANGED
@@ -21,6 +21,14 @@ module AppMap
21
21
  WHITE = "\e[37m"
22
22
 
23
23
  class << self
24
+ def class_from_string(fq_class, must: true)
25
+ fq_class.split('::').inject(Object) do |mod, class_name|
26
+ mod.const_get(class_name)
27
+ end
28
+ rescue NameError
29
+ raise if must
30
+ end
31
+
24
32
  def parse_function_name(name)
25
33
  package_tokens = name.split('/')
26
34
 
@@ -108,8 +116,18 @@ module AppMap
108
116
  event
109
117
  end
110
118
 
111
- def select_headers(env)
119
+ def select_rack_headers(env)
120
+ finalize_headers = lambda do |headers|
121
+ blank?(headers) ? nil : headers
122
+ end
123
+
112
124
  # Rack prepends HTTP_ to all client-sent headers.
125
+
126
+ if !env['rack.version']
127
+ warn "Request headers does not contain rack.version. HTTP_ prefix is not expected."
128
+ return finalize_headers.call(env.dup)
129
+ end
130
+
113
131
  matching_headers = env
114
132
  .select { |k,v| k.start_with? 'HTTP_'}
115
133
  .reject { |k,v| blank?(v) }
@@ -118,7 +136,7 @@ module AppMap
118
136
  value = kv[1]
119
137
  memo[key] = value
120
138
  end
121
- blank?(matching_headers) ? nil : matching_headers
139
+ finalize_headers.call(matching_headers)
122
140
  end
123
141
 
124
142
  def normalize_path(path)
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.66.1'
6
+ VERSION = '0.68.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
data/lib/appmap.rb CHANGED
@@ -74,6 +74,6 @@ lambda do
74
74
  require 'appmap/depends'
75
75
  end
76
76
 
77
- end.call
77
+ end.call unless ENV['APPMAP_AUTOREQUIRE'] == 'false'
78
78
 
79
- AppMap.initialize_configuration if ENV['APPMAP'] == 'true'
79
+ AppMap.initialize_configuration if ENV['APPMAP'] == 'true' && ENV['APPMAP_INITIALIZE'] != 'false'