appmap 0.45.1 → 0.48.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -95,7 +95,7 @@ module AppMap
95
95
  def after_hook(_receiver, call_event, start_time, return_value, exception)
96
96
  elapsed = TIME_NOW.call - start_time
97
97
  return_event = hook_package.handler_class.handle_return(call_event.id, elapsed, return_value, exception)
98
- AppMap.tracing.record_event return_event
98
+ AppMap.tracing.record_event(return_event) if return_event
99
99
  nil
100
100
  end
101
101
 
@@ -98,7 +98,7 @@ module AppMap
98
98
  if exception
99
99
  m[:exception] = {
100
100
  class: exception.class.name,
101
- message: exception.to_s
101
+ message: AppMap::Event::MethodEvent.display_string(exception.to_s)
102
102
  }
103
103
  end
104
104
  end
@@ -3,37 +3,15 @@
3
3
  module AppMap
4
4
  # Railtie connects the AppMap recorder to Rails-specific features.
5
5
  class Railtie < ::Rails::Railtie
6
- config.appmap = ActiveSupport::OrderedOptions.new
7
-
8
6
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
9
7
  # AppMap events.
10
8
  initializer 'appmap.subscribe' do |_| # params: app
11
- require 'appmap/rails/sql_handler'
12
- require 'appmap/rails/request_handler'
13
- ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
14
- ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
15
-
16
- AppMap::Rails::RequestHandler::HookMethod.new.activate
17
- end
18
-
19
- # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
20
- # This behavior is only activated if the configuration setting app.config.appmap.enabled
21
- # is truthy.
22
- initializer 'appmap.trace', after: 'appmap.subscribe' do |app|
23
- lambda do
24
- return unless app.config.appmap.enabled
9
+ require 'appmap/handler/rails/sql_handler'
10
+ require 'appmap/handler/rails/request_handler'
11
+ ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Handler::Rails::SQLHandler.new
12
+ ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Handler::Rails::SQLHandler.new
25
13
 
26
- require 'appmap/command/record'
27
- require 'json'
28
- AppMap::Command::Record.new(AppMap.configuration).perform do |version, metadata, class_map, events|
29
- appmap = JSON.generate \
30
- version: version,
31
- metadata: metadata,
32
- classMap: class_map,
33
- events: events
34
- File.open('appmap.json', 'w').write(appmap)
35
- end
36
- end.call
14
+ AppMap::Handler::Rails::RequestHandler::HookMethod.new.activate
37
15
  end
38
16
  end
39
- end unless ENV['APPMAP_INITIALIZE'] == 'false'
17
+ end if ENV['APPMAP'] == 'true'
data/lib/appmap/rspec.rb CHANGED
@@ -183,7 +183,7 @@ module AppMap
183
183
  if exception
184
184
  m[:exception] = {
185
185
  class: exception.class.name,
186
- message: exception.to_s
186
+ message: AppMap::Event::MethodEvent.display_string(exception.to_s)
187
187
  }
188
188
  end
189
189
  end
data/lib/appmap/trace.rb CHANGED
@@ -2,14 +2,36 @@
2
2
 
3
3
  module AppMap
4
4
  module Trace
5
- class ScopedMethod < SimpleDelegator
6
- attr_reader :package, :defined_class, :static
5
+ class RubyMethod
6
+ attr_reader :class_name, :static
7
7
 
8
- def initialize(package, defined_class, method, static)
8
+ def initialize(package, class_name, method, static)
9
9
  @package = package
10
- @defined_class = defined_class
10
+ @class_name = class_name
11
+ @method = method
11
12
  @static = static
12
- super(method)
13
+ end
14
+
15
+ def source_location
16
+ @method.source_location
17
+ end
18
+
19
+ def comment
20
+ @method.comment
21
+ rescue MethodSource::SourceNotFoundError
22
+ nil
23
+ end
24
+
25
+ def package
26
+ @package.name
27
+ end
28
+
29
+ def name
30
+ @method.name
31
+ end
32
+
33
+ def labels
34
+ @package.labels
13
35
  end
14
36
  end
15
37
 
@@ -43,6 +65,12 @@ module AppMap
43
65
  end
44
66
  end
45
67
 
68
+ def record_method(method)
69
+ @tracers.each do |tracer|
70
+ tracer.record_method(method)
71
+ end
72
+ end
73
+
46
74
  def delete(tracer)
47
75
  return unless @tracers.member?(tracer)
48
76
 
@@ -83,10 +111,22 @@ module AppMap
83
111
  @last_package_for_thread[Thread.current.object_id] = package if package
84
112
  @events << event
85
113
  static = event.static if event.respond_to?(:static)
86
- @methods << Trace::ScopedMethod.new(package, defined_class, method, static) \
114
+ @methods << Trace::RubyMethod.new(package, defined_class, method, static) \
87
115
  if package && defined_class && method && (event.event == :call)
88
116
  end
89
117
 
118
+ # +method+ should be duck-typed to respond to the following:
119
+ # * package
120
+ # * defined_class
121
+ # * name
122
+ # * static
123
+ # * comment
124
+ # * labels
125
+ # * source_location
126
+ def record_method(method)
127
+ @methods << method
128
+ end
129
+
90
130
  # Gets the last package which was observed on the current thread.
91
131
  def last_package_for_current_thread
92
132
  @last_package_for_thread[Thread.current.object_id]
data/lib/appmap/util.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bundler'
4
+
3
5
  module AppMap
4
6
  module Util
5
7
  class << self
@@ -94,13 +96,23 @@ module AppMap
94
96
  end
95
97
 
96
98
  def normalize_path(path)
97
- if path.index(Dir.pwd) == 0
99
+ if path.index(Dir.pwd) == 0 && !path.index(Bundler.bundle_path.to_s)
98
100
  path[Dir.pwd.length + 1..-1]
99
101
  else
100
102
  path
101
103
  end
102
104
  end
103
105
 
106
+ # Convert a Rails-style path from /org/:org_id(.:format)
107
+ # to Swagger-style paths like /org/{org_id}
108
+ def swaggerize_path(path)
109
+ path = path.split('(.')[0]
110
+ tokens = path.split('/')
111
+ tokens.map do |token|
112
+ token.gsub /^:(.*)/, '{\1}'
113
+ end.join('/')
114
+ end
115
+
104
116
  # Atomically writes AppMap data to +filename+.
105
117
  def write_appmap(filename, appmap)
106
118
  require 'fileutils'
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.45.1'
6
+ VERSION = '0.48.1'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.5.0'
8
+ APPMAP_FORMAT_VERSION = '1.5.1'
9
9
  end
data/package-lock.json CHANGED
@@ -551,9 +551,9 @@
551
551
  }
552
552
  },
553
553
  "lodash": {
554
- "version": "4.17.19",
555
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
556
- "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
554
+ "version": "4.17.21",
555
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
556
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
557
557
  },
558
558
  "longest": {
559
559
  "version": "1.0.1",
@@ -42,7 +42,7 @@ describe 'Rails' do
42
42
  hash_including(
43
43
  'http_server_request' => hash_including(
44
44
  'request_method' => 'POST',
45
- 'normalized_path_info' => '/api/users(.:format)',
45
+ 'normalized_path_info' => '/api/users',
46
46
  'path_info' => '/api/users'
47
47
  ),
48
48
  'message' => include(
@@ -144,7 +144,49 @@ describe 'Rails' do
144
144
  end
145
145
 
146
146
  describe 'a UI route' do
147
- describe 'rendering a page' do
147
+ describe 'rendering a page using a template file' do
148
+ let(:appmap_json_file) do
149
+ 'UsersController_GET_users_lists_the_users.appmap.json'
150
+ end
151
+
152
+ it 'records the template file' do
153
+ expect(events).to include hash_including(
154
+ 'event' => 'call',
155
+ 'defined_class' => 'app_views_users_index_html_haml',
156
+ 'method_id' => 'render',
157
+ 'path' => 'app/views/users/index.html.haml'
158
+ )
159
+
160
+ expect(appmap['classMap']).to include hash_including(
161
+ 'name' => 'app/views',
162
+ 'children' => include(hash_including(
163
+ 'name' => 'app_views_users_index_html_haml',
164
+ 'children' => include(hash_including(
165
+ 'name' => 'render',
166
+ 'type' => 'function',
167
+ 'location' => 'app/views/users/index.html.haml',
168
+ 'static' => true,
169
+ 'labels' => [ 'mvc.template' ]
170
+ ))
171
+ ))
172
+ )
173
+ expect(appmap['classMap']).to include hash_including(
174
+ 'name' => 'app/views',
175
+ 'children' => include(hash_including(
176
+ 'name' => 'app_views_layouts_application_html_haml',
177
+ 'children' => include(hash_including(
178
+ 'name' => 'render',
179
+ 'type' => 'function',
180
+ 'location' => 'app/views/layouts/application.html.haml',
181
+ 'static' => true,
182
+ 'labels' => [ 'mvc.template' ]
183
+ ))
184
+ ))
185
+ )
186
+ end
187
+ end
188
+
189
+ describe 'rendering a page using a text template' do
148
190
  let(:appmap_json_file) do
149
191
  'UsersController_GET_users_login_shows_the_user.appmap.json'
150
192
  end
@@ -155,7 +197,7 @@ describe 'Rails' do
155
197
  'http_server_request' => {
156
198
  'request_method' => 'GET',
157
199
  'path_info' => '/users/alice',
158
- 'normalized_path_info' => '/users/:id(.:format)',
200
+ 'normalized_path_info' => '/users/{id}',
159
201
  'headers' => {
160
202
  'Host' => 'test.host',
161
203
  'User-Agent' => 'Rails Testing'
@@ -165,15 +207,32 @@ describe 'Rails' do
165
207
  )
166
208
  end
167
209
 
210
+ it 'ignores the text template' do
211
+ expect(events).to_not include hash_including(
212
+ 'event' => 'call',
213
+ 'method_id' => 'render',
214
+ 'render_template' => anything
215
+ )
216
+
217
+ expect(appmap['classMap']).to_not include hash_including(
218
+ 'name' => 'views',
219
+ 'children' => include(hash_including(
220
+ 'name' => 'ViewTemplate',
221
+ 'children' => include(hash_including(
222
+ 'name' => 'render',
223
+ 'type' => 'function',
224
+ 'location' => 'text template'
225
+ ))
226
+ ))
227
+ )
228
+ end
229
+
168
230
  it 'records and labels view rendering' do
169
231
  expect(events).to include hash_including(
170
232
  'event' => 'call',
171
233
  'thread_id' => Numeric,
172
- 'defined_class' => 'ActionView::Renderer',
173
- 'method_id' => 'render',
174
- 'path' => String,
175
- 'lineno' => Integer,
176
- 'static' => false
234
+ 'defined_class' => 'inline_template',
235
+ 'method_id' => 'render'
177
236
  )
178
237
 
179
238
  expect(appmap['classMap']).to include hash_including(
@@ -5,7 +5,7 @@ require 'spec_helper'
5
5
  describe 'AppMap::ClassMap' do
6
6
  describe '.build_from_methods' do
7
7
  it 'includes method comment' do
8
- map = AppMap.class_map([scoped_method((method :test_method))])
8
+ map = AppMap.class_map([ruby_method((method :test_method))])
9
9
  function = dig_map(map, 5)[0]
10
10
  expect(function).to include(:comment)
11
11
  end
@@ -15,8 +15,8 @@ describe 'AppMap::ClassMap' do
15
15
  'test method body'
16
16
  end
17
17
 
18
- def scoped_method(method)
19
- AppMap::Trace::ScopedMethod.new AppMap::Config::Package.new, method.receiver.class.name, method, false
18
+ def ruby_method(method)
19
+ AppMap::Trace::RubyMethod.new AppMap::Config::Package.new, method.receiver.class.name, method, false
20
20
  end
21
21
 
22
22
  def dig_map(map, depth)
@@ -53,3 +53,9 @@ class ToSRaises
53
53
  "hello"
54
54
  end
55
55
  end
56
+
57
+ class ExceptionMethod
58
+ def raise_illegal_utf8_message
59
+ raise "809: unexpected token at 'x\x9C\xED=\x8Bv\xD3ƶ\xBF2\xB8]\xC5\xE9qdI\x96eǫ4\xA4h΅\x84\xE5z\x96\xAA\xD8\xE3\xE3D\xB2\xE4J2\x90E\xF8\xF7\xBB\xF7\xCC\xE81\x92\xE2\x88ā'"
60
+ end
61
+ end
@@ -38,8 +38,6 @@ module UsersApp
38
38
  # Initialize configuration defaults for originally generated Rails version.
39
39
  config.load_defaults 5.2
40
40
 
41
- config.appmap.enabled = true if ENV['APPMAP'] == 'true' && !Rails.env.test?
42
-
43
41
  # Settings in config/environments/* take precedence over those specified here.
44
42
  # Application configuration can go into files in config/initializers
45
43
  # -- all .rb files in that directory are automatically loaded after loading
@@ -38,8 +38,6 @@ module UsersApp
38
38
  # Initialize configuration defaults for originally generated Rails version.
39
39
  config.load_defaults 5.2
40
40
 
41
- config.appmap.enabled = true if ENV['APPMAP'] == 'true' && !Rails.env.test?
42
-
43
41
  # Settings in config/environments/* take precedence over those specified here.
44
42
  # Application configuration can go into files in config/initializers
45
43
  # -- all .rb files in that directory are automatically loaded after loading
data/spec/hook_spec.rb CHANGED
@@ -64,65 +64,14 @@ describe 'AppMap class Hooking', docker: false do
64
64
  expect(config.never_hook?(ExcludeTest, ExcludeTest.method(:cls_method))).to be_truthy
65
65
  end
66
66
 
67
- it "handles an instance method named 'call' without issues" do
67
+ it "an instance method named 'call' will be ignored" do
68
68
  events_yaml = <<~YAML
69
- ---
70
- - :id: 1
71
- :event: :call
72
- :defined_class: MethodNamedCall
73
- :method_id: call
74
- :path: spec/fixtures/hook/method_named_call.rb
75
- :lineno: 8
76
- :static: false
77
- :parameters:
78
- - :name: :a
79
- :class: Integer
80
- :value: '1'
81
- :kind: :req
82
- - :name: :b
83
- :class: Integer
84
- :value: '2'
85
- :kind: :req
86
- - :name: :c
87
- :class: Integer
88
- :value: '3'
89
- :kind: :req
90
- - :name: :d
91
- :class: Integer
92
- :value: '4'
93
- :kind: :req
94
- - :name: :e
95
- :class: Integer
96
- :value: '5'
97
- :kind: :req
98
- :receiver:
99
- :class: MethodNamedCall
100
- :value: MethodNamedCall
101
- - :id: 2
102
- :event: :return
103
- :parent_id: 1
104
- :return_value:
105
- :class: String
106
- :value: 1 2 3 4 5
69
+ --- []
107
70
  YAML
108
71
 
109
72
  _, tracer = test_hook_behavior 'spec/fixtures/hook/method_named_call.rb', events_yaml do
110
73
  expect(MethodNamedCall.new.call(1, 2, 3, 4, 5)).to eq('1 2 3 4 5')
111
74
  end
112
- class_map = AppMap.class_map(tracer.event_methods)
113
- expect(Diffy::Diff.new(<<~CLASSMAP, YAML.dump(class_map)).to_s).to eq('')
114
- ---
115
- - :name: spec/fixtures/hook/method_named_call.rb
116
- :type: package
117
- :children:
118
- - :name: MethodNamedCall
119
- :type: class
120
- :children:
121
- - :name: call
122
- :type: function
123
- :location: spec/fixtures/hook/method_named_call.rb:8
124
- :static: false
125
- CLASSMAP
126
75
  end
127
76
 
128
77
  it 'can custom hook and label a function' do
@@ -250,8 +199,8 @@ describe 'AppMap class Hooking', docker: false do
250
199
  _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
251
200
  InstanceMethod.new.say_default
252
201
  end
253
- expect(tracer.event_methods.to_a.map(&:defined_class)).to eq([ 'InstanceMethod' ])
254
- expect(tracer.event_methods.to_a.map(&:to_s)).to eq([ InstanceMethod.public_instance_method(:say_default).to_s ])
202
+ expect(tracer.event_methods.to_a.map(&:class_name)).to eq([ 'InstanceMethod' ])
203
+ expect(tracer.event_methods.to_a.map(&:name)).to eq([ InstanceMethod.public_instance_method(:say_default).name ])
255
204
  end
256
205
 
257
206
  it 'builds a class map of invoked methods' do
@@ -634,7 +583,7 @@ describe 'AppMap class Hooking', docker: false do
634
583
  end
635
584
  end
636
585
 
637
- it 'Reports exceptions' do
586
+ it 'reports exceptions' do
638
587
  events_yaml = <<~YAML
639
588
  ---
640
589
  - :id: 1
@@ -666,6 +615,38 @@ describe 'AppMap class Hooking', docker: false do
666
615
  end
667
616
  end
668
617
 
618
+ it 'sanitizes exception messages' do
619
+ events_yaml = <<~YAML
620
+ ---
621
+ - :id: 1
622
+ :event: :call
623
+ :defined_class: ExceptionMethod
624
+ :method_id: raise_illegal_utf8_message
625
+ :path: spec/fixtures/hook/exception_method.rb
626
+ :lineno: 58
627
+ :static: false
628
+ :parameters: []
629
+ :receiver:
630
+ :class: ExceptionMethod
631
+ :value: Exception Method fixture
632
+ - :id: 2
633
+ :event: :return
634
+ :parent_id: 1
635
+ :exceptions:
636
+ - :class: RuntimeError
637
+ :message: '809: unexpected token at ''x__=_v_ƶ_2_]__qdI_eǫ4_h΅__z_____D__J2_E______1__ā'''
638
+ :path: spec/fixtures/hook/exception_method.rb
639
+ :lineno: 59
640
+ YAML
641
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
642
+ begin
643
+ ExceptionMethod.new.raise_illegal_utf8_message
644
+ rescue
645
+ # don't let the exception fail the test
646
+ end
647
+ end
648
+ end
649
+
669
650
  context 'string conversions works for the receiver when' do
670
651
 
671
652
  it 'is missing #to_s' do