appmap 0.37.2 → 0.41.0

Sign up to get free protection for your applications and to get access to all the features.
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