Class: Findbug::ErrorEvent

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/findbug/error_event.rb

Overview

ErrorEvent stores captured exceptions in the database.

DATABASE SCHEMA

This model expects a table created by the install generator:

create_table :findbug_error_events do |t|
  t.string :fingerprint, null: false
  t.string :exception_class, null: false
  t.text :message
  t.text :backtrace
  t.jsonb :context, default: {}
  t.jsonb :request_data, default: {}
  t.string :environment
  t.string :release_version
  t.string :severity, default: 'error'
  t.string :source
  t.boolean :handled, default: false
  t.integer :occurrence_count, default: 1
  t.datetime :first_seen_at
  t.datetime :last_seen_at
  t.string :status, default: 'unresolved'
  t.timestamps
end

WHY OVERRIDE JSON ACCESSORS?

The column type for context/request_data varies by adapter:

PostgreSQL  jsonb  (AR returns Hash natively)
MySQL       json   (AR returns Hash natively)
SQLite      text   (AR returns raw JSON String)

The overrides below normalise both cases so callers always get a Hash.

Constant Summary collapse

STATUS_UNRESOLVED =

Statuses

"unresolved"
STATUS_RESOLVED =
"resolved"
STATUS_IGNORED =
"ignored"
SEVERITY_ERROR =

Severities

"error"
SEVERITY_WARNING =
"warning"
SEVERITY_INFO =
"info"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.merge_contexts(old_context, new_context) ⇒ Object



211
212
213
214
215
216
217
# File 'app/models/findbug/error_event.rb', line 211

def self.merge_contexts(old_context, new_context)
  return new_context if old_context.blank?
  return old_context if new_context.blank?

  # Deep merge, preferring new values
  old_context.deep_merge(new_context)
end

.serialize_backtrace(backtrace) ⇒ Object



219
220
221
222
223
# File 'app/models/findbug/error_event.rb', line 219

def self.serialize_backtrace(backtrace)
  return nil unless backtrace

  backtrace.is_a?(Array) ? backtrace.to_json : backtrace
end

.upsert_from_event(event_data) ⇒ ErrorEvent

Find or create an error event, incrementing count if exists

UPSERT PATTERN

We use “upsert” logic: if an error with this fingerprint exists, we update it (increment count, update last_seen_at). Otherwise, we create a new record.

This groups similar errors together instead of creating thousands of duplicate records.

Parameters:

  • event_data (Hash)

    the error event data from Redis

Returns:

  • (ErrorEvent)

    the created or updated error event



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'app/models/findbug/error_event.rb', line 115

def self.upsert_from_event(event_data)
  fingerprint = event_data[:fingerprint]

  # Use database-level locking to prevent race conditions
  transaction do
    existing = find_by(fingerprint: fingerprint)

    if existing
      # Update existing error
      existing.occurrence_count += 1
      existing.last_seen_at = Time.current

      # Update context with latest (might have new info)
      existing.context = merge_contexts(existing.context, event_data[:context])

      # If it was resolved but happened again, reopen it
      if existing.status == STATUS_RESOLVED
        existing.status = STATUS_UNRESOLVED
      end

      existing.save!
      existing
    else
      # Create new error
      create!(
        fingerprint: fingerprint,
        exception_class: event_data[:exception_class],
        message: event_data[:message],
        backtrace: serialize_backtrace(event_data[:backtrace]),
        context: event_data[:context] || {},
        request_data: event_data[:context]&.dig(:request) || {},
        environment: event_data[:environment],
        release_version: event_data[:release],
        severity: event_data[:severity] || SEVERITY_ERROR,
        source: event_data[:source],
        handled: event_data[:handled] || false,
        occurrence_count: 1,
        first_seen_at: Time.current,
        last_seen_at: Time.current,
        status: STATUS_UNRESOLVED
      )
    end
  end
end

Instance Method Details

#backtrace_linesObject

Get parsed backtrace as array



176
177
178
179
180
181
182
# File 'app/models/findbug/error_event.rb', line 176

def backtrace_lines
  return [] unless backtrace

  backtrace.is_a?(Array) ? backtrace : JSON.parse(backtrace)
rescue JSON::ParserError
  backtrace.to_s.split("\n")
end

Get breadcrumbs from context



195
196
197
# File 'app/models/findbug/error_event.rb', line 195

def breadcrumbs
  context&.dig("breadcrumbs") || context&.dig(:breadcrumbs) || []
end

#ignore!Object

Mark this error as ignored



166
167
168
# File 'app/models/findbug/error_event.rb', line 166

def ignore!
  update!(status: STATUS_IGNORED)
end

#reopen!Object

Reopen a resolved/ignored error



171
172
173
# File 'app/models/findbug/error_event.rb', line 171

def reopen!
  update!(status: STATUS_UNRESOLVED)
end

#requestObject

Get request info from context



190
191
192
# File 'app/models/findbug/error_event.rb', line 190

def request
  context&.dig("request") || context&.dig(:request)
end

#resolve!Object

Mark this error as resolved



161
162
163
# File 'app/models/findbug/error_event.rb', line 161

def resolve!
  update!(status: STATUS_RESOLVED)
end

#summaryObject

Short summary for lists



205
206
207
# File 'app/models/findbug/error_event.rb', line 205

def summary
  "#{exception_class}: #{message&.truncate(100)}"
end

#tagsObject

Get tags from context



200
201
202
# File 'app/models/findbug/error_event.rb', line 200

def tags
  context&.dig("tags") || context&.dig(:tags) || {}
end

#userObject

Get user info from context



185
186
187
# File 'app/models/findbug/error_event.rb', line 185

def user
  context&.dig("user") || context&.dig(:user)
end