Class: RailsErrorDashboard::ErrorLog
- Inherits:
-
ErrorLogsRecord
- Object
- ActiveRecord::Base
- ErrorLogsRecord
- RailsErrorDashboard::ErrorLog
- 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
-
#just_reopened ⇒ Object
Transient flag: set to true when a resolved/wont_fix error is reopened by FindOrIncrementError.
Class Method Summary collapse
-
.belongs_to(*args, **options) ⇒ Object
Override user association to use configured user model.
-
.find_or_increment_by_hash(error_hash, attributes = {}) ⇒ Object
Find existing error by hash or create new one — delegates to Command.
-
.log_error(exception, context = {}) ⇒ Object
Log an error with context (delegates to Command).
-
.priority_options(include_emoji: false) ⇒ Object
Class method to get priority options for select dropdowns.
Instance Method Summary collapse
-
#assigned? ⇒ Boolean
Assignment query.
-
#backtrace_frames ⇒ Object
Extract backtrace frames for similarity comparison.
-
#baseline_anomaly(sensitivity: 2) ⇒ Hash
Check if this error is anomalous compared to baseline.
-
#baselines ⇒ Hash
Get baseline statistics for this error type.
-
#calculate_backtrace_signature ⇒ Object
Calculate backtrace signature — delegates to Service.
- #can_transition_to?(new_status) ⇒ Boolean
-
#co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 10) ⇒ Array<Hash>
Find errors that occur together in time.
-
#critical? ⇒ Boolean
Check if this is a critical error — delegates to SeverityClassifier.
-
#error_bursts(days: 7) ⇒ Array<Hash>
Detect error bursts (many errors in short time).
-
#error_cascades(min_probability: 0.5) ⇒ Hash
Find cascade patterns (what causes this error, what this error causes).
-
#generate_error_hash ⇒ Object
Generate unique hash for error grouping — delegates to ErrorHashGenerator Service.
-
#mute!(mute_data = {}) ⇒ Object
Mute/unmute convenience methods — delegate to Commands.
-
#muted? ⇒ Boolean
Mute query — checks column existence for backward compatibility.
-
#occurrence_pattern(days: 30) ⇒ Hash
Detect cyclical occurrence patterns (daily/weekly rhythms).
- #priority_color ⇒ Object
- #priority_emoji ⇒ Object
-
#priority_label ⇒ Object
Priority methods.
-
#recent? ⇒ Boolean
Check if error is recent (< 1 hour).
-
#related_errors(limit: 5, application_id: nil) ⇒ Object
Find related errors of the same type.
-
#reopened? ⇒ Boolean
Was this error previously resolved and then reopened due to recurrence? Uses the persisted
reopened_atcolumn (set by FindOrIncrementError). -
#resolve!(resolution_data = {}) ⇒ Object
Mark error as resolved (delegates to Command).
- #set_defaults ⇒ Object
- #set_priority_score ⇒ Object
- #set_tracking_fields ⇒ Object
-
#severity ⇒ Object
Get severity level — delegates to SeverityClassifier.
-
#similar_errors(threshold: 0.6, limit: 10) ⇒ Array<Hash>
Find similar errors using fuzzy matching.
-
#snoozed? ⇒ Boolean
Snooze query.
-
#stale? ⇒ Boolean
Check if error is old unresolved (> 7 days).
-
#status_badge_color ⇒ Object
Status transition methods.
- #unmute! ⇒ Object
Instance Attribute Details
#just_reopened ⇒ Object
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, **) if args.first == :user user_model = RailsErrorDashboard.configuration.user_model [: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.(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
158 159 160 |
# File 'app/models/rails_error_dashboard/error_log.rb', line 158 def assigned? assigned_to.present? end |
#backtrace_frames ⇒ Object
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
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 |
#baselines ⇒ Hash
Get baseline statistics for this error type
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_signature ⇒ Object
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
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
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
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)
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) = 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: ) end |
#error_cascades(min_probability: 0.5) ⇒ Hash
Find cascade patterns (what causes this error, what this error causes)
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_hash ⇒ Object
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: , 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
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)
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) = self.class .where(error_type: error_type, platform: platform) .where("occurred_at >= ?", days.days.ago) .pluck(:occurred_at) Services::PatternDetector.analyze_cyclical_pattern( timestamps: , days: days ) end |
#priority_color ⇒ Object
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_emoji ⇒ Object
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_label ⇒ Object
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)
126 127 128 |
# File 'app/models/rails_error_dashboard/error_log.rb', line 126 def recent? occurred_at >= 1.hour.ago end |
#related_errors(limit: 5, application_id: nil) ⇒ Object
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 (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).
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_defaults ⇒ Object
92 93 94 |
# File 'app/models/rails_error_dashboard/error_log.rb', line 92 def set_defaults self.platform ||= "API" end |
#set_priority_score ⇒ Object
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_fields ⇒ Object
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 |
#severity ⇒ Object
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
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
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)
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_color ⇒ Object
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 |