appmap 0.45.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.
@@ -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
@@ -95,7 +95,7 @@ module AppMap
95
95
  def after_hook(_receiver, call_event, start_time, return_value, exception)
96
96
  elapsed = TIME_NOW.call - start_time
97
97
  return_event = hook_package.handler_class.handle_return(call_event.id, elapsed, return_value, exception)
98
- AppMap.tracing.record_event return_event
98
+ AppMap.tracing.record_event(return_event) if return_event
99
99
  nil
100
100
  end
101
101
 
@@ -8,12 +8,12 @@ module AppMap
8
8
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
9
9
  # AppMap events.
10
10
  initializer 'appmap.subscribe' do |_| # params: app
11
- require 'appmap/rails/sql_handler'
12
- require 'appmap/rails/request_handler'
13
- ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
14
- ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
11
+ require 'appmap/handler/rails/sql_handler'
12
+ require 'appmap/handler/rails/request_handler'
13
+ ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Handler::Rails::SQLHandler.new
14
+ ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Handler::Rails::SQLHandler.new
15
15
 
16
- AppMap::Rails::RequestHandler::HookMethod.new.activate
16
+ AppMap::Handler::Rails::RequestHandler::HookMethod.new.activate
17
17
  end
18
18
 
19
19
  # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
data/lib/appmap/trace.rb CHANGED
@@ -2,14 +2,36 @@
2
2
 
3
3
  module AppMap
4
4
  module Trace
5
- class ScopedMethod < SimpleDelegator
6
- attr_reader :package, :defined_class, :static
5
+ class RubyMethod
6
+ attr_reader :class_name, :static
7
7
 
8
- def initialize(package, defined_class, method, static)
8
+ def initialize(package, class_name, method, static)
9
9
  @package = package
10
- @defined_class = defined_class
10
+ @class_name = class_name
11
+ @method = method
11
12
  @static = static
12
- super(method)
13
+ end
14
+
15
+ def source_location
16
+ @method.source_location
17
+ end
18
+
19
+ def comment
20
+ @method.comment
21
+ rescue MethodSource::SourceNotFoundError
22
+ nil
23
+ end
24
+
25
+ def package
26
+ @package.name
27
+ end
28
+
29
+ def name
30
+ @method.name
31
+ end
32
+
33
+ def labels
34
+ @package.labels
13
35
  end
14
36
  end
15
37
 
@@ -43,6 +65,12 @@ module AppMap
43
65
  end
44
66
  end
45
67
 
68
+ def record_method(method)
69
+ @tracers.each do |tracer|
70
+ tracer.record_method(method)
71
+ end
72
+ end
73
+
46
74
  def delete(tracer)
47
75
  return unless @tracers.member?(tracer)
48
76
 
@@ -83,10 +111,22 @@ module AppMap
83
111
  @last_package_for_thread[Thread.current.object_id] = package if package
84
112
  @events << event
85
113
  static = event.static if event.respond_to?(:static)
86
- @methods << Trace::ScopedMethod.new(package, defined_class, method, static) \
114
+ @methods << Trace::RubyMethod.new(package, defined_class, method, static) \
87
115
  if package && defined_class && method && (event.event == :call)
88
116
  end
89
117
 
118
+ # +method+ should be duck-typed to respond to the following:
119
+ # * package
120
+ # * defined_class
121
+ # * name
122
+ # * static
123
+ # * comment
124
+ # * labels
125
+ # * source_location
126
+ def record_method(method)
127
+ @methods << method
128
+ end
129
+
90
130
  # Gets the last package which was observed on the current thread.
91
131
  def last_package_for_current_thread
92
132
  @last_package_for_thread[Thread.current.object_id]
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.45.1'
6
+ VERSION = '0.46.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.0'
9
9
  end
data/package-lock.json CHANGED
@@ -551,9 +551,9 @@
551
551
  }
552
552
  },
553
553
  "lodash": {
554
- "version": "4.17.19",
555
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
556
- "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
554
+ "version": "4.17.21",
555
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
556
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
557
557
  },
558
558
  "longest": {
559
559
  "version": "1.0.1",
@@ -144,7 +144,49 @@ describe 'Rails' do
144
144
  end
145
145
 
146
146
  describe 'a UI route' do
147
- describe 'rendering a page' do
147
+ describe 'rendering a page using a template file' do
148
+ let(:appmap_json_file) do
149
+ 'UsersController_GET_users_lists_the_users.appmap.json'
150
+ end
151
+
152
+ it 'records the template file' do
153
+ expect(events).to include hash_including(
154
+ 'event' => 'call',
155
+ 'defined_class' => 'app_views_users_index_html_haml',
156
+ 'method_id' => 'render',
157
+ 'path' => 'app/views/users/index.html.haml'
158
+ )
159
+
160
+ expect(appmap['classMap']).to include hash_including(
161
+ 'name' => 'app/views',
162
+ 'children' => include(hash_including(
163
+ 'name' => 'app_views_users_index_html_haml',
164
+ 'children' => include(hash_including(
165
+ 'name' => 'render',
166
+ 'type' => 'function',
167
+ 'location' => 'app/views/users/index.html.haml',
168
+ 'static' => true,
169
+ 'labels' => [ 'mvc.template' ]
170
+ ))
171
+ ))
172
+ )
173
+ expect(appmap['classMap']).to include hash_including(
174
+ 'name' => 'app/views',
175
+ 'children' => include(hash_including(
176
+ 'name' => 'app_views_layouts_application_html_haml',
177
+ 'children' => include(hash_including(
178
+ 'name' => 'render',
179
+ 'type' => 'function',
180
+ 'location' => 'app/views/layouts/application.html.haml',
181
+ 'static' => true,
182
+ 'labels' => [ 'mvc.template' ]
183
+ ))
184
+ ))
185
+ )
186
+ end
187
+ end
188
+
189
+ describe 'rendering a page using a text template' do
148
190
  let(:appmap_json_file) do
149
191
  'UsersController_GET_users_login_shows_the_user.appmap.json'
150
192
  end
@@ -165,15 +207,32 @@ describe 'Rails' do
165
207
  )
166
208
  end
167
209
 
210
+ it 'ignores the text template' do
211
+ expect(events).to_not include hash_including(
212
+ 'event' => 'call',
213
+ 'method_id' => 'render',
214
+ 'render_template' => anything
215
+ )
216
+
217
+ expect(appmap['classMap']).to_not include hash_including(
218
+ 'name' => 'views',
219
+ 'children' => include(hash_including(
220
+ 'name' => 'ViewTemplate',
221
+ 'children' => include(hash_including(
222
+ 'name' => 'render',
223
+ 'type' => 'function',
224
+ 'location' => 'text template'
225
+ ))
226
+ ))
227
+ )
228
+ end
229
+
168
230
  it 'records and labels view rendering' do
169
231
  expect(events).to include hash_including(
170
232
  'event' => 'call',
171
233
  'thread_id' => Numeric,
172
- 'defined_class' => 'ActionView::Renderer',
173
- 'method_id' => 'render',
174
- 'path' => String,
175
- 'lineno' => Integer,
176
- 'static' => false
234
+ 'defined_class' => 'inline_template',
235
+ 'method_id' => 'render'
177
236
  )
178
237
 
179
238
  expect(appmap['classMap']).to include hash_including(