appmap 0.42.1 → 0.46.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.
- checksums.yaml +4 -4
- data/.releaserc.yml +11 -0
- data/.travis.yml +33 -2
- data/CHANGELOG.md +44 -0
- data/README.md +74 -16
- data/README_CI.md +29 -0
- data/Rakefile +4 -2
- data/appmap.gemspec +5 -3
- data/lib/appmap.rb +3 -7
- data/lib/appmap/class_map.rb +11 -22
- data/lib/appmap/command/record.rb +1 -1
- data/lib/appmap/config.rb +180 -67
- data/lib/appmap/cucumber.rb +1 -1
- data/lib/appmap/event.rb +46 -27
- data/lib/appmap/handler/function.rb +19 -0
- data/lib/appmap/handler/net_http.rb +107 -0
- data/lib/appmap/handler/rails/request_handler.rb +124 -0
- data/lib/appmap/handler/rails/sql_handler.rb +152 -0
- data/lib/appmap/handler/rails/template.rb +149 -0
- data/lib/appmap/hook.rb +111 -70
- data/lib/appmap/hook/method.rb +6 -8
- data/lib/appmap/middleware/remote_recording.rb +1 -1
- data/lib/appmap/minitest.rb +22 -20
- data/lib/appmap/railtie.rb +5 -5
- data/lib/appmap/record.rb +1 -1
- data/lib/appmap/rspec.rb +22 -21
- data/lib/appmap/trace.rb +47 -6
- data/lib/appmap/util.rb +47 -2
- data/lib/appmap/version.rb +2 -2
- data/package-lock.json +3 -3
- data/release.sh +17 -0
- data/spec/abstract_controller_base_spec.rb +140 -34
- data/spec/class_map_spec.rb +5 -13
- data/spec/config_spec.rb +33 -1
- data/spec/fixtures/hook/custom_instance_method.rb +11 -0
- data/spec/fixtures/hook/method_named_call.rb +11 -0
- data/spec/fixtures/rails5_users_app/Gemfile +7 -3
- data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
- data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails5_users_app/create_app +8 -2
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
- data/spec/fixtures/rails6_users_app/Gemfile +5 -4
- data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
- data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails6_users_app/create_app +8 -2
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
- data/spec/hook_spec.rb +143 -22
- data/spec/record_net_http_spec.rb +160 -0
- data/spec/record_sql_rails_pg_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -0
- data/test/expectations/openssl_test_key_sign1.json +2 -4
- data/test/gem_test.rb +1 -1
- data/test/rspec_test.rb +0 -13
- metadata +20 -14
- data/exe/appmap +0 -154
- data/lib/appmap/rails/request_handler.rb +0 -109
- data/lib/appmap/rails/sql_handler.rb +0 -150
- data/test/cli_test.rb +0 -116
@@ -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, return_value, exception, elapsed: elapsed)
|
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
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/event'
|
4
|
+
require 'appmap/hook'
|
5
|
+
|
6
|
+
module AppMap
|
7
|
+
module Handler
|
8
|
+
module Rails
|
9
|
+
module RequestHandler
|
10
|
+
class HTTPServerRequest < AppMap::Event::MethodEvent
|
11
|
+
attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
|
12
|
+
|
13
|
+
def initialize(request)
|
14
|
+
super AppMap::Event.next_id_counter, :call, Thread.current.object_id
|
15
|
+
|
16
|
+
self.request_method = request.request_method
|
17
|
+
self.normalized_path_info = normalized_path(request)
|
18
|
+
self.mime_type = request.headers['Content-Type']
|
19
|
+
self.headers = AppMap::Util.select_headers(request.env)
|
20
|
+
self.authorization = request.headers['Authorization']
|
21
|
+
self.path_info = request.path_info.split('?')[0]
|
22
|
+
# ActionDispatch::Http::ParameterFilter is deprecated
|
23
|
+
parameter_filter_cls = \
|
24
|
+
if defined?(ActiveSupport::ParameterFilter)
|
25
|
+
ActiveSupport::ParameterFilter
|
26
|
+
else
|
27
|
+
ActionDispatch::Http::ParameterFilter
|
28
|
+
end
|
29
|
+
self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_h
|
33
|
+
super.tap do |h|
|
34
|
+
h[:http_server_request] = {
|
35
|
+
request_method: request_method,
|
36
|
+
path_info: path_info,
|
37
|
+
mime_type: mime_type,
|
38
|
+
normalized_path_info: normalized_path_info,
|
39
|
+
authorization: authorization,
|
40
|
+
headers: headers,
|
41
|
+
}.compact
|
42
|
+
|
43
|
+
unless params.blank?
|
44
|
+
h[:message] = params.keys.map do |key|
|
45
|
+
val = params[key]
|
46
|
+
{
|
47
|
+
name: key,
|
48
|
+
class: val.class.name,
|
49
|
+
value: self.class.display_string(val),
|
50
|
+
object_id: val.__id__,
|
51
|
+
}.tap do |message|
|
52
|
+
properties = object_properties(val)
|
53
|
+
message[:properties] = properties if properties
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def normalized_path(request, router = ::Rails.application.routes.router)
|
63
|
+
router.recognize request do |route, _|
|
64
|
+
app = route.app
|
65
|
+
next unless app.matches? request
|
66
|
+
return normalized_path request, app.rack_app.routes.router if app.engine?
|
67
|
+
|
68
|
+
return route.path.spec.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
|
74
|
+
attr_accessor :status, :mime_type, :headers
|
75
|
+
|
76
|
+
def initialize(response, parent_id, elapsed)
|
77
|
+
super AppMap::Event.next_id_counter, :return, Thread.current.object_id
|
78
|
+
|
79
|
+
self.status = response.status
|
80
|
+
self.mime_type = response.headers['Content-Type']
|
81
|
+
self.parent_id = parent_id
|
82
|
+
self.elapsed = elapsed
|
83
|
+
self.headers = AppMap::Util.select_headers(response.headers)
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_h
|
87
|
+
super.tap do |h|
|
88
|
+
h[:http_server_response] = {
|
89
|
+
status_code: status,
|
90
|
+
mime_type: mime_type,
|
91
|
+
headers: headers
|
92
|
+
}.compact
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class HookMethod < AppMap::Hook::Method
|
98
|
+
def initialize
|
99
|
+
# ActionController::Instrumentation has issued start_processing.action_controller and
|
100
|
+
# process_action.action_controller since Rails 3. Therefore it's a stable place to hook
|
101
|
+
# the request. Rails controller notifications can't be used directly because they don't
|
102
|
+
# provide response headers, and we want the Content-Type.
|
103
|
+
super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def before_hook(receiver, defined_class, _) # args
|
109
|
+
call_event = HTTPServerRequest.new(receiver.request)
|
110
|
+
# http_server_request events are i/o and do not require a package name.
|
111
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
|
112
|
+
[ call_event, TIME_NOW.call ]
|
113
|
+
end
|
114
|
+
|
115
|
+
def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
|
116
|
+
elapsed = TIME_NOW.call - start_time
|
117
|
+
return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
|
118
|
+
AppMap.tracing.record_event return_event
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/event'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
module Handler
|
7
|
+
module Rails
|
8
|
+
class SQLHandler
|
9
|
+
class SQLCall < AppMap::Event::MethodCall
|
10
|
+
attr_accessor :payload
|
11
|
+
|
12
|
+
def initialize(payload)
|
13
|
+
super AppMap::Event.next_id_counter, :call, Thread.current.object_id
|
14
|
+
|
15
|
+
self.payload = payload
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
super.tap do |h|
|
20
|
+
h[:sql_query] = {
|
21
|
+
sql: payload[:sql],
|
22
|
+
database_type: payload[:database_type]
|
23
|
+
}.tap do |sql_query|
|
24
|
+
%i[server_version].each do |attribute|
|
25
|
+
sql_query[attribute] = payload[attribute] if payload[attribute]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
|
33
|
+
def initialize(parent_id, elapsed)
|
34
|
+
super AppMap::Event.next_id_counter, :return, Thread.current.object_id
|
35
|
+
|
36
|
+
self.parent_id = parent_id
|
37
|
+
self.elapsed = elapsed
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module SQLExaminer
|
42
|
+
class << self
|
43
|
+
def examine(payload, sql:)
|
44
|
+
return unless (examiner = build_examiner)
|
45
|
+
|
46
|
+
payload[:server_version] = examiner.server_version
|
47
|
+
payload[:database_type] = examiner.database_type.to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def build_examiner
|
53
|
+
if defined?(Sequel)
|
54
|
+
SequelExaminer.new
|
55
|
+
elsif defined?(ActiveRecord)
|
56
|
+
ActiveRecordExaminer.new
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class SequelExaminer
|
62
|
+
def server_version
|
63
|
+
Sequel::Model.db.server_version
|
64
|
+
end
|
65
|
+
|
66
|
+
def database_type
|
67
|
+
Sequel::Model.db.database_type.to_sym
|
68
|
+
end
|
69
|
+
|
70
|
+
def execute_query(sql)
|
71
|
+
Sequel::Model.db[sql].all
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class ActiveRecordExaminer
|
76
|
+
@@db_version_warning_issued = {}
|
77
|
+
|
78
|
+
def issue_warning
|
79
|
+
db_type = database_type
|
80
|
+
return if @@db_version_warning_issued[db_type]
|
81
|
+
warn("AppMap: Unable to determine database version for #{db_type.inspect}")
|
82
|
+
@@db_version_warning_issued[db_type] = true
|
83
|
+
end
|
84
|
+
|
85
|
+
def server_version
|
86
|
+
ActiveRecord::Base.connection.try(:database_version) || issue_warning
|
87
|
+
end
|
88
|
+
|
89
|
+
def database_type
|
90
|
+
type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
|
91
|
+
type = :postgres if type == :postgresql
|
92
|
+
|
93
|
+
type
|
94
|
+
end
|
95
|
+
|
96
|
+
def execute_query(sql)
|
97
|
+
ActiveRecord::Base.connection.execute(sql).inject([]) { |memo, r| memo << r; memo }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
|
103
|
+
return if AppMap.tracing.empty?
|
104
|
+
|
105
|
+
reentry_key = "#{self.class.name}#call"
|
106
|
+
return if Thread.current[reentry_key] == true
|
107
|
+
|
108
|
+
Thread.current[reentry_key] = true
|
109
|
+
begin
|
110
|
+
sql = payload[:sql].strip
|
111
|
+
|
112
|
+
# Detect whether a function call within a specified filename is present in the call stack.
|
113
|
+
find_in_backtrace = lambda do |file_name, function_name = nil|
|
114
|
+
Thread.current.backtrace.find do |line|
|
115
|
+
tokens = line.split(':')
|
116
|
+
matches_file = tokens.find { |t| t.rindex(file_name) == (t.length - file_name.length) }
|
117
|
+
matches_function = function_name.nil? || tokens.find { |t| t == "in `#{function_name}'" }
|
118
|
+
matches_file && matches_function
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Ignore SQL calls which are made while establishing a new connection.
|
123
|
+
#
|
124
|
+
# Example:
|
125
|
+
# /path/to/ruby/2.6.0/gems/sequel-5.20.0/lib/sequel/connection_pool.rb:122:in `make_new'
|
126
|
+
return if find_in_backtrace.call('lib/sequel/connection_pool.rb', 'make_new')
|
127
|
+
# lib/active_record/connection_adapters/abstract/connection_pool.rb:811:in `new_connection'
|
128
|
+
return if find_in_backtrace.call('lib/active_record/connection_adapters/abstract/connection_pool.rb', 'new_connection')
|
129
|
+
|
130
|
+
# Ignore SQL calls which are made while inspecting the DB schema.
|
131
|
+
#
|
132
|
+
# Example:
|
133
|
+
# /path/to/ruby/2.6.0/gems/sequel-5.20.0/lib/sequel/model/base.rb:812:in `get_db_schema'
|
134
|
+
return if find_in_backtrace.call('lib/sequel/model/base.rb', 'get_db_schema')
|
135
|
+
# /usr/local/bundle/gems/activerecord-5.2.3/lib/active_record/model_schema.rb:466:in `load_schema!'
|
136
|
+
return if find_in_backtrace.call('lib/active_record/model_schema.rb', 'load_schema!')
|
137
|
+
return if find_in_backtrace.call('lib/active_model/attribute_methods.rb', 'define_attribute_methods')
|
138
|
+
return if find_in_backtrace.call('lib/active_record/connection_adapters/schema_cache.rb')
|
139
|
+
|
140
|
+
SQLExaminer.examine payload, sql: sql
|
141
|
+
|
142
|
+
call = SQLCall.new(payload)
|
143
|
+
AppMap.tracing.record_event(call)
|
144
|
+
AppMap.tracing.record_event(SQLReturn.new(call.id, finished - started))
|
145
|
+
ensure
|
146
|
+
Thread.current[reentry_key] = nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/event'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
module Handler
|
7
|
+
module Rails
|
8
|
+
class Template
|
9
|
+
LOG = (ENV['APPMAP_TEMPLATE_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
|
10
|
+
|
11
|
+
# All the code which is touched by the AppMap is recorded in the classMap.
|
12
|
+
# This duck-typed 'method' is used to represent a view template as a package,
|
13
|
+
# class, and method in the classMap.
|
14
|
+
# The class name is generated from the template path. The package name is
|
15
|
+
# 'app/views', and the method name is 'render'. The source location of the method
|
16
|
+
# is, of course, the path to the view template.
|
17
|
+
TemplateMethod = Struct.new(:path) do
|
18
|
+
private_instance_methods :path
|
19
|
+
attr_reader :class_name
|
20
|
+
|
21
|
+
def initialize(path)
|
22
|
+
super
|
23
|
+
|
24
|
+
@class_name = path.parameterize.underscore
|
25
|
+
end
|
26
|
+
|
27
|
+
def package
|
28
|
+
'app/views'
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
'render'
|
33
|
+
end
|
34
|
+
|
35
|
+
def source_location
|
36
|
+
path
|
37
|
+
end
|
38
|
+
|
39
|
+
def static
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def comment
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def labels
|
48
|
+
[ 'mvc.template' ]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# TemplateCall is a type of function call which is specialized to view template rendering. Since
|
53
|
+
# there isn't really a perfect method in Rails to hook, this one is synthesized from the available
|
54
|
+
# information.
|
55
|
+
class TemplateCall < AppMap::Event::MethodEvent
|
56
|
+
# This is basically the +self+ parameter.
|
57
|
+
attr_reader :render_instance
|
58
|
+
# Path to the view template.
|
59
|
+
attr_accessor :path
|
60
|
+
|
61
|
+
def initialize(render_instance)
|
62
|
+
super :call
|
63
|
+
|
64
|
+
AppMap::Event::MethodEvent.build_from_invocation(:call, event: self)
|
65
|
+
@render_instance = render_instance
|
66
|
+
end
|
67
|
+
|
68
|
+
def static?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_h
|
73
|
+
super.tap do |h|
|
74
|
+
h[:defined_class] = path ? path.parameterize.underscore : 'inline_template'
|
75
|
+
h[:method_id] = 'render'
|
76
|
+
h[:path] = path
|
77
|
+
h[:static] = static?
|
78
|
+
h[:parameters] = []
|
79
|
+
h[:receiver] = {
|
80
|
+
class: AppMap::Event::MethodEvent.best_class_name(render_instance),
|
81
|
+
object_id: render_instance.__id__,
|
82
|
+
value: AppMap::Event::MethodEvent.display_string(render_instance)
|
83
|
+
}
|
84
|
+
h.compact
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
TEMPLATE_RENDERER = 'appmap.handler.rails.template.renderer'
|
90
|
+
|
91
|
+
# Hooks the ActionView::Resolver methods +find_all+, +find_all_anywhere+. The resolver is used
|
92
|
+
# during template rendering to lookup the template file path from parameters such as the
|
93
|
+
# template name, prefix, and partial (boolean).
|
94
|
+
class ResolverHandler
|
95
|
+
class << self
|
96
|
+
# Handled as a normal function call.
|
97
|
+
def handle_call(defined_class, hook_method, receiver, args)
|
98
|
+
name, prefix, partial = args
|
99
|
+
warn "Resolver: #{{ name: name, prefix: prefix, partial: partial }}" if LOG
|
100
|
+
|
101
|
+
AppMap::Handler::Function.handle_call(defined_class, hook_method, receiver, args)
|
102
|
+
end
|
103
|
+
|
104
|
+
# When the resolver returns, look to see if there is template rendering underway.
|
105
|
+
# If so, populate the template path. In all cases, add a TemplateMethod so that the
|
106
|
+
# template will be recorded in the classMap.
|
107
|
+
def handle_return(call_event_id, elapsed, return_value, exception)
|
108
|
+
warn "Resolver return: #{return_value.inspect}" if LOG
|
109
|
+
|
110
|
+
renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
|
111
|
+
path = Array(return_value).first&.inspect
|
112
|
+
|
113
|
+
if path
|
114
|
+
AppMap.tracing.record_method(TemplateMethod.new(path))
|
115
|
+
renderer.path ||= path if renderer
|
116
|
+
end
|
117
|
+
|
118
|
+
AppMap::Handler::Function.handle_return(call_event_id, elapsed, return_value, exception)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Hooks the ActionView::Renderer method +render+. This method is used by Rails to perform
|
124
|
+
# template rendering. The TemplateCall event which is emitted by this handler has a
|
125
|
+
# +path+ parameter, which is nil until it's filled in by a ResolverHandler.
|
126
|
+
class RenderHandler
|
127
|
+
class << self
|
128
|
+
def handle_call(defined_class, hook_method, receiver, args)
|
129
|
+
context, options = args
|
130
|
+
|
131
|
+
warn "Renderer: #{options}" if LOG
|
132
|
+
|
133
|
+
TemplateCall.new(receiver).tap do |call|
|
134
|
+
Thread.current[TEMPLATE_RENDERER] ||= []
|
135
|
+
Thread.current[TEMPLATE_RENDERER] << call
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def handle_return(call_event_id, elapsed, return_value, exception)
|
140
|
+
Array(Thread.current[TEMPLATE_RENDERER]).pop
|
141
|
+
|
142
|
+
AppMap::Event::MethodReturnIgnoreValue.build_from_invocation(call_event_id, elapsed: elapsed)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|