appmap 0.77.2 → 0.78.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.travis.yml +0 -7
- data/CHANGELOG.md +21 -0
- data/Rakefile +29 -3
- data/ext/appmap/appmap.c +21 -2
- data/lib/appmap/builtin_hooks/ruby.yml +6 -3
- data/lib/appmap/config.rb +9 -1
- data/lib/appmap/handler/eval.rb +41 -0
- data/lib/appmap/handler/function.rb +8 -8
- data/lib/appmap/handler/net_http.rb +19 -18
- data/lib/appmap/handler/rails/request_handler.rb +3 -4
- data/lib/appmap/handler/rails/template.rb +68 -62
- data/lib/appmap/hook/method/ruby2.rb +56 -0
- data/lib/appmap/hook/method/ruby3.rb +56 -0
- data/lib/appmap/hook/method.rb +42 -98
- data/lib/appmap/hook.rb +2 -2
- data/lib/appmap/version.rb +1 -1
- data/spec/fixtures/rails7_users_app/.dockerignore +1 -0
- data/spec/fixtures/rails7_users_app/.gitattributes +7 -0
- data/spec/fixtures/rails7_users_app/.gitignore +31 -0
- data/spec/fixtures/rails7_users_app/.rspec +1 -0
- data/spec/fixtures/rails7_users_app/.ruby-version +1 -0
- data/spec/fixtures/rails7_users_app/Dockerfile +30 -0
- data/spec/fixtures/rails7_users_app/Dockerfile.pg +3 -0
- data/spec/fixtures/rails7_users_app/Gemfile +99 -0
- data/spec/fixtures/rails7_users_app/README.md +24 -0
- data/spec/fixtures/rails7_users_app/Rakefile +6 -0
- data/spec/fixtures/rails7_users_app/app/assets/config/manifest.js +4 -0
- data/spec/fixtures/rails7_users_app/app/assets/images/.keep +0 -0
- data/spec/fixtures/rails7_users_app/app/assets/stylesheets/application.css +15 -0
- data/spec/fixtures/rails7_users_app/app/channels/application_cable/channel.rb +4 -0
- data/spec/fixtures/rails7_users_app/app/channels/application_cable/connection.rb +4 -0
- data/spec/fixtures/rails7_users_app/app/controllers/application_controller.rb +2 -0
- data/spec/fixtures/rails7_users_app/app/controllers/concerns/.keep +0 -0
- data/spec/fixtures/rails7_users_app/app/helpers/application_helper.rb +2 -0
- data/spec/fixtures/rails7_users_app/app/javascript/application.js +3 -0
- data/spec/fixtures/rails7_users_app/app/javascript/controllers/application.js +9 -0
- data/spec/fixtures/rails7_users_app/app/javascript/controllers/hello_controller.js +7 -0
- data/spec/fixtures/rails7_users_app/app/javascript/controllers/index.js +11 -0
- data/spec/fixtures/rails7_users_app/app/jobs/application_job.rb +7 -0
- data/spec/fixtures/rails7_users_app/app/mailers/application_mailer.rb +4 -0
- data/spec/fixtures/rails7_users_app/app/models/application_record.rb +3 -0
- data/spec/fixtures/rails7_users_app/app/models/concerns/.keep +0 -0
- data/spec/fixtures/rails7_users_app/app/models/instance.rb +7 -0
- data/spec/fixtures/rails7_users_app/app/models/instructor.rb +7 -0
- data/spec/fixtures/rails7_users_app/app/views/layouts/application.html.erb +16 -0
- data/spec/fixtures/rails7_users_app/app/views/layouts/mailer.html.erb +13 -0
- data/spec/fixtures/rails7_users_app/app/views/layouts/mailer.text.erb +1 -0
- data/spec/fixtures/rails7_users_app/appmap.yml +3 -0
- data/spec/fixtures/rails7_users_app/bin/bundle +114 -0
- data/spec/fixtures/rails7_users_app/bin/importmap +4 -0
- data/spec/fixtures/rails7_users_app/bin/rails +4 -0
- data/spec/fixtures/rails7_users_app/bin/rake +4 -0
- data/spec/fixtures/rails7_users_app/bin/setup +33 -0
- data/spec/fixtures/rails7_users_app/config/application.rb +22 -0
- data/spec/fixtures/rails7_users_app/config/boot.rb +4 -0
- data/spec/fixtures/rails7_users_app/config/cable.yml +10 -0
- data/spec/fixtures/rails7_users_app/config/credentials.yml.enc +1 -0
- data/spec/fixtures/rails7_users_app/config/database.yml +86 -0
- data/spec/fixtures/rails7_users_app/config/environment.rb +5 -0
- data/spec/fixtures/rails7_users_app/config/environments/development.rb +70 -0
- data/spec/fixtures/rails7_users_app/config/environments/production.rb +93 -0
- data/spec/fixtures/rails7_users_app/config/environments/test.rb +60 -0
- data/spec/fixtures/rails7_users_app/config/importmap.rb +7 -0
- data/spec/fixtures/rails7_users_app/config/initializers/assets.rb +12 -0
- data/spec/fixtures/rails7_users_app/config/initializers/content_security_policy.rb +26 -0
- data/spec/fixtures/rails7_users_app/config/initializers/filter_parameter_logging.rb +8 -0
- data/spec/fixtures/rails7_users_app/config/initializers/inflections.rb +16 -0
- data/spec/fixtures/rails7_users_app/config/initializers/permissions_policy.rb +11 -0
- data/spec/fixtures/rails7_users_app/config/locales/en.yml +33 -0
- data/spec/fixtures/rails7_users_app/config/puma.rb +43 -0
- data/spec/fixtures/rails7_users_app/config/routes.rb +6 -0
- data/spec/fixtures/rails7_users_app/config/storage.yml +34 -0
- data/spec/fixtures/rails7_users_app/config.ru +6 -0
- data/spec/fixtures/rails7_users_app/create_app +31 -0
- data/spec/fixtures/rails7_users_app/db/migrate/20220328093141_create_instances.rb +8 -0
- data/spec/fixtures/rails7_users_app/db/migrate/20220328093154_create_instructors.rb +8 -0
- data/spec/fixtures/rails7_users_app/db/schema.rb +27 -0
- data/spec/fixtures/rails7_users_app/db/seeds.rb +7 -0
- data/spec/fixtures/rails7_users_app/docker-compose.yml +31 -0
- data/spec/fixtures/rails7_users_app/lib/assets/.keep +0 -0
- data/spec/fixtures/rails7_users_app/lib/tasks/.keep +0 -0
- data/spec/fixtures/rails7_users_app/log/.keep +0 -0
- data/spec/fixtures/rails7_users_app/public/404.html +67 -0
- data/spec/fixtures/rails7_users_app/public/422.html +67 -0
- data/spec/fixtures/rails7_users_app/public/500.html +66 -0
- data/spec/fixtures/rails7_users_app/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/fixtures/rails7_users_app/public/apple-touch-icon.png +0 -0
- data/spec/fixtures/rails7_users_app/public/favicon.ico +0 -0
- data/spec/fixtures/rails7_users_app/public/robots.txt +1 -0
- data/spec/fixtures/rails7_users_app/storage/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/application_system_test_case.rb +5 -0
- data/spec/fixtures/rails7_users_app/test/channels/application_cable/connection_test.rb +11 -0
- data/spec/fixtures/rails7_users_app/test/controllers/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/fixtures/files/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/fixtures/instances.yml +11 -0
- data/spec/fixtures/rails7_users_app/test/fixtures/instructors.yml +11 -0
- data/spec/fixtures/rails7_users_app/test/helpers/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/integration/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/mailers/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/models/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/models/instance_test.rb +6 -0
- data/spec/fixtures/rails7_users_app/test/models/instructor_test.rb +7 -0
- data/spec/fixtures/rails7_users_app/test/system/.keep +0 -0
- data/spec/fixtures/rails7_users_app/test/test_helper.rb +13 -0
- data/spec/handler/eval_spec.rb +66 -0
- data/spec/hook_spec.rb +2 -2
- data/spec/rails_recording_spec.rb +6 -4
- data/spec/rails_spec_helper.rb +7 -0
- data/spec/rails_test_spec.rb +45 -0
- metadata +95 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8dce970e998105e2e322410b204af7a0ff48790cd177225468e9b49c75651fdb
|
4
|
+
data.tar.gz: d93b8f4f0690bf5ade69b2c08e4354d3043567fc4bf6018d6e68084eece08be9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bea7fecb6bbae7feb68fa649ecba6276b3d497312ff99054c527d2f74117b584dd7119fa8e4a346b0091089786e90a0a2f2f375f1bc94a0e5437dfa8cff99e20
|
7
|
+
data.tar.gz: 913ab70d04467c584ecf5edfe4c8d60807974cf3d01eb9f2107ceaaa8677c157dc3b0e4130b6e90dcdaba3ba055b4a5268b40e94d0054eb6c41d1146205b0eff
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
# [0.78.0](https://github.com/applandinc/appmap-ruby/compare/v0.77.4...v0.78.0) (2022-04-04)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* Hook and label Kernel#eval ([e0c151d](https://github.com/applandinc/appmap-ruby/commit/e0c151d1371f5bed5597ffd0d3bfebb8bca247c2))
|
7
|
+
|
8
|
+
## [0.77.4](https://github.com/applandinc/appmap-ruby/compare/v0.77.3...v0.77.4) (2022-04-04)
|
9
|
+
|
10
|
+
|
11
|
+
### Bug Fixes
|
12
|
+
|
13
|
+
* Update Rails request handler to the new hook architecture ([595b39a](https://github.com/applandinc/appmap-ruby/commit/595b39abb030c1dcf85c83e4717c25d4c5177d4d))
|
14
|
+
|
15
|
+
## [0.77.3](https://github.com/applandinc/appmap-ruby/compare/v0.77.2...v0.77.3) (2022-03-29)
|
16
|
+
|
17
|
+
|
18
|
+
### Bug Fixes
|
19
|
+
|
20
|
+
* Rescue exceptions when calling Class#to_s ([f59f2f6](https://github.com/applandinc/appmap-ruby/commit/f59f2f6b39664ff050486c88ff1b859ca0db48d8))
|
21
|
+
|
1
22
|
## [0.77.2](https://github.com/applandinc/appmap-ruby/compare/v0.77.1...v0.77.2) (2022-03-25)
|
2
23
|
|
3
24
|
|
data/Rakefile
CHANGED
@@ -2,6 +2,21 @@ $: << File.join(__dir__, 'lib')
|
|
2
2
|
require 'appmap/version'
|
3
3
|
GEM_VERSION = AppMap::VERSION
|
4
4
|
|
5
|
+
# Make sure the local version is not behind the one on
|
6
|
+
# rubygems.org (it's ok if they're the same).
|
7
|
+
#
|
8
|
+
# If it is behind, the fixture images won't get updated with the gem
|
9
|
+
# built from the local source, so you'll wind up testing the rubygems
|
10
|
+
# version instead.
|
11
|
+
unless ENV['SKIP_VERSION_CHECK']
|
12
|
+
require 'json'
|
13
|
+
require 'net/http'
|
14
|
+
rubygems_version = JSON.parse(Net::HTTP.get(URI.parse('https://rubygems.org/api/v1/gems/appmap.json')))['version']
|
15
|
+
if Gem::Version.new(GEM_VERSION) < Gem::Version.new(rubygems_version)
|
16
|
+
raise "#{GEM_VERSION} < #{rubygems_version}. Rebase to avoid build issues."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
5
20
|
require 'rake/testtask'
|
6
21
|
require 'rdoc/task'
|
7
22
|
|
@@ -24,7 +39,7 @@ RUBY_VERSIONS=%w[2.6 2.7 3.0 3.1].select do |version|
|
|
24
39
|
|
25
40
|
false
|
26
41
|
end
|
27
|
-
FIXTURE_APPS
|
42
|
+
FIXTURE_APPS=[:rack_users_app, :rails6_users_app, :rails5_users_app, :rails7_users_app => {:ruby_version => '>= 2.7'}]
|
28
43
|
|
29
44
|
def run_cmd(*cmd)
|
30
45
|
$stderr.puts "Running: #{cmd}"
|
@@ -83,7 +98,17 @@ namespace :build do
|
|
83
98
|
RUBY_VERSIONS.each do |ruby_version|
|
84
99
|
namespace ruby_version do
|
85
100
|
desc "build:fixtures:#{ruby_version}"
|
86
|
-
FIXTURE_APPS.each do |
|
101
|
+
FIXTURE_APPS.each do |app_spec|
|
102
|
+
app = if app_spec.instance_of?(Hash)
|
103
|
+
app_spec = app_spec.flatten
|
104
|
+
version_rqt = Gem::Requirement.create(app_spec[1][:ruby_version])
|
105
|
+
next unless version_rqt =~ (Gem::Version.new(ruby_version))
|
106
|
+
app = app_spec[0]
|
107
|
+
else
|
108
|
+
app = app_spec
|
109
|
+
end.to_s
|
110
|
+
|
111
|
+
|
87
112
|
desc app
|
88
113
|
task app => ["base:#{ruby_version}"] do
|
89
114
|
build_app_image(app, ruby_version)
|
@@ -109,12 +134,13 @@ def run_specs(ruby_version, task_args)
|
|
109
134
|
# description), because it's not intended to be invoked directly
|
110
135
|
RSpec::Core::RakeTask.new("rspec_#{ruby_version}", [:specs]) do |task, args|
|
111
136
|
task.exclude_pattern = 'spec/fixtures/**/*_spec.rb'
|
137
|
+
task.rspec_opts = '-f doc'
|
112
138
|
if args.count > 0
|
113
139
|
# There doesn't appear to be a value for +pattern+ that will
|
114
140
|
# cause it to be ignored. Setting it to '' or +nil+ causes an
|
115
141
|
# empty argument to get passed to rspec, which confuses it.
|
116
142
|
task.pattern = 'never match this'
|
117
|
-
task.rspec_opts
|
143
|
+
task.rspec_opts += " " + args.to_a.join(' ')
|
118
144
|
end
|
119
145
|
end
|
120
146
|
|
data/ext/appmap/appmap.c
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
#include <ruby.h>
|
2
|
+
#include <ruby/debug.h>
|
2
3
|
#include <ruby/intern.h>
|
3
4
|
|
4
5
|
// Seems like CLASS_OR_MODULE_P should really be in a header file in
|
@@ -39,7 +40,7 @@ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
|
|
39
40
|
{
|
40
41
|
VALUE arities_key = rb_intern(ARITIES_KEY);
|
41
42
|
VALUE arities = rb_ivar_get(mod, arities_key);
|
42
|
-
|
43
|
+
|
43
44
|
if (arities == Qundef || NIL_P(arities)) {
|
44
45
|
arities = rb_hash_new();
|
45
46
|
rb_ivar_set(mod, arities_key, arities);
|
@@ -62,7 +63,7 @@ am_get_method_arity(VALUE method, VALUE orig_arity_method)
|
|
62
63
|
}
|
63
64
|
// Didn't find one, call the original method.
|
64
65
|
if (NIL_P(arity)) {
|
65
|
-
VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
|
66
|
+
VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
|
66
67
|
arity = rb_funcall(bound_method, rb_intern("call"), 0);
|
67
68
|
}
|
68
69
|
|
@@ -83,11 +84,29 @@ am_method_arity(VALUE method)
|
|
83
84
|
return am_get_method_arity(method, orig_method_arity);
|
84
85
|
}
|
85
86
|
|
87
|
+
static VALUE
|
88
|
+
bindings_callback(const rb_debug_inspector_t *dbg_context, void *level)
|
89
|
+
{
|
90
|
+
const int i = NUM2INT((VALUE) level);
|
91
|
+
if (i >= RARRAY_LEN(rb_debug_inspector_backtrace_locations(dbg_context))) {
|
92
|
+
return Qnil;
|
93
|
+
} else {
|
94
|
+
return rb_debug_inspector_frame_binding_get(dbg_context, i);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
static VALUE
|
99
|
+
am_previous_bindings(VALUE self, VALUE level)
|
100
|
+
{
|
101
|
+
return rb_debug_inspector_open(bindings_callback, (void *) level);
|
102
|
+
}
|
103
|
+
|
86
104
|
void Init_appmap() {
|
87
105
|
VALUE appmap = rb_define_module("AppMap");
|
88
106
|
am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
|
89
107
|
|
90
108
|
rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
|
109
|
+
rb_define_module_function(appmap, "caller_binding", am_previous_bindings, 1);
|
91
110
|
|
92
111
|
rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
|
93
112
|
rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
|
@@ -14,14 +14,17 @@
|
|
14
14
|
- String#unpack1
|
15
15
|
require_name: ruby
|
16
16
|
label: string.unpack
|
17
|
-
|
17
|
+
- methods:
|
18
|
+
- Kernel#eval
|
19
|
+
require_name: ruby
|
20
|
+
label: lang.eval
|
21
|
+
handler_class: AppMap::Handler::Eval
|
18
22
|
# TODO: eval does not happen in the right context, and therefore any new constants
|
19
23
|
# which are defined are placed on the wrong module/class.
|
20
|
-
# - Kernel#eval
|
21
24
|
# - Binding#eval
|
22
25
|
# - BasicObject#instance_eval
|
23
26
|
# These methods cannot be hooked as far as I can tell.
|
24
|
-
# Why? When calling one of these functions, the context at the point of
|
27
|
+
# Why? When calling one of these functions, the context at the point of
|
25
28
|
# definition is used. It's not possible to bind class_eval to a new context.
|
26
29
|
# - Module#class_eval
|
27
30
|
# - Module#module_eval
|
data/lib/appmap/config.rb
CHANGED
@@ -450,7 +450,15 @@ module AppMap
|
|
450
450
|
|
451
451
|
# Hook a method which is specified by class and method name.
|
452
452
|
def package_for_code_object
|
453
|
-
class_name =
|
453
|
+
class_name = begin
|
454
|
+
cls.to_s.index('#<Class:') == 0 ? cls.to_s['#<Class:'.length...-1] : cls.name
|
455
|
+
rescue
|
456
|
+
# Calling #to_s on some Rails classes
|
457
|
+
# (e.g. those generated to represent
|
458
|
+
# associations) will raise an exception. Fall
|
459
|
+
# back to using the class name.
|
460
|
+
cls.name
|
461
|
+
end
|
454
462
|
Array(config.gem_hooks[class_name])
|
455
463
|
.find { |hook| hook.include_method?(method.name) }
|
456
464
|
&.package
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'appmap/handler/function'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
module Handler
|
7
|
+
# Handler class for Kernel#eval.
|
8
|
+
#
|
9
|
+
# Second argument to eval is a Binding, which despite the name (and
|
10
|
+
# the accessible methods) in addition to locals and receiver also
|
11
|
+
# encapsulates the entire execution context, in particular including
|
12
|
+
# the lexical scope. This is especially important for constant lookup
|
13
|
+
# and definition.
|
14
|
+
#
|
15
|
+
# If the binding is not provided, by default eval will run in the
|
16
|
+
# current frame. Since we call it here, this will mean the #do_call
|
17
|
+
# frame, which would make AppMap::Handler::Eval the lexical scope
|
18
|
+
# for constant lookup and definition; as a consequence
|
19
|
+
# eg. `eval "class Foo; end"` would define
|
20
|
+
# AppMap::Handler::Eval::Foo instead of defining it in
|
21
|
+
# the module where the original call was made.
|
22
|
+
#
|
23
|
+
# To avoid this, we explicitly substitute the correct execution
|
24
|
+
# context, up several stack frames.
|
25
|
+
class Eval < Function
|
26
|
+
# The depth of the frame we need to pluck out:
|
27
|
+
# 1. Hook::Method#do_call
|
28
|
+
# 2. Hook::Method#trace_call
|
29
|
+
# 3. Hook::Method#call
|
30
|
+
# 4. proc generated by Hook::Method#hook_method_def
|
31
|
+
# 5. the (intended) frame of the original eval that we hooked
|
32
|
+
# Note it needs to be adjusted if this call sequence changes.
|
33
|
+
FRAME_DEPTH = 5
|
34
|
+
|
35
|
+
def do_call(receiver, src = nil, context = nil, *rest)
|
36
|
+
context ||= AppMap.caller_binding FRAME_DEPTH
|
37
|
+
hook_method.bind(receiver).call(src, context, *rest)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,18 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'appmap/event'
|
4
|
+
require 'appmap/hook/method'
|
4
5
|
|
5
6
|
module AppMap
|
6
7
|
module Handler
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
# Base handler class, will emit method call and return events.
|
9
|
+
class Function < Hook::Method
|
10
|
+
def handle_call(receiver, args)
|
11
|
+
AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
14
|
+
def handle_return(call_event_id, elapsed, return_value, exception)
|
15
|
+
AppMap::Event::MethodReturn.build_from_invocation(call_event_id, return_value, exception, elapsed: elapsed)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'appmap/event'
|
4
|
+
require 'appmap/hook/method'
|
4
5
|
require 'appmap/util'
|
5
6
|
require 'rack'
|
6
7
|
|
@@ -81,29 +82,29 @@ module AppMap
|
|
81
82
|
end
|
82
83
|
end
|
83
84
|
|
84
|
-
class
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
85
|
+
# Handler class for HTTP requests.
|
86
|
+
# Emits HTTP request events instead of method calls.
|
87
|
+
class NetHTTP < Hook::Method
|
88
|
+
def self.copy_headers(obj)
|
89
|
+
{}.tap do |headers|
|
90
|
+
obj.each_header do |key, value|
|
91
|
+
key = key.split('-').map(&:capitalize).join('-')
|
92
|
+
headers[key] = value
|
92
93
|
end
|
93
94
|
end
|
95
|
+
end
|
94
96
|
|
95
|
-
|
96
|
-
|
97
|
-
|
97
|
+
def handle_call(receiver, args)
|
98
|
+
# request will call itself again in a start block if it's not already started.
|
99
|
+
return unless receiver.started?
|
98
100
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
101
|
+
http = receiver
|
102
|
+
request = args.first
|
103
|
+
HTTPClientRequest.new(http, request)
|
104
|
+
end
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
-
end
|
106
|
+
def handle_return(call_event_id, elapsed, return_value, exception)
|
107
|
+
HTTPClientResponse.new(return_value, call_event_id, elapsed)
|
107
108
|
end
|
108
109
|
end
|
109
110
|
end
|
@@ -100,15 +100,14 @@ module AppMap
|
|
100
100
|
|
101
101
|
protected
|
102
102
|
|
103
|
-
def before_hook(receiver,
|
103
|
+
def before_hook(receiver, *)
|
104
104
|
call_event = HTTPServerRequest.new(receiver.request)
|
105
105
|
# http_server_request events are i/o and do not require a package name.
|
106
106
|
AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
|
107
|
-
|
107
|
+
call_event
|
108
108
|
end
|
109
109
|
|
110
|
-
def after_hook(receiver, call_event,
|
111
|
-
elapsed = TIME_NOW.call - start_time
|
110
|
+
def after_hook(receiver, call_event, elapsed, *)
|
112
111
|
return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
|
113
112
|
AppMap.tracing.record_event return_event
|
114
113
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'appmap/handler/function'
|
3
4
|
require 'appmap/event'
|
4
5
|
|
5
6
|
module AppMap
|
@@ -9,7 +10,7 @@ module AppMap
|
|
9
10
|
LOG = (ENV['APPMAP_TEMPLATE_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
|
10
11
|
|
11
12
|
# 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
|
+
# This duck-typed 'method' is used to represent a view template as a package,
|
13
14
|
# class, and method in the classMap.
|
14
15
|
# The class name is generated from the template path. The package name is
|
15
16
|
# 'app/views', and the method name is 'render'. The source location of the method
|
@@ -41,31 +42,31 @@ module AppMap
|
|
41
42
|
def package
|
42
43
|
'app/views'
|
43
44
|
end
|
44
|
-
|
45
|
+
|
45
46
|
def name
|
46
47
|
'render'
|
47
48
|
end
|
48
|
-
|
49
|
+
|
49
50
|
def source_location
|
50
51
|
path
|
51
52
|
end
|
52
|
-
|
53
|
+
|
53
54
|
def static
|
54
55
|
true
|
55
56
|
end
|
56
|
-
|
57
|
+
|
57
58
|
def comment
|
58
59
|
nil
|
59
60
|
end
|
60
|
-
|
61
|
+
|
61
62
|
def labels
|
62
63
|
[ 'mvc.template' ]
|
63
64
|
end
|
64
65
|
end
|
65
|
-
|
66
|
+
|
66
67
|
# TemplateCall is a type of function call which is specialized to view template rendering. Since
|
67
68
|
# there isn't really a perfect method in Rails to hook, this one is synthesized from the available
|
68
|
-
# information.
|
69
|
+
# information.
|
69
70
|
class TemplateCall < AppMap::Event::MethodEvent
|
70
71
|
# This is basically the +self+ parameter.
|
71
72
|
attr_reader :render_instance
|
@@ -75,19 +76,19 @@ module AppMap
|
|
75
76
|
attr_accessor :ready
|
76
77
|
|
77
78
|
alias ready? ready
|
78
|
-
|
79
|
+
|
79
80
|
def initialize(render_instance)
|
80
81
|
super :call
|
81
|
-
|
82
|
+
|
82
83
|
AppMap::Event::MethodEvent.build_from_invocation(:call, event: self)
|
83
84
|
@ready = false
|
84
85
|
@render_instance = render_instance
|
85
86
|
end
|
86
|
-
|
87
|
+
|
87
88
|
def static?
|
88
89
|
true
|
89
90
|
end
|
90
|
-
|
91
|
+
|
91
92
|
def to_h
|
92
93
|
super.tap do |h|
|
93
94
|
h[:defined_class] = path ? path.parameterize.underscore : 'inline_template'
|
@@ -103,71 +104,76 @@ module AppMap
|
|
103
104
|
end.compact
|
104
105
|
end
|
105
106
|
end
|
106
|
-
|
107
|
+
|
107
108
|
TEMPLATE_RENDERER = 'appmap.handler.rails.template.renderer'
|
108
109
|
|
109
110
|
# Hooks the ActionView::Resolver methods +find_all+, +find_all_anywhere+. The resolver is used
|
110
111
|
# during template rendering to lookup the template file path from parameters such as the
|
111
112
|
# template name, prefix, and partial (boolean).
|
112
|
-
class ResolverHandler
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
name, prefix, partial = args
|
117
|
-
warn "Resolver: #{{ name: name, prefix: prefix, partial: partial }}" if LOG
|
118
|
-
|
119
|
-
AppMap::Handler::Function.handle_call(defined_class, hook_method, receiver, args)
|
120
|
-
end
|
113
|
+
class ResolverHandler < AppMap::Handler::Function
|
114
|
+
def handle_call(receiver, args)
|
115
|
+
name, prefix, partial = args
|
116
|
+
warn "Resolver: #{{ name: name, prefix: prefix, partial: partial }}" if LOG
|
121
117
|
|
122
|
-
|
123
|
-
|
124
|
-
# template will be recorded in the classMap.
|
125
|
-
def handle_return(call_event_id, elapsed, return_value, exception)
|
126
|
-
renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
|
127
|
-
path_obj = Array(return_value).first
|
128
|
-
|
129
|
-
warn "Resolver return: #{path_obj}" if LOG
|
130
|
-
|
131
|
-
if path_obj
|
132
|
-
path = if path_obj.respond_to?(:identifier) && path_obj.inspect.index('#<')
|
133
|
-
path_obj.identifier
|
134
|
-
else
|
135
|
-
path_obj.inspect
|
136
|
-
end
|
137
|
-
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
|
138
|
-
AppMap.tracing.record_method(TemplateMethod.new(path))
|
139
|
-
renderer.path ||= path if renderer
|
140
|
-
end
|
118
|
+
super
|
119
|
+
end
|
141
120
|
|
142
|
-
|
143
|
-
|
121
|
+
# When the resolver returns, look to see if there is template rendering underway.
|
122
|
+
# If so, populate the template path. In all cases, add a TemplateMethod so that the
|
123
|
+
# template will be recorded in the classMap.
|
124
|
+
def handle_return(call_event_id, elapsed, return_value, exception)
|
125
|
+
renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
|
126
|
+
path_obj = Array(return_value).first
|
127
|
+
|
128
|
+
warn "Resolver return: #{path_obj}" if LOG
|
129
|
+
|
130
|
+
record_template_path renderer, path_obj
|
131
|
+
|
132
|
+
super
|
133
|
+
end
|
134
|
+
|
135
|
+
def record_template_path(renderer, path_obj)
|
136
|
+
return unless path_obj
|
137
|
+
|
138
|
+
path = path_from_obj path_obj
|
139
|
+
AppMap.tracing.record_method(TemplateMethod.new(path))
|
140
|
+
renderer.path ||= path if renderer
|
141
|
+
end
|
142
|
+
|
143
|
+
def path_from_obj(path_obj)
|
144
|
+
path =
|
145
|
+
if path_obj.respond_to?(:identifier) && path_obj.inspect.index('#<')
|
146
|
+
path_obj.identifier
|
147
|
+
else
|
148
|
+
path_obj.inspect
|
149
|
+
end
|
150
|
+
path = path[Dir.pwd.length + 1..] if path.index(Dir.pwd) == 0
|
151
|
+
path
|
144
152
|
end
|
145
153
|
end
|
146
154
|
|
147
155
|
# Hooks the ActionView::Renderer method +render+. This method is used by Rails to perform
|
148
156
|
# template rendering. The TemplateCall event which is emitted by this handler has a
|
149
|
-
# +path+ parameter, which is nil until it's filled in by a ResolverHandler.
|
150
|
-
class RenderHandler
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
warn "Renderer: #{options}" if LOG
|
157
|
-
|
158
|
-
TemplateCall.new(receiver).tap do |call|
|
159
|
-
Thread.current[TEMPLATE_RENDERER] ||= []
|
160
|
-
Thread.current[TEMPLATE_RENDERER] << call
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def handle_return(call_event_id, elapsed, return_value, exception)
|
165
|
-
template_call = Array(Thread.current[TEMPLATE_RENDERER]).pop
|
166
|
-
template_call.ready = true
|
157
|
+
# +path+ parameter, which is nil until it's filled in by a ResolverHandler.
|
158
|
+
class RenderHandler < AppMap::Hook::Method
|
159
|
+
def handle_call(receiver, args)
|
160
|
+
# context, options
|
161
|
+
_, options = args
|
162
|
+
|
163
|
+
warn "Renderer: #{options}" if LOG
|
167
164
|
|
168
|
-
|
165
|
+
TemplateCall.new(receiver).tap do |call|
|
166
|
+
Thread.current[TEMPLATE_RENDERER] ||= []
|
167
|
+
Thread.current[TEMPLATE_RENDERER] << call
|
169
168
|
end
|
170
169
|
end
|
170
|
+
|
171
|
+
def handle_return(call_event_id, elapsed, _return_value, _exception)
|
172
|
+
template_call = Array(Thread.current[TEMPLATE_RENDERER]).pop
|
173
|
+
template_call.ready = true
|
174
|
+
|
175
|
+
AppMap::Event::MethodReturnIgnoreValue.build_from_invocation(call_event_id, elapsed: elapsed)
|
176
|
+
end
|
171
177
|
end
|
172
178
|
end
|
173
179
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
def ruby2_keywords(*); end unless respond_to?(:ruby2_keywords, true)
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
class Hook
|
7
|
+
# Delegation methods for Ruby 2.
|
8
|
+
# cf. https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html
|
9
|
+
class Method
|
10
|
+
ruby2_keywords def call(receiver, *args, &block)
|
11
|
+
return do_call(receiver, *args, &block) unless trace?
|
12
|
+
|
13
|
+
call_event = with_disabled_hook { before_hook receiver, *args }
|
14
|
+
trace_call call_event, receiver, *args, &block
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def before_hook(receiver, *args)
|
20
|
+
call_event = handle_call(receiver, args)
|
21
|
+
if call_event
|
22
|
+
AppMap.tracing.record_event \
|
23
|
+
call_event,
|
24
|
+
package: hook_package,
|
25
|
+
defined_class: defined_class,
|
26
|
+
method: hook_method
|
27
|
+
end
|
28
|
+
call_event
|
29
|
+
end
|
30
|
+
|
31
|
+
ruby2_keywords def do_call(receiver, *args, &block)
|
32
|
+
hook_method.bind(receiver).call(*args, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
ruby2_keywords def trace_call(call_event, receiver, *args, &block)
|
36
|
+
start_time = gettime
|
37
|
+
begin
|
38
|
+
return_value = do_call(receiver, *args, &block)
|
39
|
+
rescue # rubocop:disable Style/RescueStandardError
|
40
|
+
exception = $ERROR_INFO
|
41
|
+
raise
|
42
|
+
ensure
|
43
|
+
with_disabled_hook { after_hook receiver, call_event, gettime - start_time, return_value, exception } \
|
44
|
+
if call_event
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def hook_method_def
|
49
|
+
this = self
|
50
|
+
proc { |*args, &block| this.call self, *args, &block }.tap do |hook|
|
51
|
+
hook.ruby2_keywords if hook.respond_to? :ruby2_keywords
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|