Class: RailsErrorDashboard::Services::ErrorHashGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_error_dashboard/services/error_hash_generator.rb

Overview

Pure algorithm: Generate consistent hash for error deduplication

No database access — accepts exception data, returns a hash string. Same hash = same error type for grouping purposes.

Two entry points:

  • ‘.call(exception, …)` — used by LogError command (exception-based)

  • ‘.from_attributes(…)` — used by ErrorLog model callback (attribute-based)

Examples:

ErrorHashGenerator.call(exception, controller_name: "users", action_name: "show", application_id: 1)
# => "a1b2c3d4e5f6g7h8"

Class Method Summary collapse

Class Method Details

.call(exception, controller_name: nil, action_name: nil, application_id: nil, context: {}) ⇒ String

Generate hash from an exception object (used by LogError command)

Parameters:

  • exception (Exception)

    The exception to hash

  • controller_name (String, nil) (defaults to: nil)

    Controller context

  • action_name (String, nil) (defaults to: nil)

    Action context

  • application_id (Integer, nil) (defaults to: nil)

    Application for per-app deduplication

  • context (Hash) (defaults to: {})

    Full error context (passed to custom fingerprint lambda)

Returns:

  • (String)

    16-character hex hash



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/rails_error_dashboard/services/error_hash_generator.rb', line 25

def self.call(exception, controller_name: nil, action_name: nil, application_id: nil, context: {})
  # Check for custom fingerprint lambda
  custom = try_custom_fingerprint(exception, context)
  return custom if custom

  normalized_message = normalize_message(exception.message)
  file_path = extract_app_frame_from_locations(exception) || extract_app_frame(exception.backtrace)

  digest_input = [
    exception.class.name,
    normalized_message,
    file_path,
    controller_name,
    action_name,
    application_id.to_s
  ].compact.join("|")

  Digest::SHA256.hexdigest(digest_input)[0..15]
end

.extract_app_frame(backtrace) ⇒ String?

Extract first meaningful app code frame from backtrace

Parameters:

  • backtrace (Array<String>, nil)

    Exception backtrace

Returns:

  • (String, nil)

    File path of first app code frame



106
107
108
109
110
111
112
113
114
# File 'lib/rails_error_dashboard/services/error_hash_generator.rb', line 106

def self.extract_app_frame(backtrace)
  return nil if backtrace.nil?

  first_app_frame = backtrace.find { |frame|
    !frame.include?("/gems/")
  }

  first_app_frame&.split(":")&.first
end

.extract_app_frame_from_locations(exception) ⇒ String?

Extract first meaningful app code frame using backtrace_locations More reliable than string parsing — uses Location#absolute_path directly.

Parameters:

  • exception (Exception)

    The exception with backtrace_locations

Returns:

  • (String, nil)

    File path of first app code frame, or nil



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rails_error_dashboard/services/error_hash_generator.rb', line 86

def self.extract_app_frame_from_locations(exception)
  locations = exception.backtrace_locations
  return nil if locations.nil? || locations.empty?

  first_app_location = locations.find { |loc|
    path = loc.absolute_path || loc.path
    !path&.include?("/gems/")
  }

  first_app_location && (first_app_location.absolute_path || first_app_location.path)
rescue => e
  RailsErrorDashboard::Logger.debug(
    "[RailsErrorDashboard] extract_app_frame_from_locations failed: #{e.message}"
  )
  nil
end

.from_attributes(error_type:, message: nil, backtrace: nil, controller_name: nil, action_name: nil, application_id: nil) ⇒ String

Generate hash from error attributes (used by ErrorLog model callback) Uses ErrorNormalizer for smarter normalization and significant frame extraction

Parameters:

  • error_type (String)

    The error class name

  • message (String, nil) (defaults to: nil)

    The error message

  • backtrace (String, nil) (defaults to: nil)

    The backtrace as a string

  • controller_name (String, nil) (defaults to: nil)

    Controller context

  • action_name (String, nil) (defaults to: nil)

    Action context

  • application_id (Integer, nil) (defaults to: nil)

    Application for per-app deduplication

Returns:

  • (String)

    16-character hex hash



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/rails_error_dashboard/services/error_hash_generator.rb', line 54

def self.from_attributes(error_type:, message: nil, backtrace: nil, controller_name: nil, action_name: nil, application_id: nil)
  normalized_message = ErrorNormalizer.normalize(message)
  significant_frames = ErrorNormalizer.extract_significant_frames(backtrace, count: 3)

  digest_input = [
    error_type,
    normalized_message,
    significant_frames,
    controller_name,
    action_name,
    application_id.to_s
  ].compact.join("|")

  Digest::SHA256.hexdigest(digest_input)[0..15]
end

.normalize_message(message) ⇒ String?

Normalize dynamic values in error messages for consistent hashing

Parameters:

  • message (String, nil)

    The error message

Returns:

  • (String, nil)

    Normalized message



73
74
75
76
77
78
79
80
# File 'lib/rails_error_dashboard/services/error_hash_generator.rb', line 73

def self.normalize_message(message)
  message
    &.gsub(/0x[0-9a-f]+/i, "HEX")          # Replace hex addresses (before numbers)
    &.gsub(/#<[^>]+>/, "#<OBJ>")           # Replace object inspections
    &.gsub(/\d+/, "N")                     # Replace numbers
    &.gsub(/"[^"]*"/, '""')                # Replace double-quoted strings
    &.gsub(/'[^']*'/, "''")                # Replace single-quoted strings
end