Module: Alplus::Dedup

Defined in:
lib/alplus/dedup.rb

Overview

Exception dedup (issue #15), mirroring packages/sdk/src/core/observe/dedup.ts's resolveDedupId: the same error captured twice within a short window (auto-capture AND a manual capture_exception for the same raised exception, e.g. the Rack middleware re-raising into a Rails handler that also reports it) produces ONE event, not two.

The JS SDK keys identity-bearing errors in a WeakMap so a dedup entry never outlives (or pins alive) the error object. Ruby's ObjectSpace::WeakMap holds its VALUES weakly too (not just keys) with no other strong referent to a plain dedup-entry object, so a value stashed there is eligible for GC before the next lookup -- unusable for this. Instead, the dedup entry is stashed directly on the error object itself via a hidden instance variable: it lives and dies with the exact same object, which is a stronger and simpler guarantee than a WeakMap gives (zero separate table to leak or prune for this path).

A raised String/Symbol/Number/boolean/nil can't hold an instance variable (and has no reference identity worth keying on regardless -- two unrelated "boom" literals are different objects but the same error), so those use a small bounded value-keyed Hash instead, same split as the JS SDK's isWeakKeyable.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

WINDOW_SECONDS =
2.0
VALUE_CACHE_MAX =

Bounds the primitive-keyed fallback so a flood of distinct thrown strings can't grow it unboundedly.

50
VALUE_KEYABLE_CLASSES =
[String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass].freeze

Class Method Summary collapse

Class Method Details

.reset!Object

Test-only: clears the value-keyed table between examples. The identity path needs no reset — a fresh Exception.new each example carries no leftover ivar.



64
65
66
# File 'lib/alplus/dedup.rb', line 64

def reset!
  @mutex.synchronize { @value_map.clear }
end

.resolve(error, fresh_id) ⇒ Object

Returns {id:, duplicate:} — the fresh id for a new error, or the PREVIOUS capture's id (and duplicate: true) if error was already captured within the window. Never raises: on any internal failure (e.g. a frozen error object rejecting the ivar write), treats it as a fresh, non-duplicate capture rather than risk silently dropping a real error.



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/alplus/dedup.rb', line 48

def resolve(error, fresh_id)
  now = monotonic_now
  @mutex.synchronize do
    if value_keyable?(error)
      resolve_value_keyed(error, fresh_id, now)
    else
      resolve_identity_keyed(error, fresh_id, now)
    end
  end
rescue StandardError
  { id: fresh_id, duplicate: false }
end