enhanced_errors 3.0.2 → 3.0.4

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.
@@ -1,55 +1,54 @@
1
1
  # exception.rb
2
- require_relative 'enhanced_exception_context'
3
-
4
- module ExceptionBindingInfos
5
- def binding_infos
6
- ctx = EnhancedExceptionContext.context_for(self)
7
- unless ctx
8
- ctx = Context.new
9
- EnhancedExceptionContext.store_context(self, ctx)
2
+ require_relative 'exception_context'
3
+
4
+ module Enhanced
5
+ module ExceptionBindingInfos
6
+ def binding_infos
7
+ ctx = Enhanced::ExceptionContext.context_for(self)
8
+ unless ctx
9
+ ctx = Context.new
10
+ Enhanced::ExceptionContext.store_context(self, ctx)
11
+ end
12
+ ctx.binding_infos
10
13
  end
11
- ctx.binding_infos
12
- end
13
14
 
14
- def captured_variables
15
- if binding_infos.any?
15
+ def captured_variables
16
+ return '' unless binding_infos&.any?
16
17
  bindings_of_interest = select_binding_infos
17
18
  EnhancedErrors.format(bindings_of_interest)
18
- else
19
+ rescue
19
20
  ''
20
21
  end
21
- rescue
22
- ''
23
- end
24
22
 
25
- private
23
+ private
26
24
 
27
- def select_binding_infos
28
- # Preference:
29
- # 1. First 'raise' binding that isn't from a library (gem).
30
- # 2. If none, the first binding.
31
- # 3. The last 'rescue' binding if available.
25
+ def select_binding_infos
26
+ # Preference:
27
+ # 1. First 'raise' binding that isn't from a library (gem).
28
+ # 2. If none, the first binding.
29
+ # 3. The last 'rescue' binding if available.
32
30
 
33
- bindings_of_interest = []
31
+ bindings_of_interest = []
34
32
 
35
- first_app_raise = binding_infos.find do |info|
36
- info[:capture_event] == 'raise' && !info[:library]
37
- end
38
- bindings_of_interest << first_app_raise if first_app_raise
33
+ first_app_raise = binding_infos.find do |info|
34
+ info[:capture_event] == 'raise' && !info[:library]
35
+ end
36
+ bindings_of_interest << first_app_raise if first_app_raise
39
37
 
40
- if bindings_of_interest.empty? && binding_infos.first
41
- bindings_of_interest << binding_infos.first
42
- end
38
+ if bindings_of_interest.empty? && binding_infos.first
39
+ bindings_of_interest << binding_infos.first
40
+ end
43
41
 
44
- last_rescue = binding_infos.reverse.find do |info|
45
- info[:capture_event] == 'rescue'
46
- end
47
- bindings_of_interest << last_rescue if last_rescue
42
+ last_rescue = binding_infos.reverse.find do |info|
43
+ info[:capture_event] == 'rescue'
44
+ end
45
+ bindings_of_interest << last_rescue if last_rescue
48
46
 
49
- bindings_of_interest.compact
47
+ bindings_of_interest.compact
48
+ end
50
49
  end
51
50
  end
52
51
 
53
52
  class Exception
54
- prepend ExceptionBindingInfos
53
+ prepend Enhanced::ExceptionBindingInfos
55
54
  end
@@ -0,0 +1,49 @@
1
+ require 'weakref'
2
+
3
+ require_relative 'context'
4
+
5
+ require 'weakref'
6
+ require 'monitor'
7
+
8
+ module Enhanced
9
+ module ExceptionContext
10
+ extend self
11
+
12
+ REGISTRY = {}
13
+ MUTEX = Monitor.new
14
+
15
+ def store_context(exception, context)
16
+ MUTEX.synchronize do
17
+ REGISTRY[exception.object_id] = { weak_exc: WeakRef.new(exception), context: context }
18
+ end
19
+ end
20
+
21
+ def context_for(exception)
22
+ MUTEX.synchronize do
23
+ entry = REGISTRY[exception.object_id]
24
+ return nil unless entry
25
+
26
+ begin
27
+ _ = entry[:weak_exc].__getobj__ # ensure exception is still alive
28
+ entry[:context]
29
+ rescue RefError
30
+ # Exception no longer alive, clean up
31
+ REGISTRY.delete(exception.object_id)
32
+ nil
33
+ end
34
+ end
35
+ end
36
+
37
+ def clear_context(exception)
38
+ MUTEX.synchronize do
39
+ REGISTRY.delete(exception.object_id)
40
+ end
41
+ end
42
+
43
+ def clear_all
44
+ MUTEX.synchronize do
45
+ REGISTRY.clear
46
+ end
47
+ end
48
+ end
49
+ end
@@ -9,7 +9,7 @@ module Minitest
9
9
  begin
10
10
  binding_infos = EnhancedErrors.stop_minitest_binding_capture
11
11
  EnhancedErrors.override_exception_message(result.failures.last, binding_infos) if result.failures.any?
12
- EnhancedExceptionContext.clear_all
12
+ Enhanced::ExceptionContext.clear_all
13
13
  rescue => e
14
14
  puts "Ignored error during error enhancement: #{e}"
15
15
  end
@@ -4,12 +4,12 @@ require 'set'
4
4
  require 'json'
5
5
  require 'monitor'
6
6
 
7
- require_relative 'enhanced/colors'
8
- require_relative 'enhanced/exception'
7
+ module Enhanced; end
9
8
 
10
- IGNORED_EXCEPTIONS = %w[SystemExit NoMemoryError SignalException Interrupt ScriptError LoadError
11
- NotImplementedError SyntaxError RSpec::Expectations::ExpectationNotMetError
12
- RSpec::Matchers::BuiltIn::RaiseError SystemStackError Psych::BadAlias]
9
+ # Exceptions we could handle but overlook for other reasons. These class constants are not always loaded
10
+ # and generally are only be available when `required`, so we detect them by strings.
11
+ IGNORED_EXCEPTIONS = %w[RSpec::Expectations::ExpectationNotMetError RSpec::Matchers::BuiltIn::RaiseError
12
+ JSON::ParserError Zlib::Error OpenSSL::SSL::SSLError Psych::Exception]
13
13
 
14
14
  class EnhancedErrors
15
15
  extend ::Enhanced
@@ -45,13 +45,8 @@ class EnhancedErrors
45
45
  ].freeze
46
46
 
47
47
  RAILS_SKIP_LIST = [
48
- :@new_record,
49
- :@attributes,
50
48
  :@association_cache,
51
- :@readonly,
52
- :@previously_new_record,
53
49
  :@_routes,
54
- :@routes,
55
50
  :@app,
56
51
  :@arel_table,
57
52
  :@assertion_instance,
@@ -127,7 +122,7 @@ class EnhancedErrors
127
122
  mutex.synchronize { @max_capture_length || DEFAULT_MAX_CAPTURE_LENGTH }
128
123
  end
129
124
 
130
- def max_capture_length=(val)
125
+ def max_capture_length=(value)
131
126
  mutex.synchronize { @max_capture_length = value }
132
127
  end
133
128
 
@@ -140,7 +135,6 @@ class EnhancedErrors
140
135
  end
141
136
  end
142
137
 
143
-
144
138
  def reset!
145
139
  mutex.synchronize do
146
140
  @rspec_tracepoint&.disable
@@ -155,31 +149,28 @@ class EnhancedErrors
155
149
 
156
150
  def skip_list
157
151
  mutex.synchronize do
158
- @skip_list ||= DEFAULT_SKIP_LIST
152
+ @skip_list ||= DEFAULT_SKIP_LIST.to_set
159
153
  end
160
154
  end
161
155
 
162
156
  def override_rspec_message(example, binding_or_bindings)
163
157
  exception_obj = example.exception
164
- case exception_obj
165
- when nil
166
- return nil
167
- when RSpec::Core::MultipleExceptionError
158
+ return if exception_obj.nil?
159
+
160
+ from_bindings = [binding_or_bindings].flatten.compact
161
+ case exception_obj.class.to_s
162
+ when 'RSpec::Core::MultipleExceptionError'
168
163
  exception_obj.all_exceptions.each do |exception|
169
- override_exception_message(exception, binding_or_bindings)
164
+ override_exception_message(exception, from_bindings + exception.binding_infos)
170
165
  end
171
- else
166
+ when 'RSpec::Expectations::ExpectationNotMetError'
172
167
  override_exception_message(exception_obj, binding_or_bindings)
168
+ else
169
+ override_exception_message(exception_obj, from_bindings + exception_obj.binding_infos)
173
170
  end
174
171
  end
175
172
 
176
173
  def override_exception_message(exception, binding_or_bindings)
177
- return nil unless exception && exception.respond_to?(:message)
178
- test_binding = !(binding_or_bindings.nil? || binding_or_bindings.empty?)
179
- exception_binding = (exception.binding_infos.length > 0)
180
- has_message = !(exception.respond_to?(:unaltered_message))
181
- return nil unless (test_binding || exception_binding) && has_message
182
-
183
174
  variable_str = EnhancedErrors.format(binding_or_bindings)
184
175
  message_str = exception.message
185
176
  exception.define_singleton_method(:unaltered_message) { message_str }
@@ -190,12 +181,13 @@ class EnhancedErrors
190
181
 
191
182
  def add_to_skip_list(*vars)
192
183
  mutex.synchronize do
193
- @skip_list.concat(vars)
184
+ @skip_list.add(*vars)
194
185
  end
195
186
  end
196
187
 
197
188
  def enhance_exceptions!(enabled: true, debug: false, capture_events: nil, override_messages: false, **options, &block)
198
189
  mutex.synchronize do
190
+ ensure_extensions_are_required
199
191
  @exception_trace&.disable
200
192
  @exception_trace = nil
201
193
 
@@ -226,10 +218,11 @@ class EnhancedErrors
226
218
 
227
219
  events = @capture_events ? @capture_events.to_a : default_capture_events
228
220
  @exception_trace = TracePoint.new(*events) do |tp|
221
+ return unless exception_is_handleable?(tp.raised_exception)
229
222
  handle_tracepoint_event(tp)
230
223
  end
231
224
 
232
- @exception_trace.enable if @enabled
225
+ @exception_trace&.enable if @enabled
233
226
  end
234
227
  end
235
228
 
@@ -238,7 +231,8 @@ class EnhancedErrors
238
231
  end
239
232
 
240
233
  def start_minitest_binding_capture
241
- EnhancedExceptionContext.clear_all
234
+ ensure_extensions_are_required
235
+ Enhanced::ExceptionContext.clear_all
242
236
  @enabled = true if @enabled.nil?
243
237
  return unless @enabled
244
238
  mutex.synchronize do
@@ -246,7 +240,7 @@ class EnhancedErrors
246
240
  next unless tp.method_id.to_s.start_with?('test_') && is_a_minitest?(tp.defined_class)
247
241
  @minitest_test_binding = tp.binding
248
242
  end
249
- @minitest_trace.enable
243
+ @minitest_trace&.enable
250
244
  end
251
245
  end
252
246
 
@@ -272,7 +266,8 @@ class EnhancedErrors
272
266
  end
273
267
 
274
268
  def start_rspec_binding_capture
275
- EnhancedExceptionContext.clear_all
269
+ ensure_extensions_are_required
270
+ Enhanced::ExceptionContext.clear_all
276
271
  @enabled = true if @enabled.nil?
277
272
  return unless @enabled
278
273
 
@@ -280,34 +275,43 @@ class EnhancedErrors
280
275
  @rspec_example_binding = nil
281
276
  @capture_next_binding = false
282
277
  @rspec_tracepoint&.disable
283
-
284
- @rspec_tracepoint = TracePoint.new(:raise, :b_return) do |tp|
285
- # puts "name #{tp.raised_exception.class.name rescue ''} method:#{tp.method_id} tp.binding:#{tp.binding.local_variables rescue ''}"
286
- # puts "event: #{tp.event} defined_class#{class_to_string(tp.defined_class)} #{tp.path}:#{tp.lineno} #{tp.callee_id} "
287
- # This trickery below is to help us identify the anonymous block return we want to grab
288
- # Very kluge-y and edge cases have grown it, but it works
289
- if tp.event == :b_return
290
- if RSPEC_HANDLER_NAMES.include?(class_to_string(tp.defined_class))
291
- @capture_next_binding = :next
292
- next
293
- end
294
- next unless @capture_next_binding
295
- if @capture_next_binding == :next || @capture_next_binding == :next_matching && is_rspec_example?(tp)
296
- @capture_next_binding = false
297
- @rspec_example_binding = tp.binding
298
- end
299
- elsif tp.event == :raise
300
- class_name = tp.raised_exception.class.name
301
- case class_name
302
- when 'RSpec::Expectations::ExpectationNotMetError'
303
- @capture_next_binding = :next_matching
304
- else
305
- handle_tracepoint_event(tp)
306
- end
278
+ @rspec_tracepoint = TracePoint.new(:raise) do |tp|
279
+ return unless exception_is_handleable?(tp.raised_exception)
280
+ class_name = tp.raised_exception.class.name
281
+ case class_name
282
+ when 'RSpec::Expectations::ExpectationNotMetError'
283
+ start_rspec_binding_trap
284
+ else
285
+ handle_tracepoint_event(tp)
307
286
  end
308
287
  end
309
- @rspec_tracepoint.enable
310
288
  end
289
+ @rspec_tracepoint&.enable
290
+ end
291
+
292
+ # Behavior: Grabs the next rspec spec binding that goes by, and stops the more-expensive b_return trace.
293
+ # This part of RSpec has been stable, since 2015, so although this is kluge-y, it is stable.
294
+ # The optimization does a 2-3x on spec speed vs. opening up the Tracepoint. With it,
295
+ # things are pretty close in speed to plain rspec.
296
+ # Should the behavior change this can be updated by using a trace to print out items
297
+ # and their local variables then, find the exception or call that goes by right
298
+ # before the spec blocks with the variables, and use that to narrow-down the costly part of
299
+ # the probe to just this point in time. The good news is that
300
+ # this part is test-time only, and this optimization and kluge only applies to RSpec.
301
+ def start_rspec_binding_trap
302
+ @rspec_binding_trap = TracePoint.new(:b_return) do |tp|
303
+ # kluge-y hack and will be a pain to maintain
304
+ if tp.callee_id == :handle_matcher
305
+ @capture_next_binding = :next
306
+ next
307
+ end
308
+ next unless @capture_next_binding
309
+ @capture_next_binding = false
310
+ @rspec_example_binding = tp.binding
311
+ @rspec_binding_trap&.disable
312
+ @rspec_binding_trap = nil
313
+ end
314
+ @rspec_binding_trap&.enable
311
315
  end
312
316
 
313
317
  def stop_rspec_binding_capture
@@ -328,6 +332,8 @@ class EnhancedErrors
328
332
 
329
333
  locals = b.local_variables.map { |var| [var, safe_local_variable_get(b, var)] }.to_h
330
334
  receiver = b.receiver
335
+ return unless safe_to_inspect?(receiver)
336
+
331
337
  instance_vars = receiver.instance_variables
332
338
  instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
333
339
 
@@ -356,7 +362,7 @@ class EnhancedErrors
356
362
  globals: {}
357
363
  },
358
364
  exception: 'NoException',
359
- capture_event: 'RSpecContext'
365
+ capture_event: 'test_context'
360
366
  }
361
367
 
362
368
  default_on_capture(binding_info)
@@ -438,11 +444,7 @@ class EnhancedErrors
438
444
  env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
439
445
  @output_format = case env
440
446
  when 'development', 'test'
441
- if running_in_ci?
442
- :plaintext
443
- else
444
- :terminal
445
- end
447
+ running_in_ci? ? :plaintext : :terminal
446
448
  when 'production'
447
449
  :json
448
450
  else
@@ -454,28 +456,32 @@ class EnhancedErrors
454
456
  def running_in_ci?
455
457
  mutex.synchronize do
456
458
  return @running_in_ci if defined?(@running_in_ci)
457
-
458
459
  @running_in_ci = CI_ENV_VARS.any? { |_, value| value.to_s.downcase == 'true' }
459
460
  end
460
461
  end
461
462
 
462
463
  def apply_skip_list(binding_info)
463
- mutex.synchronize do
464
- variables = binding_info[:variables]
465
- variables[:instances]&.reject! { |var, _| skip_list.include?(var) || (var.to_s.start_with?('@_') && !@debug) }
466
- variables[:locals]&.reject! { |var, _| skip_list.include?(var) }
467
- if @debug
468
- variables[:globals]&.reject! { |var, _| skip_list.include?(var) }
469
- end
464
+ binding_info[:variables][:instances]&.reject! { |var, _| skip_list.include?(var) || (var.to_s[0, 2] == '@_' && !@debug) }
465
+ binding_info[:variables][:locals]&.reject! { |var, _| skip_list.include?(var) }
466
+ if @debug
467
+ binding_info[:variables][:globals]&.reject! { |var, _| skip_list.include?(var) }
470
468
  end
471
469
  binding_info
472
470
  end
473
471
 
474
472
  def validate_binding_format(binding_info)
475
- unless binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
476
- return nil
473
+ binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
474
+ end
475
+
476
+ # Here, we are detecting BasicObject, which is surprisingly annoying.
477
+ # We also, importantly, need to detect descendants, as they will also present with a
478
+ # lack of :respond_to? and any other useful method for us.
479
+ def safe_to_inspect?(obj)
480
+ begin
481
+ obj.class
482
+ rescue NoMethodError
483
+ return false
477
484
  end
478
- binding_info
479
485
  end
480
486
 
481
487
  def binding_info_string(binding_info)
@@ -525,7 +531,6 @@ class EnhancedErrors
525
531
 
526
532
  private
527
533
 
528
-
529
534
  def handle_tracepoint_event(tp)
530
535
  # Check enabled outside the synchronized block for speed, but still safe due to re-check inside.
531
536
  return unless enabled
@@ -534,9 +539,13 @@ class EnhancedErrors
534
539
  Thread.current[:enhanced_errors_processing] = true
535
540
  exception = tp.raised_exception
536
541
 
537
- capture_me = mutex.synchronize do
538
- !exception.frozen? && (@eligible_for_capture || method(:default_eligible_for_capture)).call(exception)
539
- end
542
+ return if exception.frozen?
543
+
544
+ capture_me = if @eligible_for_capture
545
+ @eligible_for_capture.call(exception)
546
+ else
547
+ default_eligible_for_capture(exception)
548
+ end
540
549
 
541
550
  unless capture_me
542
551
  Thread.current[:enhanced_errors_processing] = false
@@ -545,32 +554,43 @@ class EnhancedErrors
545
554
 
546
555
  binding_context = tp.binding
547
556
  method_name = tp.method_id
557
+
558
+ locals = {}
559
+
560
+ binding_context.local_variables.each do |var|
561
+ locals[var] = safe_local_variable_get(binding_context, var)
562
+ end
563
+
548
564
  method_and_args = {
549
565
  object_name: determine_object_name(tp, method_name),
550
- args: extract_arguments(tp, method_name)
566
+ args: extract_arguments(tp, method_name, locals)
551
567
  }
552
568
 
553
- locals = binding_context.local_variables.map { |var|
554
- [var, safe_local_variable_get(binding_context, var)]
555
- }.to_h
569
+ instances = {}
570
+ receiver = binding_context.receiver
556
571
 
557
- instance_vars = binding_context.receiver.instance_variables
558
- instances = instance_vars.map { |var|
559
- [var, safe_instance_variable_get(binding_context.receiver, var)]
560
- }.to_h
572
+ begin
573
+ if safe_to_inspect?(receiver)
574
+ receiver.instance_variables.each { |var|
575
+ instances[var] = safe_instance_variable_get(receiver, var)
576
+ }
577
+ end
578
+ rescue => e
579
+ puts "#{e.class.name} #{e.backtrace}"
580
+ end
561
581
 
562
582
  lets = {}
563
583
 
564
584
  globals = {}
565
585
  mutex.synchronize do
566
586
  if @debug
567
- globals = (global_variables - @original_global_variables.to_a).map { |var|
568
- [var, get_global_variable_value(var)]
569
- }.to_h
587
+ globals = (global_variables - @original_global_variables.to_a).each do |var|
588
+ globals[var] = get_global_variable_value(var)
589
+ end
570
590
  end
571
591
  end
572
592
 
573
- capture_event = safe_to_s(tp.event)
593
+ capture_event = tp.event.to_s
574
594
  location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
575
595
  binding_info = {
576
596
  source: location,
@@ -584,17 +604,16 @@ class EnhancedErrors
584
604
  lets: lets,
585
605
  globals: globals
586
606
  },
587
- exception: safe_to_s(exception.class.name),
607
+ exception: exception.class.name,
588
608
  capture_event: capture_event
589
609
  }
590
610
 
591
611
  binding_info = default_on_capture(binding_info)
592
- on_capture_hook_local = mutex.synchronize { @on_capture_hook }
593
612
 
594
- if on_capture_hook_local
613
+ if on_capture_hook
595
614
  begin
596
615
  Thread.current[:on_capture] = true
597
- binding_info = on_capture_hook_local.call(binding_info)
616
+ binding_info = on_capture_hook.call(binding_info)
598
617
  rescue
599
618
  binding_info = nil
600
619
  ensure
@@ -602,20 +621,16 @@ class EnhancedErrors
602
621
  end
603
622
  end
604
623
 
605
- if binding_info
606
- binding_info = validate_binding_format(binding_info)
607
- if binding_info && exception.binding_infos.length >= MAX_BINDING_INFOS
608
- exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
609
- end
610
- if binding_info
611
- exception.binding_infos << binding_info
612
- mutex.synchronize do
613
- override_exception_message(exception, exception.binding_infos) if @override_messages
614
- end
615
- end
624
+ return unless binding_info && validate_binding_format(binding_info)
625
+ if exception.binding_infos.length >= MAX_BINDING_INFOS
626
+ exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
616
627
  end
617
- rescue
618
- # Avoid raising exceptions here
628
+ exception.binding_infos << binding_info
629
+ mutex.synchronize do
630
+ override_exception_message(exception, exception.binding_infos) if @override_messages
631
+ end
632
+ rescue => e
633
+ puts "Error: #{e&.class&.name} #{e&.backtrace}"
619
634
  ensure
620
635
  Thread.current[:enhanced_errors_processing] = false
621
636
  end
@@ -675,17 +690,15 @@ class EnhancedErrors
675
690
  capture_events.is_a?(Array) && capture_events.all? { |ev| [:raise, :rescue].include?(ev) }
676
691
  end
677
692
 
678
- def extract_arguments(tp, method_name)
693
+ def extract_arguments(tp, method_name, local_vars_hash)
679
694
  return '' unless method_name
680
695
  begin
681
- bind = tp.binding
682
696
  unbound_method = tp.defined_class.instance_method(method_name)
683
697
  method_obj = unbound_method.bind(tp.self)
684
698
  parameters = method_obj.parameters
685
- locals = bind.local_variables
686
699
 
687
700
  parameters.map do |(_, name)|
688
- value = locals.include?(name) ? safe_local_variable_get(bind, name) : nil
701
+ value = local_vars_hash[name]
689
702
  "#{name}=#{safe_inspect(value)}"
690
703
  rescue => e
691
704
  "#{name}=[Error getting argument: #{e.message}]"
@@ -697,17 +710,20 @@ class EnhancedErrors
697
710
 
698
711
  def determine_object_name(tp, method_name = '')
699
712
  begin
700
- self_class = Object.instance_method(:class).bind(tp.self).call
701
- singleton_class = Object.instance_method(:singleton_class).bind(tp.self).call
702
-
703
- if self_class && tp.defined_class == singleton_class
704
- object_identifier = safe_to_s(tp.self)
705
- method_suffix = method_name && !method_name.empty? ? ".#{method_name}" : ""
706
- "#{object_identifier}#{method_suffix}"
713
+ # Check if we're dealing with a singleton method
714
+ if (
715
+ class << tp.self
716
+ self;
717
+ end) == tp.defined_class
718
+ # Singleton method call
719
+ object_str = safe_to_s(tp.self)
720
+ method_suffix = method_name.to_s.empty? ? '' : ".#{method_name}"
721
+ "#{object_str}#{method_suffix}"
707
722
  else
708
- object_class_name = safe_to_s(self_class.name || 'UnknownClass')
709
- method_suffix = method_name && !method_name.empty? ? "##{method_name}" : ""
710
- "#{object_class_name}#{method_suffix}"
723
+ # Instance method call
724
+ klass_name = safe_to_s(tp.self.class.name || 'UnknownClass')
725
+ method_suffix = method_name.to_s.empty? ? '' : "##{method_name}"
726
+ "#{klass_name}#{method_suffix}"
711
727
  end
712
728
  rescue
713
729
  '[ErrorGettingName]'
@@ -799,10 +815,42 @@ class EnhancedErrors
799
815
  apply_skip_list(binding_info)
800
816
  end
801
817
 
818
+ # By default, we have filtering for safety, but past that, we capture everything by default
819
+ # at the moment.
802
820
  def default_eligible_for_capture(exception)
803
- ignored = ignored_exception?(exception)
804
- rspec = exception.class.name.start_with?('RSpec::Matchers')
805
- !ignored && !rspec
821
+ true
806
822
  end
823
+
824
+ def exception_is_handleable?(exception)
825
+ case exception
826
+ when SystemExit, SignalException, SystemStackError, NoMemoryError
827
+ # Non-actionable: Ignore these exceptions
828
+ false
829
+ when SyntaxError, LoadError, ScriptError
830
+ # Non-actionable: Structural issues so there's no useful runtime context
831
+ false
832
+ else
833
+ # Ignore internal fatal errors
834
+ exception.class.to_s != 'fatal'
835
+ end
836
+ end
837
+
838
+ # This prevents loading it for say, production, if you don't want to,
839
+ # and keeps things cleaner. It allows a path to put this behind a feature-flag
840
+ # or env variable, and dynamically enable some capture instrumentation only
841
+ # when a Heisenbug is being hunted.
842
+ def ensure_extensions_are_required
843
+ mutex.synchronize do
844
+ return if @loaded_required_extensions
845
+ require_relative 'enhanced/colors'
846
+ require_relative 'enhanced/exception'
847
+ @loaded_required_extensions = true
848
+ end
849
+ end
850
+
807
851
  end
808
852
  end
853
+
854
+ module Enhanced
855
+
856
+ end