@@ -174,6 +174,16 @@ def initialize(namespace, id: SecureRandom.uuid, backend: nil, opentelemetry_con
174174 @completed = false
175175 @tags = { }
176176 @store = Hash . new { |hash , key | hash [ key ] = { } }
177+ # The distinct errors added to this transaction, in add order. Drives
178+ # `last_errors`, the dedup check and the `ERRORS_LIMIT`, in both modes.
179+ # A Set, so membership is by object identity (`eql?`/`hash`), matching how
180+ # these errors used to be deduplicated as Hash keys. `Exception#==` would
181+ # instead collapse distinct errors with the same class, message and
182+ # backtrace, which we don't want.
183+ @errors = Set . new
184+ # Blocks per error, only populated in agent mode, where they run against
185+ # the duplicate transactions at completion. Collector mode runs blocks
186+ # when the error is added and never touches this.
177187 @error_blocks = Hash . new { |hash , key | hash [ key ] = [ ] }
178188 @is_duplicate = false
179189 @error_set = nil
@@ -236,7 +246,7 @@ def complete
236246 should_sample = true
237247
238248 unless duplicate?
239- self . class . last_errors = @error_blocks . keys
249+ self . class . last_errors = @errors . to_a
240250 should_sample = @backend . finish
241251 end
242252
@@ -627,7 +637,7 @@ def add_error(error, &block)
627637 return unless error
628638 return unless Appsignal . active?
629639
630- if error . instance_variable_get ( :@__appsignal_error_reported ) && !@error_blocks . include? ( error )
640+ if error . instance_variable_get ( :@__appsignal_error_reported ) && !@errors . include? ( error )
631641 return
632642 end
633643
@@ -715,26 +725,37 @@ def to_h
715725
716726 # @!visibility private
717727 def internal_set_error ( error , &block )
718- is_new_error = !@error_blocks . include? ( error )
728+ is_new_error = !@errors . include? ( error )
719729
720- if is_new_error && @error_blocks . length >= ERRORS_LIMIT
730+ if is_new_error && @errors . length >= ERRORS_LIMIT
721731 Appsignal . internal_logger . warn "Appsignal::Transaction#add_error: Transaction has more " \
722732 "than #{ ERRORS_LIMIT } distinct errors. Only the first " \
723733 "#{ ERRORS_LIMIT } distinct errors will be reported."
724734 return
725735 end
726736
727- if @error_blocks . empty?
737+ if @errors . empty?
728738 _set_error ( error )
729- elsif is_new_error && @backend . records_errors_eagerly ?
739+ elsif is_new_error && @backend . supports_multiple_errors ?
730740 # Record additional errors immediately so each exception event lands on
731741 # the span current now, not the root span at completion. The agent
732742 # backend instead reports extras as duplicate transactions.
733743 _send_error_to_backend ( error )
734744 end
735745
736- @error_blocks [ error ] << block
737- @error_blocks [ error ] . compact!
746+ @errors . add ( error )
747+
748+ if @backend . supports_multiple_errors?
749+ # Collector mode: the error is already recorded, so run its block now
750+ # rather than at completion. Anything a block attaches to the current
751+ # span -- breadcrumbs, nested errors, custom instrumentation -- then
752+ # lands where the error was reported, not on the root span at
753+ # completion.
754+ self . class . with_transaction ( self ) { block . call ( self ) } if block
755+ else
756+ @error_blocks [ error ] << block
757+ @error_blocks [ error ] . compact!
758+ end
738759 end
739760
740761 private
@@ -751,40 +772,25 @@ def run_before_complete_hooks
751772 end
752773 end
753774
754- # Reports the errors stored on the transaction at completion, in one of two
755- # ways depending on the backend:
775+ # Reports the errors stored on the transaction at completion.
756776 #
757- # - eager (collector): each error was already recorded as its own exception
758- # event when added, on the span current at that moment; here we only run
759- # the error blocks.
760- # - deferred (agent): the extension holds a single error, so the primary
761- # error's blocks run on this transaction and every additional error is
762- # reported as a duplicate transaction.
777+ # In eager (collector) mode nothing is left to do: each error was recorded
778+ # and its block run when the error was added. In deferred (agent) mode the
779+ # extension holds a single error, so the primary error's blocks run on this
780+ # transaction and every additional error is reported as a duplicate
781+ # transaction.
763782 def report_errors
764- if @backend . records_errors_eagerly?
765- run_error_blocks
766- else
767- report_errors_as_duplicates
768- end
769- end
783+ return if @backend . supports_multiple_errors?
770784
771- # Eager mode: the errors are already recorded, so just run their blocks.
772- # Blocks run in add-order, so a later error's block wins on a shared key, and
773- # all block-set metadata merges onto the root span. (Per-error metadata
774- # isolation is deferred -- the processor/UI does not read per-event
775- # attributes yet.)
776- def run_error_blocks
777- @error_blocks . each_value do |blocks |
778- self . class . with_transaction ( self ) do
779- blocks . each { |block | block . call ( self ) }
780- end
781- end
785+ report_errors_as_duplicates
782786 end
783787
784- # Agent mode: the extension transaction holds a single error, so report each
785- # additional error as a duplicate transaction.
788+ # Agent-only legacy path. The extension transaction holds a single error, so
789+ # extra errors are reported as duplicate transactions. This whole method
790+ # disappears once agent mode is dropped: collector mode records every error
791+ # eagerly and leaves nothing to do at completion.
786792 def report_errors_as_duplicates
787- @error_blocks . each do |error , blocks |
793+ @errors . each do |error |
788794 # Ignore the error that is already set in this transaction.
789795 next if error == @error_set
790796
@@ -793,7 +799,7 @@ def report_errors_as_duplicates
793799 # with a block that calls all the blocks set for that error
794800 # in the original transaction.
795801 transaction . internal_set_error ( error ) do
796- blocks . each { |block | block . call ( transaction ) }
802+ @error_blocks [ error ] . each { |block | block . call ( transaction ) }
797803 end
798804
799805 transaction . complete
0 commit comments