Class: RailsErrorDashboard::ErrorLog

Inherits:
ErrorLogsRecord show all
Defined in:
app/models/rails_error_dashboard/error_log.rb

Constant Summary collapse

CRITICAL_ERROR_TYPES =
%w[
  SecurityError
  NoMemoryError
  SystemStackError
  SignalException
  ActiveRecord::StatementInvalid
].freeze
HIGH_SEVERITY_ERROR_TYPES =
%w[
  ActiveRecord::RecordNotFound
  ArgumentError
  TypeError
  NoMethodError
  NameError
].freeze
MEDIUM_SEVERITY_ERROR_TYPES =
%w[
  ActiveRecord::RecordInvalid
  Timeout::Error
  Net::ReadTimeout
  Net::OpenTimeout
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.belongs_to(*args, **options) ⇒ Object

Override user association to use configured user model



177
178
179
180
181
182
183
# File 'app/models/rails_error_dashboard/error_log.rb', line 177

def self.belongs_to(*args, **options)
  if args.first == :user
    user_model = RailsErrorDashboard.configuration.user_model
    options[:class_name] = user_model if user_model.present?
  end
  super
end

.find_or_increment_by_hash(error_hash, attributes = {}) ⇒ Object

Find existing error by hash or create new one This is CRITICAL for accurate occurrence tracking



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/models/rails_error_dashboard/error_log.rb', line 109

def self.find_or_increment_by_hash(error_hash, attributes = {})
  # Look for unresolved error with same hash in last 24 hours
  # (resolved errors are considered "fixed" so new occurrence = new issue)
  existing = unresolved
              .where(error_hash: error_hash)
              .where("occurred_at >= ?", 24.hours.ago)
              .order(last_seen_at: :desc)
              .first

  if existing
    # Increment existing error
    existing.update!(
      occurrence_count: existing.occurrence_count + 1,
      last_seen_at: Time.current,
      # Update context from latest occurrence
      user_id: attributes[:user_id] || existing.user_id,
      request_url: attributes[:request_url] || existing.request_url,
      request_params: attributes[:request_params] || existing.request_params,
      user_agent: attributes[:user_agent] || existing.user_agent,
      ip_address: attributes[:ip_address] || existing.ip_address
    )
    existing
  else
    # Create new error record
    # Ensure resolved has a value (default to false)
    create!(attributes.reverse_merge(resolved: false))
  end
end

.log_error(exception, context = {}) ⇒ Object

Log an error with context (delegates to Command)



139
140
141
# File 'app/models/rails_error_dashboard/error_log.rb', line 139

def self.log_error(exception, context = {})
  Commands::LogError.call(exception, context)
end

.statistics(days = 7) ⇒ Object

Get error statistics



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'app/models/rails_error_dashboard/error_log.rb', line 149

def self.statistics(days = 7)
  start_date = days.days.ago

  {
    total: where("occurred_at >= ?", start_date).count,
    unresolved: where("occurred_at >= ?", start_date).unresolved.count,
    by_type: where("occurred_at >= ?", start_date)
      .group(:error_type)
      .count
      .sort_by { |_, count| -count }
      .to_h,
    by_day: where("occurred_at >= ?", start_date)
      .group("DATE(occurred_at)")
      .count
  }
end

Instance Method Details

#critical?Boolean

Check if this is a critical error

Returns:

  • (Boolean)


62
63
64
# File 'app/models/rails_error_dashboard/error_log.rb', line 62

def critical?
  CRITICAL_ERROR_TYPES.include?(error_type)
end

#generate_error_hashObject

Generate unique hash for error grouping Includes controller/action for better context-aware grouping



48
49
50
51
52
53
54
55
56
57
58
59
# File 'app/models/rails_error_dashboard/error_log.rb', line 48

def generate_error_hash
  # Hash based on error class, normalized message, first stack frame, controller, and action
  digest_input = [
    error_type,
    message&.gsub(/\d+/, "N")&.gsub(/"[^"]*"/, '""'), # Normalize numbers and strings
    backtrace&.lines&.first&.split(":")&.first, # Just the file, not line number
    controller_name, # Controller context
    action_name      # Action context
  ].compact.join("|")

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

#recent?Boolean

Check if error is recent (< 1 hour)

Returns:

  • (Boolean)


67
68
69
# File 'app/models/rails_error_dashboard/error_log.rb', line 67

def recent?
  occurred_at >= 1.hour.ago
end

Find related errors of the same type



167
168
169
170
171
172
# File 'app/models/rails_error_dashboard/error_log.rb', line 167

def related_errors(limit: 5)
  self.class.where(error_type: error_type)
      .where.not(id: id)
      .order(occurred_at: :desc)
      .limit(limit)
end

#resolve!(resolution_data = {}) ⇒ Object

Mark error as resolved (delegates to Command)



144
145
146
# File 'app/models/rails_error_dashboard/error_log.rb', line 144

def resolve!(resolution_data = {})
  Commands::ResolveError.call(id, resolution_data)
end

#set_defaultsObject



34
35
36
37
# File 'app/models/rails_error_dashboard/error_log.rb', line 34

def set_defaults
  self.environment ||= Rails.env.to_s
  self.platform ||= "API"
end

#set_tracking_fieldsObject



39
40
41
42
43
44
# File 'app/models/rails_error_dashboard/error_log.rb', line 39

def set_tracking_fields
  self.error_hash ||= generate_error_hash
  self.first_seen_at ||= Time.current
  self.last_seen_at ||= Time.current
  self.occurrence_count ||= 1
end

#severityObject

Get severity level



77
78
79
80
81
82
# File 'app/models/rails_error_dashboard/error_log.rb', line 77

def severity
  return :critical if CRITICAL_ERROR_TYPES.include?(error_type)
  return :high if HIGH_SEVERITY_ERROR_TYPES.include?(error_type)
  return :medium if MEDIUM_SEVERITY_ERROR_TYPES.include?(error_type)
  :low
end

#stale?Boolean

Check if error is old unresolved (> 7 days)

Returns:

  • (Boolean)


72
73
74
# File 'app/models/rails_error_dashboard/error_log.rb', line 72

def stale?
  !resolved? && occurred_at < 7.days.ago
end