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

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

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

Override user association to use configured user model



360
361
362
363
364
365
366
# File 'app/models/rails_error_dashboard/error_log.rb', line 360

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



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

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)



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

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



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

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)


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

def assigned?
  assigned_to.present?
end

#backtrace_framesObject

Extract backtrace frames for similarity comparison



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'app/models/rails_error_dashboard/error_log.rb', line 221

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



310
311
312
313
314
315
316
317
318
319
320
321
# File 'app/models/rails_error_dashboard/error_log.rb', line 310

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



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

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



255
256
257
# File 'app/models/rails_error_dashboard/error_log.rb', line 255

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

#can_transition_to?(new_status) ⇒ Boolean

Returns:

  • (Boolean)


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

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



274
275
276
277
278
279
280
281
282
283
284
285
# File 'app/models/rails_error_dashboard/error_log.rb', line 274

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)


107
108
109
# File 'app/models/rails_error_dashboard/error_log.rb', line 107

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



344
345
346
347
348
349
350
351
352
353
354
355
# File 'app/models/rails_error_dashboard/error_log.rb', line 344

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



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

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



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

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



326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'app/models/rails_error_dashboard/error_log.rb', line 326

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



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

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

  priority_data[:color]
end

#priority_emojiObject



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

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

  priority_data[:emoji]
end

#priority_labelObject

Priority methods



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

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)


112
113
114
# File 'app/models/rails_error_dashboard/error_log.rb', line 112

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

Find related errors of the same type



213
214
215
216
217
218
# File 'app/models/rails_error_dashboard/error_log.rb', line 213

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

#resolve!(resolution_data = {}) ⇒ Object

Mark error as resolved (delegates to Command)



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

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

#set_defaultsObject



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

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

#set_priority_scoreObject



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

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

#set_tracking_fieldsObject



82
83
84
85
86
87
# File 'app/models/rails_error_dashboard/error_log.rb', line 82

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



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

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



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

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)


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

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

#stale?Boolean

Check if error is old unresolved (> 7 days)

Returns:

  • (Boolean)


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

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

#status_badge_colorObject

Status transition methods



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

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