Class: RailsErrorDashboard::ErrorLog

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

Constant Summary collapse

PRIORITY_LEVELS =

Priority level constants Using industry standard: P0 = Critical (highest), P3 = Low (lowest)

{
  3 => { label: "Critical", short_label: "P0", color: "danger", emoji: "🔴" },
  2 => { label: "High", short_label: "P1", color: "warning", emoji: "🟠" },
  1 => { label: "Medium", short_label: "P2", color: "info", emoji: "🟡" },
  0 => { label: "Low", short_label: "P3", color: "secondary", emoji: "⚪" }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#just_reopenedObject

Transient flag: set to true when a resolved/wont_fix error is reopened by FindOrIncrementError. Not persisted — used by LogError to decide notification behavior.



9
10
11
# File 'app/models/rails_error_dashboard/error_log.rb', line 9

def just_reopened
  @just_reopened
end

Class Method Details

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

Override user association to use configured user model



370
371
372
373
374
375
376
# File 'app/models/rails_error_dashboard/error_log.rb', line 370

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 — delegates to Command



137
138
139
# File 'app/models/rails_error_dashboard/error_log.rb', line 137

def self.find_or_increment_by_hash(error_hash, attributes = {})
  Commands::FindOrIncrementError.call(error_hash, attributes)
end

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

Log an error with context (delegates to Command)



142
143
144
# File 'app/models/rails_error_dashboard/error_log.rb', line 142

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

.priority_options(include_emoji: false) ⇒ Object

Class method to get priority options for select dropdowns



186
187
188
189
190
191
192
193
194
195
# File 'app/models/rails_error_dashboard/error_log.rb', line 186

def self.priority_options(include_emoji: false)
  PRIORITY_LEVELS.sort_by { |level, _| -level }.map do |level, data|
    label = if include_emoji
      "#{data[:emoji]} #{data[:label]} (#{data[:short_label]})"
    else
      "#{data[:label]} (#{data[:short_label]})"
    end
    [ label, level ]
  end
end

Instance Method Details

#assigned?Boolean

Assignment query

Returns:

  • (Boolean)


154
155
156
# File 'app/models/rails_error_dashboard/error_log.rb', line 154

def assigned?
  assigned_to.present?
end

#backtrace_framesObject

Extract backtrace frames for similarity comparison



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'app/models/rails_error_dashboard/error_log.rb', line 231

def backtrace_frames
  return [] if backtrace.blank?

  # Handle different backtrace formats
  lines = if backtrace.is_a?(Array)
    backtrace
  elsif backtrace.is_a?(String)
    # Check if it's a serialized array (starts with "[")
    if backtrace.strip.start_with?("[")
      # Try to parse as JSON array
      begin
        JSON.parse(backtrace)
      rescue JSON::ParserError
        # Fall back to newline split
        backtrace.split("\n")
      end
    else
      backtrace.split("\n")
    end
  else
    []
  end

  lines.first(20).map do |line|
    # Extract file path and method name, ignore line numbers
    if line =~ %r{([^/]+\.rb):.*?in `(.+)'$}
      "#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
    elsif line =~ %r{([^/]+\.rb)}
      Regexp.last_match(1)
    end
  end.compact.uniq
end

#baseline_anomaly(sensitivity: 2) ⇒ Hash

Check if this error is anomalous compared to baseline

Parameters:

  • sensitivity (Integer) (defaults to: 2)

    Standard deviations threshold (default: 2)

Returns:

  • (Hash)

    Anomaly check result



320
321
322
323
324
325
326
327
328
329
330
331
# File 'app/models/rails_error_dashboard/error_log.rb', line 320

def baseline_anomaly(sensitivity: 2)
  return { anomaly: false, message: "Feature disabled" } unless RailsErrorDashboard.configuration.enable_baseline_alerts
  return { anomaly: false, message: "No baseline available" } unless defined?(Queries::BaselineStats)

  # Get count of this error type today
  today_count = ErrorLog.where(
    error_type: error_type,
    platform: platform
  ).where("occurred_at >= ?", Time.current.beginning_of_day).count

  Queries::BaselineStats.new(error_type, platform).check_anomaly(today_count, sensitivity: sensitivity)
end

#baselinesHash

Get baseline statistics for this error type

Returns:

  • (Hash)

    ErrorBaseline, daily: ErrorBaseline, weekly: ErrorBaseline



310
311
312
313
314
315
# File 'app/models/rails_error_dashboard/error_log.rb', line 310

def baselines
  return {} unless RailsErrorDashboard.configuration.enable_baseline_alerts
  return {} unless defined?(Queries::BaselineStats)

  Queries::BaselineStats.new(error_type, platform).all_baselines
end

#calculate_backtrace_signatureObject

Calculate backtrace signature — delegates to Service



265
266
267
# File 'app/models/rails_error_dashboard/error_log.rb', line 265

def calculate_backtrace_signature
  Services::BacktraceProcessor.calculate_signature(backtrace)
end

#can_transition_to?(new_status) ⇒ Boolean

Returns:

  • (Boolean)


209
210
211
212
213
214
215
216
217
218
219
220
# File 'app/models/rails_error_dashboard/error_log.rb', line 209

def can_transition_to?(new_status)
  # Define valid status transitions
  valid_transitions = {
    "new" => [ "in_progress", "investigating", "wont_fix" ],
    "in_progress" => [ "investigating", "resolved", "new" ],
    "investigating" => [ "resolved", "in_progress", "wont_fix" ],
    "resolved" => [ "new" ], # Can reopen if error recurs
    "wont_fix" => [ "new" ]  # Can reopen
  }

  valid_transitions[status]&.include?(new_status) || false
end

#co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 10) ⇒ Array<Hash>

Find errors that occur together in time

Parameters:

  • window_minutes (Integer) (defaults to: 5)

    Time window in minutes (default: 5)

  • min_frequency (Integer) (defaults to: 2)

    Minimum co-occurrence count (default: 2)

  • limit (Integer) (defaults to: 10)

    Maximum results (default: 10)

Returns:

  • (Array<Hash>)

    Array of ErrorLog, frequency: Integer, avg_delay_seconds: Float



284
285
286
287
288
289
290
291
292
293
294
295
# File 'app/models/rails_error_dashboard/error_log.rb', line 284

def co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 10)
  return [] unless persisted?
  return [] unless RailsErrorDashboard.configuration.enable_co_occurring_errors
  return [] unless defined?(Queries::CoOccurringErrors)

  Queries::CoOccurringErrors.call(
    error_log_id: id,
    window_minutes: window_minutes,
    min_frequency: min_frequency,
    limit: limit
  )
end

#critical?Boolean

Check if this is a critical error — delegates to SeverityClassifier

Returns:

  • (Boolean)


117
118
119
# File 'app/models/rails_error_dashboard/error_log.rb', line 117

def critical?
  Services::SeverityClassifier.critical?(error_type)
end

#error_bursts(days: 7) ⇒ Array<Hash>

Detect error bursts (many errors in short time)

Parameters:

  • days (Integer) (defaults to: 7)

    Number of days to analyze (default: 7)

Returns:

  • (Array<Hash>)

    Array of burst metadata



354
355
356
357
358
359
360
361
362
363
364
365
# File 'app/models/rails_error_dashboard/error_log.rb', line 354

def error_bursts(days: 7)
  return [] unless RailsErrorDashboard.configuration.enable_occurrence_patterns
  return [] unless defined?(Services::PatternDetector)

  timestamps = self.class
    .where(error_type: error_type, platform: platform)
    .where("occurred_at >= ?", days.days.ago)
    .order(:occurred_at)
    .pluck(:occurred_at)

  Services::PatternDetector.detect_bursts(timestamps: timestamps)
end

#error_cascades(min_probability: 0.5) ⇒ Hash

Find cascade patterns (what causes this error, what this error causes)

Parameters:

  • min_probability (Float) (defaults to: 0.5)

    Minimum cascade probability (0.0-1.0), default 0.5

Returns:

  • (Hash)

    Array, children: Array of cascade patterns



300
301
302
303
304
305
306
# File 'app/models/rails_error_dashboard/error_log.rb', line 300

def error_cascades(min_probability: 0.5)
  return { parents: [], children: [] } unless persisted?
  return { parents: [], children: [] } unless RailsErrorDashboard.configuration.enable_error_cascades
  return { parents: [], children: [] } unless defined?(Queries::ErrorCascades)

  Queries::ErrorCascades.call(error_id: id, min_probability: min_probability)
end

#generate_error_hashObject

Generate unique hash for error grouping — delegates to ErrorHashGenerator Service



105
106
107
108
109
110
111
112
113
114
# File 'app/models/rails_error_dashboard/error_log.rb', line 105

def generate_error_hash
  Services::ErrorHashGenerator.from_attributes(
    error_type: error_type,
    message: message,
    backtrace: backtrace,
    controller_name: controller_name,
    action_name: action_name,
    application_id: application_id
  )
end

#occurrence_pattern(days: 30) ⇒ Hash

Detect cyclical occurrence patterns (daily/weekly rhythms)

Parameters:

  • days (Integer) (defaults to: 30)

    Number of days to analyze (default: 30)

Returns:

  • (Hash)

    Pattern analysis result



336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/rails_error_dashboard/error_log.rb', line 336

def occurrence_pattern(days: 30)
  return {} unless RailsErrorDashboard.configuration.enable_occurrence_patterns
  return {} unless defined?(Services::PatternDetector)

  timestamps = self.class
    .where(error_type: error_type, platform: platform)
    .where("occurred_at >= ?", days.days.ago)
    .pluck(:occurred_at)

  Services::PatternDetector.analyze_cyclical_pattern(
    timestamps: timestamps,
    days: days
  )
end

#priority_colorObject



171
172
173
174
175
176
# File 'app/models/rails_error_dashboard/error_log.rb', line 171

def priority_color
  priority_data = PRIORITY_LEVELS[priority_level]
  return "light" unless priority_data

  priority_data[:color]
end

#priority_emojiObject



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

def priority_emoji
  priority_data = PRIORITY_LEVELS[priority_level]
  return "" unless priority_data

  priority_data[:emoji]
end

#priority_labelObject

Priority methods



164
165
166
167
168
169
# File 'app/models/rails_error_dashboard/error_log.rb', line 164

def priority_label
  priority_data = PRIORITY_LEVELS[priority_level]
  return "Unset" unless priority_data

  "#{priority_data[:label]} (#{priority_data[:short_label]})"
end

#recent?Boolean

Check if error is recent (< 1 hour)

Returns:

  • (Boolean)


122
123
124
# File 'app/models/rails_error_dashboard/error_log.rb', line 122

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

Find related errors of the same type



223
224
225
226
227
228
# File 'app/models/rails_error_dashboard/error_log.rb', line 223

def related_errors(limit: 5, application_id: nil)
  scope = self.class.where(error_type: error_type)
          .where.not(id: id)
  scope = scope.where(application_id: application_id) if application_id.present?
  scope.order(occurred_at: :desc).limit(limit)
end

#reopened?Boolean

Was this error previously resolved and then reopened due to recurrence? Uses the persisted ‘reopened_at` column (set by FindOrIncrementError).

Returns:

  • (Boolean)


13
14
15
# File 'app/models/rails_error_dashboard/error_log.rb', line 13

def reopened?
  respond_to?(:reopened_at) && reopened_at.present?
end

#resolve!(resolution_data = {}) ⇒ Object

Mark error as resolved (delegates to Command)



147
148
149
# File 'app/models/rails_error_dashboard/error_log.rb', line 147

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

#set_defaultsObject



88
89
90
# File 'app/models/rails_error_dashboard/error_log.rb', line 88

def set_defaults
  self.platform ||= "API"
end

#set_priority_scoreObject



99
100
101
102
# File 'app/models/rails_error_dashboard/error_log.rb', line 99

def set_priority_score
  return unless respond_to?(:priority_score=)
  self.priority_score = Services::PriorityScoreCalculator.compute(self)
end

#set_tracking_fieldsObject



92
93
94
95
96
97
# File 'app/models/rails_error_dashboard/error_log.rb', line 92

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 — delegates to SeverityClassifier



132
133
134
# File 'app/models/rails_error_dashboard/error_log.rb', line 132

def severity
  Services::SeverityClassifier.classify(error_type)
end

#similar_errors(threshold: 0.6, limit: 10) ⇒ Array<Hash>

Find similar errors using fuzzy matching

Parameters:

  • threshold (Float) (defaults to: 0.6)

    Minimum similarity score (0.0-1.0), default 0.6

  • limit (Integer) (defaults to: 10)

    Maximum results, default 10

Returns:

  • (Array<Hash>)

    Array of ErrorLog, similarity: Float



273
274
275
276
277
# File 'app/models/rails_error_dashboard/error_log.rb', line 273

def similar_errors(threshold: 0.6, limit: 10)
  return [] unless persisted?
  return [] unless RailsErrorDashboard.configuration.enable_similar_errors
  Queries::SimilarErrors.call(id, threshold: threshold, limit: limit)
end

#snoozed?Boolean

Snooze query

Returns:

  • (Boolean)


159
160
161
# File 'app/models/rails_error_dashboard/error_log.rb', line 159

def snoozed?
  snoozed_until.present? && snoozed_until >= Time.current
end

#stale?Boolean

Check if error is old unresolved (> 7 days)

Returns:

  • (Boolean)


127
128
129
# File 'app/models/rails_error_dashboard/error_log.rb', line 127

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

#status_badge_colorObject

Status transition methods



198
199
200
201
202
203
204
205
206
207
# File 'app/models/rails_error_dashboard/error_log.rb', line 198

def status_badge_color
  case status
  when "new" then "primary"
  when "in_progress" then "info"
  when "investigating" then "warning"
  when "resolved" then "success"
  when "wont_fix" then "secondary"
  else "light"
  end
end