appmap 0.43.0 → 0.47.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +33 -2
  4. data/CHANGELOG.md +44 -0
  5. data/README.md +66 -11
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +3 -7
  10. data/lib/appmap/class_map.rb +11 -22
  11. data/lib/appmap/command/record.rb +1 -1
  12. data/lib/appmap/config.rb +180 -67
  13. data/lib/appmap/cucumber.rb +1 -1
  14. data/lib/appmap/event.rb +29 -28
  15. data/lib/appmap/handler/function.rb +19 -0
  16. data/lib/appmap/handler/net_http.rb +107 -0
  17. data/lib/appmap/handler/rails/request_handler.rb +124 -0
  18. data/lib/appmap/handler/rails/sql_handler.rb +152 -0
  19. data/lib/appmap/handler/rails/template.rb +149 -0
  20. data/lib/appmap/hook.rb +111 -70
  21. data/lib/appmap/hook/method.rb +6 -8
  22. data/lib/appmap/middleware/remote_recording.rb +1 -1
  23. data/lib/appmap/minitest.rb +22 -20
  24. data/lib/appmap/railtie.rb +5 -5
  25. data/lib/appmap/record.rb +1 -1
  26. data/lib/appmap/rspec.rb +22 -21
  27. data/lib/appmap/trace.rb +47 -6
  28. data/lib/appmap/util.rb +57 -2
  29. data/lib/appmap/version.rb +2 -2
  30. data/package-lock.json +3 -3
  31. data/release.sh +17 -0
  32. data/spec/abstract_controller_base_spec.rb +76 -15
  33. data/spec/class_map_spec.rb +5 -13
  34. data/spec/config_spec.rb +33 -1
  35. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  36. data/spec/fixtures/hook/method_named_call.rb +11 -0
  37. data/spec/hook_spec.rb +143 -22
  38. data/spec/record_net_http_spec.rb +160 -0
  39. data/spec/spec_helper.rb +10 -0
  40. data/spec/util_spec.rb +18 -1
  41. data/test/expectations/openssl_test_key_sign1.json +2 -4
  42. data/test/gem_test.rb +1 -1
  43. data/test/rspec_test.rb +0 -13
  44. metadata +20 -14
  45. data/exe/appmap +0 -154
  46. data/lib/appmap/rails/request_handler.rb +0 -140
  47. data/lib/appmap/rails/sql_handler.rb +0 -150
  48. data/test/cli_test.rb +0 -116
data/lib/appmap/hook.rb CHANGED
@@ -5,6 +5,7 @@ require 'English'
5
5
  module AppMap
6
6
  class Hook
7
7
  LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
+ LOG_HOOK = (ENV['DEBUG_HOOK'] == 'true')
8
9
 
9
10
  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
11
  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
@@ -32,108 +33,148 @@ module AppMap
32
33
  end
33
34
 
34
35
  attr_reader :config
36
+
35
37
  def initialize(config)
36
38
  @config = config
39
+ @trace_locations = []
40
+ # Paths that are known to be non-tracing
41
+ @notrace_paths = Set.new
37
42
  end
38
43
 
39
44
  # Observe class loading and hook all methods which match the config.
40
- def enable &block
45
+ def enable(&block)
41
46
  require 'appmap/hook/method'
42
47
 
43
48
  hook_builtins
44
49
 
45
- tp = TracePoint.new(:end) do |trace_point|
46
- cls = trace_point.self
47
-
48
- instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
49
- # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
50
- class_methods = begin
51
- if cls.respond_to?(:singleton_class)
52
- cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
53
- else
54
- []
55
- end
56
- rescue NameError
57
- []
58
- end
59
-
60
- hook = lambda do |hook_cls|
61
- lambda do |method_id|
62
- method = begin
63
- hook_cls.public_instance_method(method_id)
64
- rescue NameError
65
- warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
66
- return
67
- end
50
+ @trace_begin = TracePoint.new(:class, &method(:trace_class))
51
+ @trace_end = TracePoint.new(:end, &method(:trace_end))
68
52
 
69
- warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
53
+ @trace_begin.enable(&block)
54
+ end
70
55
 
71
- disasm = RubyVM::InstructionSequence.disasm(method)
72
- # Skip methods that have no instruction sequence, as they are obviously trivial.
73
- next unless disasm
56
+ # hook_builtins builds hooks for code that is built in to the Ruby standard library.
57
+ # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
58
+ def hook_builtins
59
+ return unless self.class.lock_builtins
74
60
 
75
- next if config.never_hook?(method)
61
+ class_from_string = lambda do |fq_class|
62
+ fq_class.split('::').inject(Object) do |mod, class_name|
63
+ mod.const_get(class_name)
64
+ end
65
+ end
76
66
 
77
- next unless \
78
- config.always_hook?(hook_cls, method.name) ||
79
- config.included_by_location?(method)
67
+ config.builtin_methods.each do |class_name, hooks|
68
+ Array(hooks).each do |hook|
69
+ require hook.package.package_name if hook.package.package_name
70
+ Array(hook.method_names).each do |method_name|
71
+ method_name = method_name.to_sym
72
+ base_cls = class_from_string.(class_name)
80
73
 
81
- hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
74
+ hook_method = lambda do |entry|
75
+ cls, method = entry
76
+ return false if config.never_hook?(cls, method)
82
77
 
83
- # Don't try and trace the AppMap methods or there will be
84
- # a stack overflow in the defined hook method.
85
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
78
+ Hook::Method.new(hook.package, cls, method).activate
79
+ end
86
80
 
87
- hook_method.activate
81
+ methods = []
82
+ methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
83
+ if base_cls.respond_to?(:singleton_class)
84
+ methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
85
+ end
86
+ methods.compact!
87
+ if methods.empty?
88
+ warn "Method #{method_name} not found on #{base_cls.name}"
89
+ else
90
+ methods.each(&hook_method)
91
+ end
88
92
  end
89
93
  end
94
+ end
95
+ end
90
96
 
91
- instance_methods.each(&hook.(cls))
92
- # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
93
- begin
94
- class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
95
- rescue NameError
96
- # NameError:
97
- # uninitialized constant Faraday::Connection
97
+ protected
98
+
99
+ def trace_class(trace_point)
100
+ path = trace_point.path
101
+
102
+ return if @notrace_paths.member?(path)
103
+
104
+ if config.path_enabled?(path)
105
+ location = trace_location(trace_point)
106
+ warn "Entering hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
107
+ @trace_locations << location
108
+ unless @trace_end.enabled?
109
+ warn "Enabling hooking" if Hook::LOG || Hook::LOG_HOOK
110
+ @trace_end.enable
98
111
  end
112
+ else
113
+ @notrace_paths << path
99
114
  end
115
+ end
100
116
 
101
- tp.enable(&block)
117
+ def trace_location(trace_point)
118
+ [ trace_point.path, trace_point.lineno ].join(':')
102
119
  end
103
120
 
104
- # hook_builtins builds hooks for code that is built in to the Ruby standard library.
105
- # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
106
- def hook_builtins
107
- return unless self.class.lock_builtins
121
+ def trace_end(trace_point)
122
+ cls = trace_point.self
108
123
 
109
- class_from_string = lambda do |fq_class|
110
- fq_class.split('::').inject(Object) do |mod, class_name|
111
- mod.const_get(class_name)
124
+ instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
125
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
126
+ class_methods = begin
127
+ if cls.respond_to?(:singleton_class)
128
+ cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
129
+ else
130
+ []
112
131
  end
132
+ rescue NameError
133
+ []
113
134
  end
114
135
 
115
- Config::BUILTIN_METHODS.each do |class_name, hook|
116
- require hook.package.package_name if hook.package.package_name
117
- Array(hook.method_names).each do |method_name|
118
- method_name = method_name.to_sym
119
-
120
- cls = class_from_string.(class_name)
121
- method = \
122
- begin
123
- cls.instance_method(method_name)
124
- rescue NameError
125
- cls.method(method_name) rescue nil
126
- end
136
+ hook = lambda do |hook_cls|
137
+ lambda do |method_id|
138
+ # Don't try and trace the AppMap methods or there will be
139
+ # a stack overflow in the defined hook method.
140
+ next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || '').split('::')[0])
141
+
142
+ method = begin
143
+ hook_cls.public_instance_method(method_id)
144
+ rescue NameError
145
+ warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
146
+ next
147
+ end
127
148
 
128
- next if config.never_hook?(method)
149
+ warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
129
150
 
130
- if method
131
- Hook::Method.new(hook.package, cls, method).activate
132
- else
133
- warn "Method #{method_name} not found on #{cls.name}"
134
- end
151
+ disasm = RubyVM::InstructionSequence.disasm(method)
152
+ # Skip methods that have no instruction sequence, as they are obviously trivial.
153
+ next unless disasm
154
+
155
+ package = config.lookup_package(hook_cls, method)
156
+ next unless package
157
+
158
+ Hook::Method.new(package, hook_cls, method).activate
135
159
  end
136
160
  end
161
+
162
+ instance_methods.each(&hook.(cls))
163
+ begin
164
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
165
+ class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
166
+ rescue NameError
167
+ # NameError:
168
+ # uninitialized constant Faraday::Connection
169
+ warn "NameError in #{__FILE__}: #{$!.message}"
170
+ end
171
+
172
+ location = @trace_locations.pop
173
+ warn "Leaving hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
174
+ if @trace_locations.empty?
175
+ warn "Disabling hooking" if Hook::LOG || Hook::LOG_HOOK
176
+ @trace_end.disable
177
+ end
137
178
  end
138
179
  end
139
180
  end
@@ -76,7 +76,7 @@ module AppMap
76
76
  raise
77
77
  ensure
78
78
  with_disabled_hook.call do
79
- after_hook.call(self, call_event, start_time, return_value, exception)
79
+ after_hook.call(self, call_event, start_time, return_value, exception) if call_event
80
80
  end
81
81
  end
82
82
  end
@@ -87,18 +87,16 @@ module AppMap
87
87
  protected
88
88
 
89
89
  def before_hook(receiver, defined_class, args)
90
- require 'appmap/event'
91
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
92
- AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
90
+ call_event = hook_package.handler_class.handle_call(defined_class, hook_method, receiver, args)
91
+ AppMap.tracing.record_event(call_event, package: hook_package, defined_class: defined_class, method: hook_method) if call_event
93
92
  [ call_event, TIME_NOW.call ]
94
93
  end
95
94
 
96
95
  def after_hook(_receiver, call_event, start_time, return_value, exception)
97
- require 'appmap/event'
98
96
  elapsed = TIME_NOW.call - start_time
99
- return_event = \
100
- AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
101
- AppMap.tracing.record_event return_event
97
+ return_event = hook_package.handler_class.handle_return(call_event.id, elapsed, return_value, exception)
98
+ AppMap.tracing.record_event(return_event) if return_event
99
+ nil
102
100
  end
103
101
 
104
102
  def with_disabled_hook(&function)
@@ -67,7 +67,7 @@ module AppMap
67
67
 
68
68
  response = JSON.generate \
69
69
  version: AppMap::APPMAP_FORMAT_VERSION,
70
- classMap: AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
70
+ classMap: AppMap.class_map(tracer.event_methods),
71
71
  metadata: metadata,
72
72
  events: @events
73
73
 
@@ -26,8 +26,9 @@ module AppMap
26
26
  end
27
27
 
28
28
 
29
- def finish
29
+ def finish(exception)
30
30
  warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
31
+ warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
31
32
 
32
33
  events = []
33
34
  AppMap.tracing.delete @trace
@@ -36,15 +37,17 @@ module AppMap
36
37
 
37
38
  AppMap::Minitest.add_event_methods @trace.event_methods
38
39
 
39
- class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
40
+ class_map = AppMap.class_map(@trace.event_methods)
40
41
 
41
42
  feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
42
43
  feature_name = test.name.split('_')[1..-1].join(' ')
43
44
  scenario_name = [ feature_group, feature_name ].join(' ')
44
45
 
45
- AppMap::Minitest.save scenario_name,
46
- class_map,
47
- source_location,
46
+ AppMap::Minitest.save name: scenario_name,
47
+ class_map: class_map,
48
+ source_location: source_location,
49
+ test_status: exception ? 'failed' : 'succeeded',
50
+ exception: exception,
48
51
  events: events
49
52
  end
50
53
  end
@@ -63,11 +66,11 @@ module AppMap
63
66
  @recordings_by_test[test.object_id] = Recording.new(test, name)
64
67
  end
65
68
 
66
- def end_test(test)
69
+ def end_test(test, exception:)
67
70
  recording = @recordings_by_test.delete(test.object_id)
68
71
  return warn "No recording found for #{test}" unless recording
69
72
 
70
- recording.finish
73
+ recording.finish exception
71
74
  end
72
75
 
73
76
  def config
@@ -78,9 +81,9 @@ module AppMap
78
81
  @event_methods += event_methods
79
82
  end
80
83
 
81
- def save(example_name, class_map, source_location, events: nil, labels: nil)
84
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
82
85
  metadata = AppMap::Minitest.metadata.tap do |m|
83
- m[:name] = example_name
86
+ m[:name] = name
84
87
  m[:source_location] = source_location
85
88
  m[:app] = AppMap.configuration.name
86
89
  m[:frameworks] ||= []
@@ -91,6 +94,13 @@ module AppMap
91
94
  m[:recorder] = {
92
95
  name: 'minitest'
93
96
  }
97
+ m[:test_status] = test_status
98
+ if exception
99
+ m[:exception] = {
100
+ class: exception.class.name,
101
+ message: exception.to_s
102
+ }
103
+ end
94
104
  end
95
105
 
96
106
  appmap = {
@@ -99,14 +109,9 @@ module AppMap
99
109
  classMap: class_map,
100
110
  events: events
101
111
  }.compact
102
- fname = AppMap::Util.scenario_filename(example_name)
112
+ fname = AppMap::Util.scenario_filename(name)
103
113
 
104
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
105
- end
106
-
107
- def print_inventory
108
- class_map = AppMap.class_map(@event_methods)
109
- save 'Inventory', class_map, labels: %w[inventory]
114
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
110
115
  end
111
116
 
112
117
  def enabled?
@@ -115,9 +120,6 @@ module AppMap
115
120
 
116
121
  def run
117
122
  init
118
- at_exit do
119
- print_inventory
120
- end
121
123
  end
122
124
  end
123
125
  end
@@ -135,7 +137,7 @@ if AppMap::Minitest.enabled?
135
137
  begin
136
138
  run_without_hook
137
139
  ensure
138
- AppMap::Minitest.end_test self
140
+ AppMap::Minitest.end_test self, exception: $!
139
141
  end
140
142
  end
141
143
  end
@@ -8,12 +8,12 @@ module AppMap
8
8
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
9
9
  # AppMap events.
10
10
  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
11
+ require 'appmap/handler/rails/sql_handler'
12
+ require 'appmap/handler/rails/request_handler'
13
+ ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Handler::Rails::SQLHandler.new
14
+ ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Handler::Rails::SQLHandler.new
15
15
 
16
- AppMap::Rails::RequestHandler::HookMethod.new.activate
16
+ AppMap::Handler::Rails::RequestHandler::HookMethod.new.activate
17
17
  end
18
18
 
19
19
  # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
data/lib/appmap/record.rb CHANGED
@@ -23,5 +23,5 @@ at_exit do
23
23
  'classMap' => AppMap.class_map(tracer.event_methods),
24
24
  'events' => events
25
25
  }
26
- File.write 'appmap.json', JSON.generate(appmap)
26
+ AppMap::Util.write_appmap('appmap.json', JSON.generate(appmap))
27
27
  end
data/lib/appmap/rspec.rb CHANGED
@@ -94,8 +94,9 @@ module AppMap
94
94
  result
95
95
  end
96
96
 
97
- def finish
97
+ def finish(exception)
98
98
  warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
99
+ warn "Exception: #{exception}" if exception && AppMap::RSpec::LOG
99
100
 
100
101
  events = []
101
102
  AppMap.tracing.delete @trace
@@ -104,7 +105,7 @@ module AppMap
104
105
 
105
106
  AppMap::RSpec.add_event_methods @trace.event_methods
106
107
 
107
- class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
108
+ class_map = AppMap.class_map(@trace.event_methods)
108
109
 
109
110
  description = []
110
111
  scope = ScopeExample.new(example)
@@ -127,9 +128,11 @@ module AppMap
127
128
 
128
129
  full_description = normalize.call(description.join(' '))
129
130
 
130
- AppMap::RSpec.save full_description,
131
- class_map,
132
- source_location,
131
+ AppMap::RSpec.save name: full_description,
132
+ class_map: class_map,
133
+ source_location: source_location,
134
+ test_status: exception ? 'failed' : 'succeeded',
135
+ exception: exception,
133
136
  events: events
134
137
  end
135
138
  end
@@ -148,11 +151,11 @@ module AppMap
148
151
  @recordings_by_example[example.object_id] = Recording.new(example)
149
152
  end
150
153
 
151
- def end_spec(example)
154
+ def end_spec(example, exception:)
152
155
  recording = @recordings_by_example.delete(example.object_id)
153
156
  return warn "No recording found for #{example}" unless recording
154
157
 
155
- recording.finish
158
+ recording.finish exception
156
159
  end
157
160
 
158
161
  def config
@@ -163,12 +166,11 @@ module AppMap
163
166
  @event_methods += event_methods
164
167
  end
165
168
 
166
- def save(example_name, class_map, source_location, events: nil, labels: nil)
169
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
167
170
  metadata = AppMap::RSpec.metadata.tap do |m|
168
- m[:name] = example_name
171
+ m[:name] = name
169
172
  m[:source_location] = source_location
170
173
  m[:app] = AppMap.configuration.name
171
- m[:labels] = labels if labels
172
174
  m[:frameworks] ||= []
173
175
  m[:frameworks] << {
174
176
  name: 'rspec',
@@ -177,6 +179,13 @@ module AppMap
177
179
  m[:recorder] = {
178
180
  name: 'rspec'
179
181
  }
182
+ m[:test_status] = test_status
183
+ if exception
184
+ m[:exception] = {
185
+ class: exception.class.name,
186
+ message: exception.to_s
187
+ }
188
+ end
180
189
  end
181
190
 
182
191
  appmap = {
@@ -185,14 +194,9 @@ module AppMap
185
194
  classMap: class_map,
186
195
  events: events
187
196
  }.compact
188
- fname = AppMap::Util.scenario_filename(example_name)
189
-
190
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
191
- end
197
+ fname = AppMap::Util.scenario_filename(name)
192
198
 
193
- def print_inventory
194
- class_map = AppMap.class_map(@event_methods)
195
- save 'Inventory', class_map, labels: %w[inventory]
199
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
196
200
  end
197
201
 
198
202
  def enabled?
@@ -201,9 +205,6 @@ module AppMap
201
205
 
202
206
  def run
203
207
  init
204
- at_exit do
205
- print_inventory
206
- end
207
208
  end
208
209
  end
209
210
  end
@@ -225,7 +226,7 @@ if AppMap::RSpec.enabled?
225
226
  begin
226
227
  instance_exec(&fn)
227
228
  ensure
228
- AppMap::RSpec.end_spec example
229
+ AppMap::RSpec.end_spec example, exception: $!
229
230
  end
230
231
  end
231
232
  end