Module: Sentiero::Fingerprint

Defined in:
lib/sentiero/fingerprint.rb,
lib/sentiero/fingerprint/config.rb

Overview

Computes the grouping fingerprint for an exception occurrence. The normalization regexes are deliberately linear-time (simple character classes with a single quantifier, no nesting) so untrusted backtraces cannot trigger catastrophic backtracking (ReDoS).

Defined Under Namespace

Classes: Config

Constant Summary collapse

MAX_FRAMES =

Only the top frames drive grouping (deeper frames vary by call site).

5
MAX_FRAME_LENGTH =
1000
RUBY_NORMALIZER =

path:LINE:in 'method' (colon-glued) or bare path:LINE. Verbatim pre-refactor logic, kept byte-identical so upgrading does not fracture existing problem groups — this is the tier-1 (no platform tag) default.

->(frame) {
  frame
    .gsub(/0x[0-9a-fA-F]+/, "0xHEX") # memory addresses
    .gsub(/:[0-9]+(?=:in )/, ":N") # `path:LINE:in 'method'`
    .gsub(/:[0-9]+\z/, ":N") # `path:LINE` (top-level frame, no method)
}
CRYSTAL_NORMALIZER =

path.cr:LINE in 'method' or path.cr:LINE:COL in 'method' — Crystal separates the line marker from in with a space, not a colon, and may carry an optional :column, so the Ruby regex is a no-op on it.

->(frame) {
  frame
    .gsub(/0x[0-9a-fA-F]+/, "0xHEX")
    .gsub(/:[0-9]+(?::[0-9]+)?(?= in )/, ":N")
}
GENERIC_NORMALIZER =

Grammar-agnostic fallback: strips a trailing :LINE or :LINE:COL only, with no in-token assumption. Coarser than a language-specific normalizer (it can't see line noise that isn't at the end of the frame) but never mis-applies a foreign grammar.

->(frame) {
  frame
    .gsub(/0x[0-9a-fA-F]+/, "0xHEX")
    .gsub(/:[0-9]+(?::[0-9]+)?\z/, ":N")
}

Class Method Summary collapse

Class Method Details

.compute(exception_class:, backtrace:, project:, normalizer: RUBY_NORMALIZER) ⇒ Object



47
48
49
50
51
# File 'lib/sentiero/fingerprint.rb', line 47

def compute(exception_class:, backtrace:, project:, normalizer: RUBY_NORMALIZER)
  frames = Array(backtrace).first(MAX_FRAMES).map { |frame| safe_normalize(normalizer, frame.to_s) }
  input = "#{project}\n#{exception_class}\n#{frames.join("\n")}"
  Digest::SHA256.hexdigest(input)[0, 40]
end

.safe_normalize(normalizer, frame) ⇒ Object

Internal helper, not part of the public API. Wraps a (possibly user-registered) normalizer so a broken or pathological callable can never break fingerprinting: the frame is capped before and after normalization, and any exception falls back to the raw capped frame. SystemStackError and NoMemoryError are non-StandardError exceptions a pathological normalizer can realistically raise (e.g. unbounded recursion); a bare rescue Exception is deliberately avoided so Interrupt/SystemExit still propagate.



61
62
63
64
65
66
# File 'lib/sentiero/fingerprint.rb', line 61

def safe_normalize(normalizer, frame)
  capped = frame[0, MAX_FRAME_LENGTH].strip
  normalizer.call(capped).to_s[0, MAX_FRAME_LENGTH]
rescue StandardError, SystemStackError, NoMemoryError
  capped
end