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



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

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



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

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)



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

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



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

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)


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

def assigned?
  assigned_to.present?
end

#backtrace_framesObject

Extract backtrace frames for similarity comparison



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
280
# File 'app/models/rails_error_dashboard/error_log.rb', line 249

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



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

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



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

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



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

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

#can_transition_to?(new_status) ⇒ Boolean

Returns:

  • (Boolean)


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

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



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

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)


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

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



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

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



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

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



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

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



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

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

#muted?Boolean

Mute query — checks column existence for backward compatibility

Returns:

  • (Boolean)


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

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



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

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



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

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

  priority_data[:color]
end

#priority_emojiObject



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

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

  priority_data[:emoji]
end

#priority_labelObject

Priority methods



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

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)


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

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

Find related errors of the same type



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

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)



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

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

#set_defaultsObject



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

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

#set_priority_scoreObject



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

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

#set_tracking_fieldsObject



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

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



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

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



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

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)


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

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

#stale?Boolean

Check if error is old unresolved (> 7 days)

Returns:

  • (Boolean)


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

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

#status_badge_colorObject

Status transition methods



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

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



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

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