appmap 0.37.2 → 0.41.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/.travis.yml +2 -23
  4. data/CHANGELOG.md +23 -0
  5. data/CONTRIBUTING.md +22 -0
  6. data/README.md +102 -54
  7. data/Rakefile +3 -3
  8. data/lib/appmap/class_map.rb +25 -8
  9. data/lib/appmap/config.rb +54 -26
  10. data/lib/appmap/hook.rb +18 -3
  11. data/lib/appmap/hook/method.rb +18 -12
  12. data/lib/appmap/rails/request_handler.rb +17 -3
  13. data/lib/appmap/railtie.rb +1 -5
  14. data/lib/appmap/trace.rb +18 -7
  15. data/lib/appmap/version.rb +2 -2
  16. data/spec/abstract_controller_base_spec.rb +125 -64
  17. data/spec/config_spec.rb +1 -0
  18. data/spec/fixtures/hook/exclude.rb +15 -0
  19. data/spec/fixtures/hook/labels.rb +6 -0
  20. data/spec/fixtures/rails5_users_app/Gemfile +3 -4
  21. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +8 -0
  22. data/spec/fixtures/rails5_users_app/appmap.yml +5 -1
  23. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  24. data/spec/fixtures/rails5_users_app/config/routes.rb +1 -1
  25. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +1 -1
  26. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +27 -0
  27. data/spec/fixtures/rails6_users_app/Gemfile +3 -4
  28. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +8 -0
  29. data/spec/fixtures/rails6_users_app/appmap.yml +6 -1
  30. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  31. data/spec/fixtures/rails6_users_app/config/routes.rb +1 -1
  32. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +1 -1
  33. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +27 -0
  34. data/spec/hook_spec.rb +69 -47
  35. data/spec/rails_spec_helper.rb +2 -2
  36. data/spec/record_sql_rails_pg_spec.rb +1 -1
  37. data/spec/rspec_feature_metadata_spec.rb +1 -1
  38. data/spec/spec_helper.rb +1 -0
  39. metadata +7 -68
  40. data/spec/abstract_controller4_base_spec.rb +0 -66
  41. data/spec/fixtures/rails4_users_app/.gitignore +0 -13
  42. data/spec/fixtures/rails4_users_app/.rbenv-gemsets +0 -2
  43. data/spec/fixtures/rails4_users_app/.ruby-version +0 -1
  44. data/spec/fixtures/rails4_users_app/Dockerfile +0 -30
  45. data/spec/fixtures/rails4_users_app/Dockerfile.pg +0 -3
  46. data/spec/fixtures/rails4_users_app/Gemfile +0 -77
  47. data/spec/fixtures/rails4_users_app/README.rdoc +0 -28
  48. data/spec/fixtures/rails4_users_app/Rakefile +0 -6
  49. data/spec/fixtures/rails4_users_app/app/assets/images/.keep +0 -0
  50. data/spec/fixtures/rails4_users_app/app/assets/javascripts/application.js +0 -16
  51. data/spec/fixtures/rails4_users_app/app/assets/stylesheets/application.css +0 -15
  52. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +0 -27
  53. data/spec/fixtures/rails4_users_app/app/controllers/application_controller.rb +0 -5
  54. data/spec/fixtures/rails4_users_app/app/controllers/concerns/.keep +0 -0
  55. data/spec/fixtures/rails4_users_app/app/controllers/health_controller.rb +0 -5
  56. data/spec/fixtures/rails4_users_app/app/controllers/users_controller.rb +0 -5
  57. data/spec/fixtures/rails4_users_app/app/helpers/application_helper.rb +0 -2
  58. data/spec/fixtures/rails4_users_app/app/mailers/.keep +0 -0
  59. data/spec/fixtures/rails4_users_app/app/models/.keep +0 -0
  60. data/spec/fixtures/rails4_users_app/app/models/concerns/.keep +0 -0
  61. data/spec/fixtures/rails4_users_app/app/models/user.rb +0 -18
  62. data/spec/fixtures/rails4_users_app/app/views/layouts/application.html.haml +0 -7
  63. data/spec/fixtures/rails4_users_app/app/views/users/index.html.haml +0 -7
  64. data/spec/fixtures/rails4_users_app/appmap.yml +0 -3
  65. data/spec/fixtures/rails4_users_app/bin/rails +0 -9
  66. data/spec/fixtures/rails4_users_app/bin/setup +0 -29
  67. data/spec/fixtures/rails4_users_app/bin/spring +0 -17
  68. data/spec/fixtures/rails4_users_app/config.ru +0 -4
  69. data/spec/fixtures/rails4_users_app/config/application.rb +0 -26
  70. data/spec/fixtures/rails4_users_app/config/boot.rb +0 -3
  71. data/spec/fixtures/rails4_users_app/config/database.yml +0 -18
  72. data/spec/fixtures/rails4_users_app/config/environment.rb +0 -5
  73. data/spec/fixtures/rails4_users_app/config/environments/development.rb +0 -41
  74. data/spec/fixtures/rails4_users_app/config/environments/production.rb +0 -79
  75. data/spec/fixtures/rails4_users_app/config/environments/test.rb +0 -42
  76. data/spec/fixtures/rails4_users_app/config/initializers/assets.rb +0 -11
  77. data/spec/fixtures/rails4_users_app/config/initializers/backtrace_silencers.rb +0 -7
  78. data/spec/fixtures/rails4_users_app/config/initializers/cookies_serializer.rb +0 -3
  79. data/spec/fixtures/rails4_users_app/config/initializers/filter_parameter_logging.rb +0 -4
  80. data/spec/fixtures/rails4_users_app/config/initializers/inflections.rb +0 -16
  81. data/spec/fixtures/rails4_users_app/config/initializers/mime_types.rb +0 -4
  82. data/spec/fixtures/rails4_users_app/config/initializers/session_store.rb +0 -3
  83. data/spec/fixtures/rails4_users_app/config/initializers/to_time_preserves_timezone.rb +0 -10
  84. data/spec/fixtures/rails4_users_app/config/initializers/wrap_parameters.rb +0 -14
  85. data/spec/fixtures/rails4_users_app/config/locales/en.yml +0 -23
  86. data/spec/fixtures/rails4_users_app/config/routes.rb +0 -12
  87. data/spec/fixtures/rails4_users_app/config/secrets.yml +0 -22
  88. data/spec/fixtures/rails4_users_app/create_app +0 -23
  89. data/spec/fixtures/rails4_users_app/db/migrate/20191127112304_create_users.rb +0 -10
  90. data/spec/fixtures/rails4_users_app/db/schema.rb +0 -26
  91. data/spec/fixtures/rails4_users_app/db/seeds.rb +0 -7
  92. data/spec/fixtures/rails4_users_app/docker-compose.yml +0 -26
  93. data/spec/fixtures/rails4_users_app/lib/assets/.keep +0 -0
  94. data/spec/fixtures/rails4_users_app/lib/tasks/.keep +0 -0
  95. data/spec/fixtures/rails4_users_app/log/.keep +0 -0
  96. data/spec/fixtures/rails4_users_app/public/404.html +0 -67
  97. data/spec/fixtures/rails4_users_app/public/422.html +0 -67
  98. data/spec/fixtures/rails4_users_app/public/500.html +0 -66
  99. data/spec/fixtures/rails4_users_app/public/favicon.ico +0 -0
  100. data/spec/fixtures/rails4_users_app/public/robots.txt +0 -5
  101. data/spec/fixtures/rails4_users_app/spec/controllers/users_controller_api_spec.rb +0 -49
  102. data/spec/fixtures/rails4_users_app/spec/rails_helper.rb +0 -95
  103. data/spec/fixtures/rails4_users_app/spec/spec_helper.rb +0 -96
  104. data/spec/fixtures/rails4_users_app/test/fixtures/users.yml +0 -9
  105. data/spec/record_sql_rails4_pg_spec.rb +0 -75
data/lib/appmap/config.rb CHANGED
@@ -2,15 +2,22 @@
2
2
 
3
3
  module AppMap
4
4
  class Config
5
- Package = Struct.new(:path, :gem, :package_name, :exclude, :labels) do
5
+ Package = Struct.new(:path, :gem, :package_name, :exclude, :labels, :shallow) do
6
+ # Indicates that only the entry points to a package will be recorded.
7
+ # Once the code has entered a package, subsequent calls within the package will not be
8
+ # recorded unless the code leaves the package and re-enters it.
9
+ def shallow?
10
+ shallow
11
+ end
12
+
6
13
  class << self
7
- def build_from_path(path, package_name: nil, exclude: [], labels: [])
8
- Package.new(path, nil, package_name, exclude, labels)
14
+ def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
15
+ Package.new(path, nil, package_name, exclude, labels, shallow)
9
16
  end
10
17
 
11
- def build_from_gem(gem, package_name: nil, exclude: [], labels: [])
18
+ def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
12
19
  gem_paths(gem).map do |gem_path|
13
- Package.new(gem_path, gem, package_name, exclude, labels)
20
+ Package.new(gem_path, gem, package_name, exclude, labels, shallow)
14
21
  end
15
22
  end
16
23
 
@@ -36,7 +43,8 @@ module AppMap
36
43
  package_name: package_name,
37
44
  gem: gem,
38
45
  exclude: exclude.blank? ? nil : exclude,
39
- labels: labels.blank? ? nil : labels
46
+ labels: labels.blank? ? nil : labels,
47
+ shallow: shallow
40
48
  }.compact
41
49
  end
42
50
  end
@@ -49,31 +57,32 @@ module AppMap
49
57
  # Methods that should always be hooked, with their containing
50
58
  # package and labels that should be applied to them.
51
59
  HOOKED_METHODS = {
52
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[security crypto]))
60
+ 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[provider.secure_compare])),
61
+ 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', package_name: 'action_view', labels: %w[mvc.view]))
53
62
  }.freeze
54
63
 
55
64
  BUILTIN_METHODS = {
56
65
  'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
57
- 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGES),
58
66
  'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
59
67
  'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
60
68
  'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
61
69
  'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
62
- 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[http io])),
63
- 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
64
- 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
65
- 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
66
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[serialization marshal])),
67
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[serialization yaml])),
68
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json])),
69
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json]))
70
+ 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http io])),
71
+ 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.smtp protocol.email io])),
72
+ 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.pop protocol.email io])),
73
+ 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.imap protocol.email io])),
74
+ 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization marshal])),
75
+ 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml provider.serialization])),
76
+ 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
77
+ 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
70
78
  }.freeze
71
79
 
72
- attr_reader :name, :packages
80
+ attr_reader :name, :packages, :exclude
73
81
 
74
- def initialize(name, packages = [])
82
+ def initialize(name, packages = [], exclude = [])
75
83
  @name = name
76
84
  @packages = packages
85
+ @exclude = exclude
77
86
  end
78
87
 
79
88
  class << self
@@ -91,27 +100,38 @@ module AppMap
91
100
  raise 'AppMap package configuration should specify gem or path, not both' if gem && path
92
101
 
93
102
  if gem
94
- Package.build_from_gem(gem, exclude: package['exclude'] || [])
103
+ shallow = package['shallow']
104
+ # shallow is true by default for gems
105
+ shallow = true if shallow.nil?
106
+ Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
95
107
  else
96
- [ Package.build_from_path(path, exclude: package['exclude'] || []) ]
108
+ [ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow']) ]
97
109
  end
98
110
  end.flatten
99
- Config.new config_data['name'], packages
111
+ Config.new config_data['name'], packages, config_data['exclude'] || []
100
112
  end
101
113
  end
102
114
 
103
115
  def to_h
104
116
  {
105
117
  name: name,
106
- packages: packages.map(&:to_h)
118
+ packages: packages.map(&:to_h),
119
+ exclude: exclude
107
120
  }
108
121
  end
109
122
 
123
+ # package_for_method finds the Package, if any, which configures the hook
124
+ # for a method.
110
125
  def package_for_method(method)
126
+ package_hooked_by_class(method) || package_hooked_by_source_location(method)
127
+ end
128
+
129
+ def package_hooked_by_class(method)
111
130
  defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
112
- package = find_package(defined_class, method_name)
113
- return package if package
131
+ return find_package(defined_class, method_name)
132
+ end
114
133
 
134
+ def package_hooked_by_source_location(method)
115
135
  location = method.source_location
116
136
  location_file, = location
117
137
  return unless location_file
@@ -123,14 +143,22 @@ module AppMap
123
143
  end
124
144
  end
125
145
 
126
- def included_by_location?(method)
127
- !!package_for_method(method)
146
+ def never_hook?(method)
147
+ defined_class, separator, method_name = ::AppMap::Hook.qualify_method_name(method)
148
+ return true if exclude.member?(defined_class) || exclude.member?([ defined_class, separator, method_name ].join)
128
149
  end
129
150
 
151
+ # always_hook? indicates a method that should always be hooked.
130
152
  def always_hook?(defined_class, method_name)
131
153
  !!find_package(defined_class, method_name)
132
154
  end
133
155
 
156
+ # included_by_location? indicates a method whose source location matches a method definition that has been
157
+ # configured for inclusion.
158
+ def included_by_location?(method)
159
+ !!package_for_method(method)
160
+ end
161
+
134
162
  def find_package(defined_class, method_name)
135
163
  hook = find_hook(defined_class)
136
164
  return nil unless hook
data/lib/appmap/hook.rb CHANGED
@@ -6,6 +6,9 @@ module AppMap
6
6
  class Hook
7
7
  LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
8
 
9
+ OBJECT_INSTANCE_METHODS = %i[! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods taint tainted? tap then to_enum to_s to_h to_a trust untaint untrust untrusted? yield_self].freeze
10
+ OBJECT_STATIC_METHODS = %i[! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr attr_accessor attr_reader attr_writer autoload autoload? class class_eval class_exec class_variable_defined? class_variable_get class_variable_set class_variables clone const_defined? const_get const_missing const_set constants define_method define_singleton_method deprecate_constant display dup enum_for eql? equal? extend freeze frozen? hash include include? included_modules inspect instance_eval instance_exec instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method method_defined? methods module_eval module_exec name new nil? object_id prepend private_class_method private_constant private_instance_methods private_method_defined? private_methods protected_instance_methods protected_method_defined? protected_methods public_class_method public_constant public_instance_method public_instance_methods public_method public_method_defined? public_methods public_send remove_class_variable remove_instance_variable remove_method respond_to? send singleton_class singleton_class? singleton_method singleton_methods superclass taint tainted? tap then to_enum to_s trust undef_method untaint untrust untrusted? yield_self].freeze
11
+
9
12
  @unbound_method_arity = ::UnboundMethod.instance_method(:arity)
10
13
  @method_arity = ::Method.instance_method(:arity)
11
14
 
@@ -42,12 +45,17 @@ module AppMap
42
45
  tp = TracePoint.new(:end) do |trace_point|
43
46
  cls = trace_point.self
44
47
 
45
- instance_methods = cls.public_instance_methods(false)
46
- class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
48
+ instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
49
+ class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
47
50
 
48
51
  hook = lambda do |hook_cls|
49
52
  lambda do |method_id|
50
- method = hook_cls.public_instance_method(method_id)
53
+ method = begin
54
+ hook_cls.public_instance_method(method_id)
55
+ rescue NameError
56
+ warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
57
+ return
58
+ end
51
59
 
52
60
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
53
61
 
@@ -55,6 +63,8 @@ module AppMap
55
63
  # Skip methods that have no instruction sequence, as they are obviously trivial.
56
64
  next unless disasm
57
65
 
66
+ next if config.never_hook?(method)
67
+
58
68
  next unless \
59
69
  config.always_hook?(hook_cls, method.name) ||
60
70
  config.included_by_location?(method)
@@ -76,6 +86,8 @@ module AppMap
76
86
  tp.enable(&block)
77
87
  end
78
88
 
89
+ # hook_builtins builds hooks for code that is built in to the Ruby standard library.
90
+ # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
79
91
  def hook_builtins
80
92
  return unless self.class.lock_builtins
81
93
 
@@ -89,6 +101,7 @@ module AppMap
89
101
  require hook.package.package_name if hook.package.package_name
90
102
  Array(hook.method_names).each do |method_name|
91
103
  method_name = method_name.to_sym
104
+
92
105
  cls = class_from_string.(class_name)
93
106
  method = \
94
107
  begin
@@ -97,6 +110,8 @@ module AppMap
97
110
  cls.method(method_name) rescue nil
98
111
  end
99
112
 
113
+ next if config.never_hook?(method)
114
+
100
115
  if method
101
116
  Hook::Method.new(hook.package, cls, method).activate
102
117
  else
@@ -39,6 +39,7 @@ module AppMap
39
39
  end
40
40
 
41
41
  defined_class = @defined_class
42
+ hook_package = self.hook_package
42
43
  hook_method = self.hook_method
43
44
  before_hook = self.method(:before_hook)
44
45
  after_hook = self.method(:after_hook)
@@ -48,29 +49,34 @@ module AppMap
48
49
  hook_class.instance_eval do
49
50
  hook_method_def = Proc.new do |*args, &block|
50
51
  instance_method = hook_method.bind(self).to_proc
52
+ call_instance_method = -> { instance_method.call(*args, &block) }
51
53
 
52
54
  # We may not have gotten the class for the method during
53
55
  # initialization (e.g. for a singleton method on an embedded
54
56
  # struct), so make sure we have it now.
55
- defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
57
+ defined_class, = Hook.qualify_method_name(hook_method) unless defined_class
56
58
 
57
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
58
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
59
- return instance_method.call(*args, &block) unless enabled
59
+ reentrant = Thread.current[HOOK_DISABLE_KEY]
60
+ disabled_by_shallow_flag = \
61
+ -> { hook_package&.shallow? && AppMap.tracing.last_package_for_current_thread == hook_package }
60
62
 
61
- call_event, start_time = with_disabled_hook.() do
62
- before_hook.(self, defined_class, args)
63
+ enabled = true if AppMap.tracing.enabled? && !reentrant && !disabled_by_shallow_flag.call
64
+
65
+ return call_instance_method.call unless enabled
66
+
67
+ call_event, start_time = with_disabled_hook.call do
68
+ before_hook.call(self, defined_class, args)
63
69
  end
64
70
  return_value = nil
65
71
  exception = nil
66
72
  begin
67
- return_value = instance_method.(*args, &block)
73
+ return_value = call_instance_method.call
68
74
  rescue
69
75
  exception = $ERROR_INFO
70
76
  raise
71
77
  ensure
72
- with_disabled_hook.() do
73
- after_hook.(self, call_event, start_time, return_value, exception)
78
+ with_disabled_hook.call do
79
+ after_hook.call(self, call_event, start_time, return_value, exception)
74
80
  end
75
81
  end
76
82
  end
@@ -87,7 +93,7 @@ module AppMap
87
93
  [ call_event, TIME_NOW.call ]
88
94
  end
89
95
 
90
- def after_hook(receiver, call_event, start_time, return_value, exception)
96
+ def after_hook(_receiver, call_event, start_time, return_value, exception)
91
97
  require 'appmap/event'
92
98
  elapsed = TIME_NOW.call - start_time
93
99
  return_event = \
@@ -95,12 +101,12 @@ module AppMap
95
101
  AppMap.tracing.record_event return_event
96
102
  end
97
103
 
98
- def with_disabled_hook(&fn)
104
+ def with_disabled_hook(&function)
99
105
  # Don't record functions, such as to_s and inspect, that might be called
100
106
  # by the fn. Otherwise there can be a stack overflow.
101
107
  Thread.current[HOOK_DISABLE_KEY] = true
102
108
  begin
103
- fn.call
109
+ function.call
104
110
  ensure
105
111
  Thread.current[HOOK_DISABLE_KEY] = false
106
112
  end
@@ -7,12 +7,13 @@ module AppMap
7
7
  module Rails
8
8
  module RequestHandler
9
9
  class HTTPServerRequest < AppMap::Event::MethodEvent
10
- attr_accessor :request_method, :path_info, :params
10
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params
11
11
 
12
12
  def initialize(request)
13
13
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
14
 
15
15
  @request_method = request.request_method
16
+ @normalized_path_info = normalized_path request
16
17
  @path_info = request.path_info.split('?')[0]
17
18
  # ActionDispatch::Http::ParameterFilter is deprecated
18
19
  parameter_filter_cls = \
@@ -28,8 +29,9 @@ module AppMap
28
29
  super.tap do |h|
29
30
  h[:http_server_request] = {
30
31
  request_method: request_method,
31
- path_info: path_info
32
- }
32
+ path_info: path_info,
33
+ normalized_path_info: normalized_path_info
34
+ }.compact
33
35
 
34
36
  h[:message] = params.keys.map do |key|
35
37
  val = params[key]
@@ -42,6 +44,18 @@ module AppMap
42
44
  end
43
45
  end
44
46
  end
47
+
48
+ private
49
+
50
+ def normalized_path(request, router = ::Rails.application.routes.router)
51
+ router.recognize request do |route, _|
52
+ app = route.app
53
+ next unless app.matches? request
54
+ return normalized_path request, app.rack_app.routes.router if app.engine?
55
+
56
+ return route.path.spec.to_s
57
+ end
58
+ end
45
59
  end
46
60
 
47
61
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
@@ -5,13 +5,9 @@ module AppMap
5
5
  class Railtie < ::Rails::Railtie
6
6
  config.appmap = ActiveSupport::OrderedOptions.new
7
7
 
8
- initializer 'appmap.init' do |_| # params: app
9
- require 'appmap'
10
- end
11
-
12
8
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
13
9
  # AppMap events.
14
- initializer 'appmap.subscribe', after: 'appmap.init' do |_| # params: app
10
+ initializer 'appmap.subscribe' do |_| # params: app
15
11
  require 'appmap/rails/sql_handler'
16
12
  require 'appmap/rails/request_handler'
17
13
  ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
data/lib/appmap/trace.rb CHANGED
@@ -15,34 +15,38 @@ module AppMap
15
15
 
16
16
  class Tracing
17
17
  def initialize
18
- @tracing = []
18
+ @tracers = []
19
19
  end
20
20
 
21
21
  def empty?
22
- @tracing.empty?
22
+ @tracers.empty?
23
23
  end
24
24
 
25
25
  def trace(enable: true)
26
26
  Tracer.new.tap do |tracer|
27
- @tracing << tracer
27
+ @tracers << tracer
28
28
  tracer.enable if enable
29
29
  end
30
30
  end
31
31
 
32
32
  def enabled?
33
- @tracing.any?(&:enabled?)
33
+ @tracers.any?(&:enabled?)
34
+ end
35
+
36
+ def last_package_for_current_thread
37
+ @tracers.first&.last_package_for_current_thread
34
38
  end
35
39
 
36
40
  def record_event(event, package: nil, defined_class: nil, method: nil)
37
- @tracing.each do |tracer|
41
+ @tracers.each do |tracer|
38
42
  tracer.record_event(event, package: package, defined_class: defined_class, method: method)
39
43
  end
40
44
  end
41
45
 
42
46
  def delete(tracer)
43
- return unless @tracing.member?(tracer)
47
+ return unless @tracers.member?(tracer)
44
48
 
45
- @tracing.delete(tracer)
49
+ @tracers.delete(tracer)
46
50
  tracer.disable
47
51
  end
48
52
  end
@@ -52,6 +56,7 @@ module AppMap
52
56
  # Records the events which happen in a program.
53
57
  def initialize
54
58
  @events = []
59
+ @last_package_for_thread = {}
55
60
  @methods = Set.new
56
61
  @enabled = false
57
62
  end
@@ -75,11 +80,17 @@ module AppMap
75
80
  def record_event(event, package: nil, defined_class: nil, method: nil)
76
81
  return unless @enabled
77
82
 
83
+ @last_package_for_thread[Thread.current.object_id] = package if package
78
84
  @events << event
79
85
  @methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
80
86
  if package && defined_class && method && (event.event == :call)
81
87
  end
82
88
 
89
+ # Gets the last package which was observed on the current thread.
90
+ def last_package_for_current_thread
91
+ @last_package_for_thread[Thread.current.object_id]
92
+ end
93
+
83
94
  # Gets a unique list of the methods that were invoked by the program.
84
95
  def event_methods
85
96
  @methods.to_a
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.37.2'
6
+ VERSION = '0.41.0'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.3'
8
+ APPMAP_FORMAT_VERSION = '1.4'
9
9
  end
@@ -1,89 +1,150 @@
1
1
  require 'rails_spec_helper'
2
2
 
3
3
  describe 'AbstractControllerBase' do
4
- shared_examples 'rails version' do |rails_major_version|
5
- include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app" do
6
- around(:each) do |example|
4
+ %w[5 6].each do |rails_major_version| # rubocop:disable Metrics/BlockLength
5
+ context "in Rails #{rails_major_version}" do
6
+ include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app"
7
+ def run_spec(spec_name)
7
8
  FileUtils.rm_rf tmpdir
8
9
  FileUtils.mkdir_p tmpdir
9
- cmd = "docker-compose run --rm -e APPMAP=true -v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec spec/controllers/users_controller_api_spec.rb:8"
10
+ cmd = <<~CMD.gsub "\n", ' '
11
+ docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true
12
+ -v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
13
+ CMD
10
14
  run_cmd cmd, chdir: fixture_dir
15
+ end
11
16
 
12
- example.run
17
+ def tmpdir
18
+ 'tmp/spec/AbstractControllerBase'
13
19
  end
14
20
 
15
- let(:tmpdir) { 'tmp/spec/AbstractControllerBase' }
16
- let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json') }
21
+ let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
22
+ let(:events) { appmap['events'] }
17
23
 
18
24
  describe 'testing with rspec' do
19
- it 'inventory file is printed' do
20
- expect(File).to exist(File.join(tmpdir, 'appmap/rspec/Inventory.appmap.json'))
21
- end
22
-
23
- it 'message fields are recorded in the appmap' do
24
- expect(File).to exist(appmap_json)
25
- appmap = JSON.parse(File.read(appmap_json)).to_yaml
25
+ describe 'creating a user' do
26
+ before(:all) { run_spec 'spec/controllers/users_controller_api_spec.rb:8' }
27
+ let(:appmap_json_file) do
28
+ 'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
29
+ end
26
30
 
27
- expect(appmap).to include(<<-MESSAGE.strip)
28
- message:
29
- - name: login
30
- class: String
31
- value: alice
32
- object_id:
33
- MESSAGE
31
+ it 'inventory file is printed' do
32
+ expect(File).to exist(File.join(tmpdir, 'appmap/rspec/Inventory.appmap.json'))
33
+ end
34
34
 
35
- expect(appmap).to include(<<-MESSAGE.strip)
36
- - name: password
37
- class: String
38
- value: "[FILTERED]"
39
- object_id:
40
- MESSAGE
35
+ it 'message fields are recorded in the appmap' do
36
+ expect(events).to include(
37
+ hash_including(
38
+ 'http_server_request' => hash_including(
39
+ 'request_method' => 'POST',
40
+ 'path_info' => '/api/users'
41
+ ),
42
+ 'message' => include(
43
+ hash_including(
44
+ 'name' => 'login',
45
+ 'class' => 'String',
46
+ 'value' => 'alice',
47
+ 'object_id' => Integer
48
+ ),
49
+ hash_including(
50
+ 'name' => 'password',
51
+ 'class' => 'String',
52
+ 'value' => '[FILTERED]',
53
+ 'object_id' => Integer
54
+ )
55
+ )
56
+ ),
57
+ hash_including(
58
+ 'http_server_response' => {
59
+ 'status' => 201,
60
+ 'mime_type' => 'application/json; charset=utf-8'
61
+ }
62
+ )
63
+ )
64
+ end
41
65
 
42
- expect(appmap).to include(<<-SERVER_REQUEST.strip)
43
- http_server_request:
44
- request_method: POST
45
- path_info: "/api/users"
46
- SERVER_REQUEST
66
+ it 'properly captures method parameters in the appmap' do
67
+ expect(events).to include hash_including(
68
+ 'event' => 'call',
69
+ 'thread_id' => Integer,
70
+ 'defined_class' => 'Api::UsersController',
71
+ 'method_id' => 'build_user',
72
+ 'path' => 'app/controllers/api/users_controller.rb',
73
+ 'lineno' => 23,
74
+ 'static' => false,
75
+ 'parameters' => include(
76
+ 'name' => 'params',
77
+ 'class' => 'ActiveSupport::HashWithIndifferentAccess',
78
+ 'object_id' => Integer,
79
+ 'value' => '{"login"=>"alice"}',
80
+ 'kind' => 'req'
81
+ ),
82
+ 'receiver' => anything
83
+ )
84
+ end
47
85
 
48
- expect(appmap).to include(<<-SERVER_RESPONSE.strip)
49
- http_server_response:
50
- status: 201
51
- mime_type: application/json; charset=utf-8
52
- SERVER_RESPONSE
86
+ it 'returns a minimal event' do
87
+ expect(events).to include hash_including(
88
+ 'event' => 'return',
89
+ 'return_value' => Hash,
90
+ 'id' => Integer,
91
+ 'thread_id' => Integer,
92
+ 'parent_id' => Integer,
93
+ 'elapsed' => Numeric
94
+ )
95
+ end
53
96
  end
54
97
 
55
- it 'properly captures method parameters in the appmap' do
56
- expect(File).to exist(appmap_json)
57
- appmap = JSON.parse(File.read(appmap_json)).to_yaml
98
+ describe 'showing a user' do
99
+ before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:22' }
100
+ let(:appmap_json_file) do
101
+ 'UsersController_GET_users_login_shows_the_user.appmap.json'
102
+ end
58
103
 
59
- expect(appmap).to match(<<-CREATE_CALL.strip)
60
- event: call
61
- thread_id: .*
62
- defined_class: Api::UsersController
63
- method_id: build_user
64
- path: app/controllers/api/users_controller.rb
65
- lineno: 23
66
- static: false
67
- parameters:
68
- - name: params
69
- class: ActiveSupport::HashWithIndifferentAccess
70
- object_id: .*
71
- value: '{"login"=>"alice"}'
72
- kind: req
73
- receiver:
74
- CREATE_CALL
104
+ it 'records the normalized path info' do
105
+ expect(events).to include(
106
+ hash_including(
107
+ 'http_server_request' => {
108
+ 'request_method' => 'GET',
109
+ 'path_info' => '/users/alice',
110
+ 'normalized_path_info' => '/users/:id(.:format)'
111
+ }
112
+ )
113
+ )
114
+ end
75
115
  end
76
116
 
77
- it 'returns a minimal event' do
78
- expect(File).to exist(appmap_json)
79
- appmap = JSON.parse(File.read(appmap_json))
80
- event = appmap['events'].find { |event| event['event'] == 'return' && event['return_value'] }
81
- expect(event.keys).to eq(%w[id event thread_id parent_id elapsed return_value])
117
+ describe 'listing users' do
118
+ before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:11' }
119
+ let(:appmap_json_file) { 'UsersController_GET_users_lists_the_users.appmap.json' }
120
+
121
+ it 'records and labels view rendering' do
122
+ expect(events).to include hash_including(
123
+ 'event' => 'call',
124
+ 'thread_id' => Numeric,
125
+ 'defined_class' => 'ActionView::Renderer',
126
+ 'method_id' => 'render',
127
+ 'path' => String,
128
+ 'lineno' => Integer,
129
+ 'static' => false
130
+ )
131
+
132
+ expect(appmap['classMap']).to include hash_including(
133
+ 'name' => 'action_view',
134
+ 'children' => include(hash_including(
135
+ 'name' => 'ActionView',
136
+ 'children' => include(hash_including(
137
+ 'name' => 'Renderer',
138
+ 'children' => include(hash_including(
139
+ 'name' => 'render',
140
+ 'labels' => ['mvc.view']
141
+ ))
142
+ ))
143
+ ))
144
+ )
145
+ end
82
146
  end
83
147
  end
84
148
  end
85
149
  end
86
-
87
- it_behaves_like 'rails version', '5'
88
- it_behaves_like 'rails version', '6'
89
150
  end