appmap 0.48.2 → 0.51.2
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/CHANGELOG.md +45 -0
- data/README.md +3 -332
- data/appmap.gemspec +3 -0
- data/exe/appmap-agent-setup +47 -0
- data/lib/appmap.rb +74 -7
- data/lib/appmap/command/init.rb +42 -0
- data/lib/appmap/config.rb +104 -33
- data/lib/appmap/handler/rails/template.rb +19 -5
- data/lib/appmap/minitest.rb +8 -2
- data/lib/appmap/railtie.rb +7 -0
- data/lib/appmap/rspec.rb +8 -2
- data/lib/appmap/service/guesser.rb +26 -0
- data/lib/appmap/trace.rb +4 -2
- data/lib/appmap/util.rb +21 -0
- data/lib/appmap/version.rb +4 -1
- data/spec/abstract_controller_base_spec.rb +57 -18
- data/spec/config_spec.rb +21 -0
- data/spec/fixtures/rails5_users_app/config/application.rb +0 -8
- data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +0 -2
- data/spec/fixtures/rails6_users_app/config/application.rb +0 -8
- data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +0 -2
- data/spec/hook_spec.rb +2 -2
- data/spec/record_net_http_spec.rb +1 -1
- data/test/cli_test.rb +37 -0
- metadata +9 -6
- data/spec/fixtures/rails5_users_app/config/initializers/record_button.rb +0 -3
- data/spec/fixtures/rails6_users_app/config/initializers/record_button.rb +0 -3
data/lib/appmap.rb
CHANGED
@@ -27,7 +27,7 @@ module AppMap
|
|
27
27
|
# Gets the configuration. If there is no configuration, the default
|
28
28
|
# configuration is initialized.
|
29
29
|
def configuration
|
30
|
-
@configuration ||=
|
30
|
+
@configuration ||= initialize_configuration
|
31
31
|
end
|
32
32
|
|
33
33
|
# Sets the configuration. This is only expected to happen once per
|
@@ -38,19 +38,34 @@ module AppMap
|
|
38
38
|
@configuration = config
|
39
39
|
end
|
40
40
|
|
41
|
-
|
41
|
+
def default_config_file_path
|
42
|
+
ENV['APPMAP_CONFIG_FILE'] || 'appmap.yml'
|
43
|
+
end
|
44
|
+
|
45
|
+
# Configures AppMap for recording. Default behavior is to configure from
|
46
|
+
# APPMAP_CONFIG_FILE, or 'appmap.yml'. If no config file is available, a
|
47
|
+
# configuration will be automatically generated and used - and the user is prompted
|
48
|
+
# to create the config file.
|
49
|
+
#
|
42
50
|
# This method also activates the code hooks which record function calls as trace events.
|
43
51
|
# Call this function before the program code is loaded by the Ruby VM, otherwise
|
44
52
|
# the load events won't be seen and the hooks won't activate.
|
45
|
-
def
|
46
|
-
|
47
|
-
warn "Configuring AppMap from path #{config_file_path}"
|
53
|
+
def initialize_configuration(config_file_path = default_config_file_path)
|
54
|
+
startup_message "Configuring AppMap from path #{config_file_path}"
|
48
55
|
Config.load_from_file(config_file_path).tap do |configuration|
|
49
56
|
self.configuration = configuration
|
50
57
|
Hook.new(configuration).enable
|
51
58
|
end
|
52
59
|
end
|
53
60
|
|
61
|
+
def info(msg)
|
62
|
+
if defined?(::Rails) && defined?(::Rails.logger)
|
63
|
+
::Rails.logger.info msg
|
64
|
+
else
|
65
|
+
warn msg
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
54
69
|
# Used to start tracing, stop tracing, and record events.
|
55
70
|
def tracing
|
56
71
|
@tracing ||= Trace::Tracing.new
|
@@ -94,8 +109,60 @@ module AppMap
|
|
94
109
|
@metadata ||= Metadata.detect.freeze
|
95
110
|
@metadata.deep_dup
|
96
111
|
end
|
112
|
+
|
113
|
+
def startup_message(msg)
|
114
|
+
if defined?(::Rails) && defined?(::Rails.logger) && ::Rails.logger
|
115
|
+
::Rails.logger.debug msg
|
116
|
+
elsif ENV['DEBUG'] == 'true'
|
117
|
+
warn msg
|
118
|
+
end
|
119
|
+
end
|
97
120
|
end
|
98
121
|
end
|
99
122
|
|
100
|
-
|
101
|
-
|
123
|
+
lambda do
|
124
|
+
Initializer = Struct.new(:class_name, :module_name, :gem_module_name)
|
125
|
+
|
126
|
+
INITIALIZERS = {
|
127
|
+
'Rails::Railtie' => Initializer.new('AppMap::Railtie', 'appmap/railtie', 'railtie'),
|
128
|
+
'RSpec' => Initializer.new('AppMap::RSpec', 'appmap/rspec', 'rspec-core'),
|
129
|
+
'Minitest::Unit::TestCase' => Initializer.new('AppMap::Minitest', 'appmap/minitest', 'minitest')
|
130
|
+
}
|
131
|
+
|
132
|
+
TracePoint.new(:class) do |tp|
|
133
|
+
cls_name = tp.self.name
|
134
|
+
initializers = INITIALIZERS.delete(cls_name)
|
135
|
+
if initializers
|
136
|
+
initializers = [ initializers ] unless initializers.is_a?(Array)
|
137
|
+
next if Object.const_defined?(initializers.first.class_name)
|
138
|
+
|
139
|
+
gem_module_name = initializers.first.gem_module_name
|
140
|
+
|
141
|
+
AppMap.startup_message AppMap::Util.color(<<~LOAD_MSG, :magenta)
|
142
|
+
When 'appmap' was loaded, '#{gem_module_name}' had not been loaded yet. Now '#{gem_module_name}' has
|
143
|
+
just been loaded, so the following AppMap modules will be automatically required:
|
144
|
+
|
145
|
+
#{initializers.map(&:module_name).join("\n")}
|
146
|
+
|
147
|
+
To suppress this message, ensure '#{gem_module_name}' appears before 'appmap' in your Gemfile.
|
148
|
+
LOAD_MSG
|
149
|
+
initializers.each do |init|
|
150
|
+
require init.module_name
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end.enable
|
154
|
+
|
155
|
+
if defined?(::Rails::Railtie)
|
156
|
+
require 'appmap/railtie'
|
157
|
+
end
|
158
|
+
|
159
|
+
if defined?(::RSpec)
|
160
|
+
require 'appmap/rspec'
|
161
|
+
end
|
162
|
+
|
163
|
+
if defined?(::Minitest)
|
164
|
+
require 'appmap/minitest'
|
165
|
+
end
|
166
|
+
end.call
|
167
|
+
|
168
|
+
AppMap.initialize_configuration if ENV['APPMAP'] == 'true'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'appmap/service/guesser'
|
5
|
+
require 'appmap/util'
|
6
|
+
|
7
|
+
module AppMap
|
8
|
+
module Command
|
9
|
+
InitStruct = Struct.new(:config_file)
|
10
|
+
|
11
|
+
class Init < InitStruct
|
12
|
+
def perform
|
13
|
+
if File.exist?(config_file)
|
14
|
+
puts AppMap::Util.color(%(The AppMap config file #{config_file} already exists.), :magenta)
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
ensure_directory_exists
|
19
|
+
|
20
|
+
config = {
|
21
|
+
'name' => Service::Guesser.guess_name,
|
22
|
+
'packages' => Service::Guesser.guess_paths.map { |path| { 'path' => path } }
|
23
|
+
}
|
24
|
+
content = YAML.dump(config).gsub("---\n", '')
|
25
|
+
|
26
|
+
File.write(config_file, content)
|
27
|
+
puts AppMap::Util.color(
|
28
|
+
%(The following AppMap config file #{config_file} has been created:),
|
29
|
+
:green
|
30
|
+
)
|
31
|
+
puts content
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def ensure_directory_exists
|
37
|
+
dirname = File.dirname(config_file)
|
38
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/appmap/config.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'yaml'
|
3
4
|
require 'appmap/handler/net_http'
|
4
5
|
require 'appmap/handler/rails/template'
|
6
|
+
require 'appmap/service/guesser'
|
5
7
|
|
6
8
|
module AppMap
|
7
9
|
class Config
|
@@ -167,8 +169,10 @@ module AppMap
|
|
167
169
|
),
|
168
170
|
package_hooks('actionpack',
|
169
171
|
[
|
170
|
-
method_hook('ActionDispatch::Request::Session', %i[
|
171
|
-
method_hook('ActionDispatch::
|
172
|
+
method_hook('ActionDispatch::Request::Session', %i[[] dig values fetch], %w[http.session.read]),
|
173
|
+
method_hook('ActionDispatch::Request::Session', %i[destroy[]= clear update delete merge], %w[http.session.write]),
|
174
|
+
method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.read]),
|
175
|
+
method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.write]),
|
172
176
|
method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
|
173
177
|
],
|
174
178
|
package_name: 'action_dispatch'
|
@@ -213,15 +217,22 @@ module AppMap
|
|
213
217
|
# This is happening: Method send_command not found on Net::IMAP
|
214
218
|
# 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
|
215
219
|
# 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
|
216
|
-
'Psych' =>
|
217
|
-
|
218
|
-
|
220
|
+
'Psych' => [
|
221
|
+
TargetMethods.new(%i[load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.parse])),
|
222
|
+
TargetMethods.new(%i[dump dump_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.generate])),
|
223
|
+
],
|
224
|
+
'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.parse])),
|
225
|
+
'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.generate])),
|
219
226
|
}.freeze
|
220
227
|
|
221
|
-
attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_hooks
|
228
|
+
attr_reader :name, :appmap_dir, :packages, :exclude, :hooked_methods, :builtin_hooks
|
222
229
|
|
223
|
-
def initialize(name,
|
230
|
+
def initialize(name,
|
231
|
+
packages: [],
|
232
|
+
exclude: [],
|
233
|
+
functions: [])
|
224
234
|
@name = name
|
235
|
+
@appmap_dir = AppMap::DEFAULT_APPMAP_DIR
|
225
236
|
@packages = packages
|
226
237
|
@hook_paths = Set.new(packages.map(&:path))
|
227
238
|
@exclude = exclude
|
@@ -248,38 +259,98 @@ module AppMap
|
|
248
259
|
class << self
|
249
260
|
# Loads configuration data from a file, specified by the file name.
|
250
261
|
def load_from_file(config_file_name)
|
251
|
-
|
252
|
-
|
262
|
+
logo = lambda do
|
263
|
+
Util.color(<<~LOGO, :magenta)
|
264
|
+
___ __ ___
|
265
|
+
/ _ | ___ ___ / |/ /__ ____
|
266
|
+
/ __ |/ _ \\/ _ \\/ /|_/ / _ `/ _ \\
|
267
|
+
/_/ |_/ .__/ .__/_/ /_/\\_,_/ .__/
|
268
|
+
/_/ /_/ /_/
|
269
|
+
LOGO
|
270
|
+
end
|
271
|
+
|
272
|
+
config_present = true if File.exists?(config_file_name)
|
273
|
+
|
274
|
+
config_data = if config_present
|
275
|
+
YAML.safe_load(::File.read(config_file_name))
|
276
|
+
else
|
277
|
+
warn logo.()
|
278
|
+
warn ''
|
279
|
+
warn Util.color(%Q|NOTICE: The AppMap config file #{config_file_name} was not found!|, :magenta, bold: true)
|
280
|
+
warn ''
|
281
|
+
warn Util.color(<<~MISSING_FILE_MSG, :magenta)
|
282
|
+
AppMap uses this file to customize its behavior. For example, you can use
|
283
|
+
the 'packages' setting to indicate which local file paths and dependency
|
284
|
+
gems you want to include in the AppMap. Since you haven't provided specific
|
285
|
+
settings, the appmap gem will try and guess some reasonable defaults.
|
286
|
+
To suppress this message, create the file:
|
287
|
+
|
288
|
+
#{Pathname.new(config_file_name).expand_path}
|
289
|
+
|
290
|
+
Here are the default settings that will be used in the meantime. You can
|
291
|
+
copy and paste this example to start your appmap.yml.
|
292
|
+
MISSING_FILE_MSG
|
293
|
+
{}
|
294
|
+
end
|
295
|
+
load(config_data).tap do |config|
|
296
|
+
config_yaml = {
|
297
|
+
'name' => config.name,
|
298
|
+
'packages' => config.packages.select{|p| p.path}.map do |pkg|
|
299
|
+
{ 'path' => pkg.path }
|
300
|
+
end,
|
301
|
+
'exclude' => []
|
302
|
+
}.compact
|
303
|
+
unless config_present
|
304
|
+
warn Util.color(YAML.dump(config_yaml), :magenta)
|
305
|
+
warn logo.()
|
306
|
+
end
|
307
|
+
end
|
253
308
|
end
|
254
309
|
|
255
310
|
# Loads configuration from a Hash.
|
256
311
|
def load(config_data)
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
312
|
+
name = config_data['name'] || Service::Guesser.guess_name
|
313
|
+
config_params = {
|
314
|
+
exclude: config_data['exclude']
|
315
|
+
}.compact
|
316
|
+
|
317
|
+
if config_data['functions']
|
318
|
+
config_params[:functions] = config_data['functions'].map do |function_data|
|
319
|
+
package = function_data['package']
|
320
|
+
cls = function_data['class']
|
321
|
+
functions = function_data['function'] || function_data['functions']
|
322
|
+
raise %q(AppMap config 'function' element should specify 'package', 'class' and 'function' or 'functions') unless package && cls && functions
|
323
|
+
|
324
|
+
functions = Array(functions).map(&:to_sym)
|
325
|
+
labels = function_data['label'] || function_data['labels']
|
326
|
+
labels = Array(labels).map(&:to_s) if labels
|
327
|
+
Function.new(package, cls, labels, functions)
|
328
|
+
end
|
266
329
|
end
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
330
|
+
|
331
|
+
config_params[:packages] = \
|
332
|
+
if config_data['packages']
|
333
|
+
config_data['packages'].map do |package|
|
334
|
+
gem = package['gem']
|
335
|
+
path = package['path']
|
336
|
+
raise %q(AppMap config 'package' element should specify 'gem' or 'path', not both) if gem && path
|
337
|
+
|
338
|
+
if gem
|
339
|
+
shallow = package['shallow']
|
340
|
+
# shallow is true by default for gems
|
341
|
+
shallow = true if shallow.nil?
|
342
|
+
Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
|
343
|
+
else
|
344
|
+
Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
|
345
|
+
end
|
346
|
+
end.compact
|
277
347
|
else
|
278
|
-
|
348
|
+
Array(Service::Guesser.guess_paths).map do |path|
|
349
|
+
Package.build_from_path(path)
|
350
|
+
end
|
279
351
|
end
|
280
|
-
|
281
|
-
|
282
|
-
Config.new config_data['name'], packages, exclude: exclude, functions: functions
|
352
|
+
|
353
|
+
Config.new name, config_params
|
283
354
|
end
|
284
355
|
end
|
285
356
|
|
@@ -289,7 +360,7 @@ module AppMap
|
|
289
360
|
packages: packages.map(&:to_h),
|
290
361
|
functions: @functions.map(&:to_h),
|
291
362
|
exclude: exclude
|
292
|
-
}
|
363
|
+
}.compact
|
293
364
|
end
|
294
365
|
|
295
366
|
# Determines if methods defined in a file path should possibly be hooked.
|
@@ -14,16 +14,30 @@ module AppMap
|
|
14
14
|
# The class name is generated from the template path. The package name is
|
15
15
|
# 'app/views', and the method name is 'render'. The source location of the method
|
16
16
|
# is, of course, the path to the view template.
|
17
|
-
TemplateMethod
|
18
|
-
private_instance_methods :path
|
17
|
+
class TemplateMethod
|
19
18
|
attr_reader :class_name
|
20
|
-
|
19
|
+
|
20
|
+
attr_reader :path
|
21
|
+
private_instance_methods :path
|
22
|
+
|
21
23
|
def initialize(path)
|
22
|
-
|
24
|
+
@path = path
|
23
25
|
|
24
26
|
@class_name = path.parameterize.underscore
|
25
27
|
end
|
26
|
-
|
28
|
+
|
29
|
+
def id
|
30
|
+
[ package, path, name ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash
|
34
|
+
id.hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def eql?(other)
|
38
|
+
other.is_a?(TemplateMethod) && id.eql?(other.id)
|
39
|
+
end
|
40
|
+
|
27
41
|
def package
|
28
42
|
'app/views'
|
29
43
|
end
|
data/lib/appmap/minitest.rb
CHANGED
@@ -54,15 +54,21 @@ module AppMap
|
|
54
54
|
|
55
55
|
@recordings_by_test = {}
|
56
56
|
@event_methods = Set.new
|
57
|
+
@recording_count = 0
|
57
58
|
|
58
59
|
class << self
|
59
60
|
def init
|
60
|
-
warn 'Configuring AppMap recorder for Minitest'
|
61
|
-
|
62
61
|
FileUtils.mkdir_p APPMAP_OUTPUT_DIR
|
63
62
|
end
|
64
63
|
|
64
|
+
def first_recording?
|
65
|
+
@recording_count == 0
|
66
|
+
end
|
67
|
+
|
65
68
|
def begin_test(test, name)
|
69
|
+
AppMap.info 'Configuring AppMap recorder for Minitest' if first_recording?
|
70
|
+
@recording_count += 1
|
71
|
+
|
66
72
|
@recordings_by_test[test.object_id] = Recording.new(test, name)
|
67
73
|
end
|
68
74
|
|
data/lib/appmap/railtie.rb
CHANGED
@@ -3,6 +3,13 @@
|
|
3
3
|
module AppMap
|
4
4
|
# Railtie connects the AppMap recorder to Rails-specific features.
|
5
5
|
class Railtie < ::Rails::Railtie
|
6
|
+
initializer 'appmap.remote_recording' do
|
7
|
+
require 'appmap/middleware/remote_recording'
|
8
|
+
Rails.application.config.middleware.insert_after \
|
9
|
+
Rails::Rack::Logger,
|
10
|
+
AppMap::Middleware::RemoteRecording
|
11
|
+
end
|
12
|
+
|
6
13
|
# appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
|
7
14
|
# AppMap events.
|
8
15
|
initializer 'appmap.subscribe' do |_| # params: app
|
data/lib/appmap/rspec.rb
CHANGED
@@ -139,15 +139,21 @@ module AppMap
|
|
139
139
|
|
140
140
|
@recordings_by_example = {}
|
141
141
|
@event_methods = Set.new
|
142
|
+
@recording_count = 0
|
142
143
|
|
143
144
|
class << self
|
144
145
|
def init
|
145
|
-
warn 'Configuring AppMap recorder for RSpec'
|
146
|
-
|
147
146
|
FileUtils.mkdir_p APPMAP_OUTPUT_DIR
|
148
147
|
end
|
149
148
|
|
149
|
+
def first_recording?
|
150
|
+
@recording_count == 0
|
151
|
+
end
|
152
|
+
|
150
153
|
def begin_spec(example)
|
154
|
+
AppMap.info 'Configuring AppMap recorder for RSpec' if first_recording?
|
155
|
+
@recording_count += 1
|
156
|
+
|
151
157
|
@recordings_by_example[example.object_id] = Recording.new(example)
|
152
158
|
end
|
153
159
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppMap
|
4
|
+
module Service
|
5
|
+
class Guesser
|
6
|
+
POSSIBLE_PATHS = %w[app/controllers app/models lib]
|
7
|
+
class << self
|
8
|
+
def guess_name
|
9
|
+
reponame = lambda do
|
10
|
+
next unless File.directory?('.git')
|
11
|
+
|
12
|
+
repo_name = `git config --get remote.origin.url`.strip
|
13
|
+
repo_name.split('/').last.split('.').first unless repo_name == ''
|
14
|
+
end
|
15
|
+
dirname = -> { Dir.pwd.split('/').last }
|
16
|
+
|
17
|
+
reponame.() || dirname.()
|
18
|
+
end
|
19
|
+
|
20
|
+
def guess_paths
|
21
|
+
POSSIBLE_PATHS.select { |path| File.directory?(path) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|