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



387
388
389
390
391
392
393
# File 'app/models/rails_error_dashboard/error_log.rb', line 387

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



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

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)



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

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



203
204
205
206
207
208
209
210
211
212
# File 'app/models/rails_error_dashboard/error_log.rb', line 203

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)


157
158
159
# File 'app/models/rails_error_dashboard/error_log.rb', line 157

def assigned?
  assigned_to.present?
end

#backtrace_framesObject

Extract backtrace frames for similarity comparison



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'app/models/rails_error_dashboard/error_log.rb', line 248

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



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

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



327
328
329
330
331
332
# File 'app/models/rails_error_dashboard/error_log.rb', line 327

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



282
283
284
# File 'app/models/rails_error_dashboard/error_log.rb', line 282

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

#can_transition_to?(new_status) ⇒ Boolean

Returns:

  • (Boolean)


226
227
228
229
230
231
232
233
234
235
236
237
# File 'app/models/rails_error_dashboard/error_log.rb', line 226

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



301
302
303
304
305
306
307
308
309
310
311
312
# File 'app/models/rails_error_dashboard/error_log.rb', line 301

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)


120
121
122
# File 'app/models/rails_error_dashboard/error_log.rb', line 120

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



371
372
373
374
375
376
377
378
379
380
381
382
# File 'app/models/rails_error_dashboard/error_log.rb', line 371

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



317
318
319
320
321
322
323
# File 'app/models/rails_error_dashboard/error_log.rb', line 317

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



108
109
110
111
112
113
114
115
116
117
# File 'app/models/rails_error_dashboard/error_log.rb', line 108

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

#mute!(mute_data = {}) ⇒ Object

Mute/unmute convenience methods β€” delegate to Commands



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

def mute!(mute_data = {})
  Commands::MuteError.call(id, **mute_data)
end

#muted?Boolean

Mute query β€” checks column existence for backward compatibility

Returns:

  • (Boolean)


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

def muted?
  self.class.column_names.include?("muted") && muted == true
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



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

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



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

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

  priority_data[:color]
end

#priority_emojiObject



195
196
197
198
199
200
# File 'app/models/rails_error_dashboard/error_log.rb', line 195

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

  priority_data[:emoji]
end

#priority_labelObject

Priority methods



181
182
183
184
185
186
# File 'app/models/rails_error_dashboard/error_log.rb', line 181

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)


125
126
127
# File 'app/models/rails_error_dashboard/error_log.rb', line 125

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

Find related errors of the same type



240
241
242
243
244
245
# File 'app/models/rails_error_dashboard/error_log.rb', line 240

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)



150
151
152
# File 'app/models/rails_error_dashboard/error_log.rb', line 150

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

#set_defaultsObject



91
92
93
# File 'app/models/rails_error_dashboard/error_log.rb', line 91

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

#set_priority_scoreObject



102
103
104
105
# File 'app/models/rails_error_dashboard/error_log.rb', line 102

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

#set_tracking_fieldsObject



95
96
97
98
99
100
# File 'app/models/rails_error_dashboard/error_log.rb', line 95

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



135
136
137
# File 'app/models/rails_error_dashboard/error_log.rb', line 135

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



290
291
292
293
294
# File 'app/models/rails_error_dashboard/error_log.rb', line 290

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)


162
163
164
# File 'app/models/rails_error_dashboard/error_log.rb', line 162

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

#stale?Boolean

Check if error is old unresolved (> 7 days)

Returns:

  • (Boolean)


130
131
132
# File 'app/models/rails_error_dashboard/error_log.rb', line 130

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

#status_badge_colorObject

Status transition methods



215
216
217
218
219
220
221
222
223
224
# File 'app/models/rails_error_dashboard/error_log.rb', line 215

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

#unmute!Object



176
177
178
# File 'app/models/rails_error_dashboard/error_log.rb', line 176

def unmute!
  Commands::UnmuteError.call(id)
end