appmap 0.77.2 → 0.78.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.travis.yml +0 -7
  4. data/CHANGELOG.md +21 -0
  5. data/Rakefile +29 -3
  6. data/ext/appmap/appmap.c +21 -2
  7. data/lib/appmap/builtin_hooks/ruby.yml +6 -3
  8. data/lib/appmap/config.rb +9 -1
  9. data/lib/appmap/handler/eval.rb +41 -0
  10. data/lib/appmap/handler/function.rb +8 -8
  11. data/lib/appmap/handler/net_http.rb +19 -18
  12. data/lib/appmap/handler/rails/request_handler.rb +3 -4
  13. data/lib/appmap/handler/rails/template.rb +68 -62
  14. data/lib/appmap/hook/method/ruby2.rb +56 -0
  15. data/lib/appmap/hook/method/ruby3.rb +56 -0
  16. data/lib/appmap/hook/method.rb +42 -98
  17. data/lib/appmap/hook.rb +2 -2
  18. data/lib/appmap/version.rb +1 -1
  19. data/spec/fixtures/rails7_users_app/.dockerignore +1 -0
  20. data/spec/fixtures/rails7_users_app/.gitattributes +7 -0
  21. data/spec/fixtures/rails7_users_app/.gitignore +31 -0
  22. data/spec/fixtures/rails7_users_app/.rspec +1 -0
  23. data/spec/fixtures/rails7_users_app/.ruby-version +1 -0
  24. data/spec/fixtures/rails7_users_app/Dockerfile +30 -0
  25. data/spec/fixtures/rails7_users_app/Dockerfile.pg +3 -0
  26. data/spec/fixtures/rails7_users_app/Gemfile +99 -0
  27. data/spec/fixtures/rails7_users_app/README.md +24 -0
  28. data/spec/fixtures/rails7_users_app/Rakefile +6 -0
  29. data/spec/fixtures/rails7_users_app/app/assets/config/manifest.js +4 -0
  30. data/spec/fixtures/rails7_users_app/app/assets/images/.keep +0 -0
  31. data/spec/fixtures/rails7_users_app/app/assets/stylesheets/application.css +15 -0
  32. data/spec/fixtures/rails7_users_app/app/channels/application_cable/channel.rb +4 -0
  33. data/spec/fixtures/rails7_users_app/app/channels/application_cable/connection.rb +4 -0
  34. data/spec/fixtures/rails7_users_app/app/controllers/application_controller.rb +2 -0
  35. data/spec/fixtures/rails7_users_app/app/controllers/concerns/.keep +0 -0
  36. data/spec/fixtures/rails7_users_app/app/helpers/application_helper.rb +2 -0
  37. data/spec/fixtures/rails7_users_app/app/javascript/application.js +3 -0
  38. data/spec/fixtures/rails7_users_app/app/javascript/controllers/application.js +9 -0
  39. data/spec/fixtures/rails7_users_app/app/javascript/controllers/hello_controller.js +7 -0
  40. data/spec/fixtures/rails7_users_app/app/javascript/controllers/index.js +11 -0
  41. data/spec/fixtures/rails7_users_app/app/jobs/application_job.rb +7 -0
  42. data/spec/fixtures/rails7_users_app/app/mailers/application_mailer.rb +4 -0
  43. data/spec/fixtures/rails7_users_app/app/models/application_record.rb +3 -0
  44. data/spec/fixtures/rails7_users_app/app/models/concerns/.keep +0 -0
  45. data/spec/fixtures/rails7_users_app/app/models/instance.rb +7 -0
  46. data/spec/fixtures/rails7_users_app/app/models/instructor.rb +7 -0
  47. data/spec/fixtures/rails7_users_app/app/views/layouts/application.html.erb +16 -0
  48. data/spec/fixtures/rails7_users_app/app/views/layouts/mailer.html.erb +13 -0
  49. data/spec/fixtures/rails7_users_app/app/views/layouts/mailer.text.erb +1 -0
  50. data/spec/fixtures/rails7_users_app/appmap.yml +3 -0
  51. data/spec/fixtures/rails7_users_app/bin/bundle +114 -0
  52. data/spec/fixtures/rails7_users_app/bin/importmap +4 -0
  53. data/spec/fixtures/rails7_users_app/bin/rails +4 -0
  54. data/spec/fixtures/rails7_users_app/bin/rake +4 -0
  55. data/spec/fixtures/rails7_users_app/bin/setup +33 -0
  56. data/spec/fixtures/rails7_users_app/config/application.rb +22 -0
  57. data/spec/fixtures/rails7_users_app/config/boot.rb +4 -0
  58. data/spec/fixtures/rails7_users_app/config/cable.yml +10 -0
  59. data/spec/fixtures/rails7_users_app/config/credentials.yml.enc +1 -0
  60. data/spec/fixtures/rails7_users_app/config/database.yml +86 -0
  61. data/spec/fixtures/rails7_users_app/config/environment.rb +5 -0
  62. data/spec/fixtures/rails7_users_app/config/environments/development.rb +70 -0
  63. data/spec/fixtures/rails7_users_app/config/environments/production.rb +93 -0
  64. data/spec/fixtures/rails7_users_app/config/environments/test.rb +60 -0
  65. data/spec/fixtures/rails7_users_app/config/importmap.rb +7 -0
  66. data/spec/fixtures/rails7_users_app/config/initializers/assets.rb +12 -0
  67. data/spec/fixtures/rails7_users_app/config/initializers/content_security_policy.rb +26 -0
  68. data/spec/fixtures/rails7_users_app/config/initializers/filter_parameter_logging.rb +8 -0
  69. data/spec/fixtures/rails7_users_app/config/initializers/inflections.rb +16 -0
  70. data/spec/fixtures/rails7_users_app/config/initializers/permissions_policy.rb +11 -0
  71. data/spec/fixtures/rails7_users_app/config/locales/en.yml +33 -0
  72. data/spec/fixtures/rails7_users_app/config/puma.rb +43 -0
  73. data/spec/fixtures/rails7_users_app/config/routes.rb +6 -0
  74. data/spec/fixtures/rails7_users_app/config/storage.yml +34 -0
  75. data/spec/fixtures/rails7_users_app/config.ru +6 -0
  76. data/spec/fixtures/rails7_users_app/create_app +31 -0
  77. data/spec/fixtures/rails7_users_app/db/migrate/20220328093141_create_instances.rb +8 -0
  78. data/spec/fixtures/rails7_users_app/db/migrate/20220328093154_create_instructors.rb +8 -0
  79. data/spec/fixtures/rails7_users_app/db/schema.rb +27 -0
  80. data/spec/fixtures/rails7_users_app/db/seeds.rb +7 -0
  81. data/spec/fixtures/rails7_users_app/docker-compose.yml +31 -0
  82. data/spec/fixtures/rails7_users_app/lib/assets/.keep +0 -0
  83. data/spec/fixtures/rails7_users_app/lib/tasks/.keep +0 -0
  84. data/spec/fixtures/rails7_users_app/log/.keep +0 -0
  85. data/spec/fixtures/rails7_users_app/public/404.html +67 -0
  86. data/spec/fixtures/rails7_users_app/public/422.html +67 -0
  87. data/spec/fixtures/rails7_users_app/public/500.html +66 -0
  88. data/spec/fixtures/rails7_users_app/public/apple-touch-icon-precomposed.png +0 -0
  89. data/spec/fixtures/rails7_users_app/public/apple-touch-icon.png +0 -0
  90. data/spec/fixtures/rails7_users_app/public/favicon.ico +0 -0
  91. data/spec/fixtures/rails7_users_app/public/robots.txt +1 -0
  92. data/spec/fixtures/rails7_users_app/storage/.keep +0 -0
  93. data/spec/fixtures/rails7_users_app/test/application_system_test_case.rb +5 -0
  94. data/spec/fixtures/rails7_users_app/test/channels/application_cable/connection_test.rb +11 -0
  95. data/spec/fixtures/rails7_users_app/test/controllers/.keep +0 -0
  96. data/spec/fixtures/rails7_users_app/test/fixtures/files/.keep +0 -0
  97. data/spec/fixtures/rails7_users_app/test/fixtures/instances.yml +11 -0
  98. data/spec/fixtures/rails7_users_app/test/fixtures/instructors.yml +11 -0
  99. data/spec/fixtures/rails7_users_app/test/helpers/.keep +0 -0
  100. data/spec/fixtures/rails7_users_app/test/integration/.keep +0 -0
  101. data/spec/fixtures/rails7_users_app/test/mailers/.keep +0 -0
  102. data/spec/fixtures/rails7_users_app/test/models/.keep +0 -0
  103. data/spec/fixtures/rails7_users_app/test/models/instance_test.rb +6 -0
  104. data/spec/fixtures/rails7_users_app/test/models/instructor_test.rb +7 -0
  105. data/spec/fixtures/rails7_users_app/test/system/.keep +0 -0
  106. data/spec/fixtures/rails7_users_app/test/test_helper.rb +13 -0
  107. data/spec/handler/eval_spec.rb +66 -0
  108. data/spec/hook_spec.rb +2 -2
  109. data/spec/rails_recording_spec.rb +6 -4
  110. data/spec/rails_spec_helper.rb +7 -0
  111. data/spec/rails_test_spec.rb +45 -0
  112. metadata +95 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c287b504d552b0f627f6343e61f42835b144d12f3dea4e9f8a6e33bd72dbc581
4
- data.tar.gz: b00990f93c7961487a0c4ea6bc1f0828c3efa391578100de41600cc72ee1727d
3
+ metadata.gz: 8dce970e998105e2e322410b204af7a0ff48790cd177225468e9b49c75651fdb
4
+ data.tar.gz: d93b8f4f0690bf5ade69b2c08e4354d3043567fc4bf6018d6e68084eece08be9
5
5
  SHA512:
6
- metadata.gz: 0245f3bb4e483b6fba7326d759af828022e4339e05a293144e628c7fae558d744b105a0d3be8f44caa106f9b7b056fb3886176f3cdd513fe400c18126c4b7488
7
- data.tar.gz: 84cf727259f8ec5bad21b9d2ce799bc0753aeaeca0047fcf4296a0e5782d392a28340d44af0c08ab5d6a44fddb7baea4ea784ddd65e942329f159cf9eb4eac5b
6
+ metadata.gz: bea7fecb6bbae7feb68fa649ecba6276b3d497312ff99054c527d2f74117b584dd7119fa8e4a346b0091089786e90a0a2f2f375f1bc94a0e5437dfa8cff99e20
7
+ data.tar.gz: 913ab70d04467c584ecf5edfe4c8d60807974cf3d01eb9f2107ceaaa8677c157dc3b0e4130b6e90dcdaba3ba055b4a5268b40e94d0054eb6c41d1146205b0eff
data/.rubocop.yml CHANGED
@@ -19,6 +19,7 @@ Layout/LineLength:
19
19
 
20
20
  Metrics/BlockLength:
21
21
  ExcludedMethods:
22
+ - describe
22
23
  - it
23
24
  - context
24
25
 
data/.travis.yml CHANGED
@@ -33,13 +33,6 @@ before_install:
33
33
  && nvm use --lts \
34
34
  && npm i -g yarn
35
35
 
36
- jobs:
37
- include:
38
- - stage: test
39
- env:
40
- # GEM_ALTERNATIVE_NAME only needed for deployment
41
- - GEM_ALTERNATIVE_NAME=''
42
-
43
36
  before_deploy:
44
37
  - |
45
38
  npm i -g \
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=%w[rack_users_app rails6_users_app rails5_users_app]
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 |app|
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 = args.to_a.join(' ')
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
- #- methods:
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 = cls.to_s.index('#<Class:') == 0 ? cls.to_s['#<Class:'.length...-1] : cls.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
- 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
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
- 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
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 NetHTTP
85
- class << self
86
- def copy_headers(obj)
87
- {}.tap do |headers|
88
- obj.each_header do |key, value|
89
- key = key.split('-').map(&:capitalize).join('-')
90
- headers[key] = value
91
- end
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
- def handle_call(defined_class, hook_method, receiver, args)
96
- # request will call itself again in a start block if it's not already started.
97
- return unless receiver.started?
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
- http = receiver
100
- request = args.first
101
- HTTPClientRequest.new(http, request)
102
- end
101
+ http = receiver
102
+ request = args.first
103
+ HTTPClientRequest.new(http, request)
104
+ end
103
105
 
104
- def handle_return(call_event_id, elapsed, return_value, exception)
105
- HTTPClientResponse.new(return_value, call_event_id, elapsed)
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, defined_class, _) # args
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
- [ call_event, TIME_NOW.call ]
107
+ call_event
108
108
  end
109
109
 
110
- def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
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
- class << self
114
- # Handled as a normal function call.
115
- def handle_call(defined_class, hook_method, receiver, args)
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
- # When the resolver returns, look to see if there is template rendering underway.
123
- # If so, populate the template path. In all cases, add a TemplateMethod so that the
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
- AppMap::Handler::Function.handle_return(call_event_id, elapsed, return_value, exception)
143
- end
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
- class << self
152
- def handle_call(defined_class, hook_method, receiver, args)
153
- # context, options
154
- _, options = args
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
- AppMap::Event::MethodReturnIgnoreValue.build_from_invocation(call_event_id, elapsed: elapsed)
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