Class: Tina4::ErrorTracker

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/dev_admin.rb

Overview

Thread-safe, file-persisted error tracker for the dev dashboard Error Tracker panel.

Errors are stored in a JSON file in the system temp directory keyed by project path, so they survive across requests and server restarts. Duplicate errors (same type + message + file + line) are de-duplicated —the count increments and the entry is re-opened if it was resolved.

Instance Method Summary collapse

Constructor Details

#initializeErrorTracker

Returns a new instance of ErrorTracker.



122
123
124
125
126
127
128
129
130
# File 'lib/tina4/dev_admin.rb', line 122

def initialize
  @mutex  = Mutex.new
  @errors = nil  # lazy-loaded
  @registered = false
  @store_path = File.join(
    Dir.tmpdir,
    "tina4_dev_errors_#{Digest::MD5.hexdigest(Dir.pwd)}.json"
  )
end

Instance Method Details

#capture(error_type:, message:, traceback: "", file: "", line: 0) ⇒ Object

Capture a Ruby error / exception into the tracker.

Parameters:

  • error_type (String)

    e.g. “RuntimeError” or “NoMethodError”

  • message (String)

    exception message

  • traceback (String) (defaults to: "")

    formatted backtrace (optional)

  • file (String) (defaults to: "")

    source file (optional)

  • line (Integer) (defaults to: 0)

    source line (optional)



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/tina4/dev_admin.rb', line 138

def capture(error_type:, message:, traceback: "", file: "", line: 0)
  @mutex.synchronize do
    load_unlocked
    fingerprint = Digest::MD5.hexdigest("#{error_type}|#{message}|#{file}|#{line}")
    now = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")

    if @errors.key?(fingerprint)
      @errors[fingerprint][:count]    += 1
      @errors[fingerprint][:last_seen] = now
      @errors[fingerprint][:resolved]  = false  # re-open resolved duplicates
    else
      @errors[fingerprint] = {
        id:          fingerprint,
        error_type:  error_type,
        message:     message,
        traceback:   traceback,
        file:        file,
        line:        line,
        first_seen:  now,
        last_seen:   now,
        count:       1,
        resolved:    false
      }
    end
    save_unlocked
  end
end

#capture_exception(exc) ⇒ Object

Capture a Ruby exception object directly.



167
168
169
170
171
172
173
174
175
# File 'lib/tina4/dev_admin.rb', line 167

def capture_exception(exc)
  capture(
    error_type: exc.class.name,
    message:    exc.message,
    traceback:  (exc.backtrace || []).first(20).join("\n"),
    file:       (exc.backtrace_locations&.first&.path || ""),
    line:       (exc.backtrace_locations&.first&.lineno || 0)
  )
end

#clear_allObject

Remove ALL errors.



229
230
231
232
233
234
# File 'lib/tina4/dev_admin.rb', line 229

def clear_all
  @mutex.synchronize do
    @errors = {}
    save_unlocked
  end
end

#clear_resolvedObject

Remove all resolved errors.



220
221
222
223
224
225
226
# File 'lib/tina4/dev_admin.rb', line 220

def clear_resolved
  @mutex.synchronize do
    load_unlocked
    @errors.reject! { |_, e| e[:resolved] }
    save_unlocked
  end
end

#get(include_resolved: true) ⇒ Object

Return all errors (newest first).

Parameters:

  • include_resolved (Boolean) (defaults to: true)


179
180
181
182
183
184
185
186
# File 'lib/tina4/dev_admin.rb', line 179

def get(include_resolved: true)
  @mutex.synchronize do
    load_unlocked
    entries = @errors.values
    entries = entries.reject { |e| e[:resolved] } unless include_resolved
    entries.sort_by { |e| e[:last_seen] }.reverse
  end
end

#healthObject

Health summary (matches Python BrokenTracker interface).



197
198
199
200
201
202
203
204
205
# File 'lib/tina4/dev_admin.rb', line 197

def health
  @mutex.synchronize do
    load_unlocked
    total    = @errors.size
    resolved = @errors.count { |_, e| e[:resolved] }
    unresolved = total - resolved
    { total: total, unresolved: unresolved, resolved: resolved, healthy: unresolved.zero? }
  end
end

#registerObject

Register Ruby error handlers to feed the tracker. Installs an at_exit hook that captures unhandled exceptions. Safe to call multiple times — only registers once.



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/tina4/dev_admin.rb', line 239

def register
  return if @registered

  @registered = true
  tracker = self
  at_exit do
    if (exc = $!) && !exc.is_a?(SystemExit)
      tracker.capture_exception(exc)
    end
  end
end

#reset!Object

Reset (for testing).



252
253
254
255
256
257
258
# File 'lib/tina4/dev_admin.rb', line 252

def reset!
  @mutex.synchronize do
    @errors = {}
    @registered = false
    File.delete(@store_path) if File.exist?(@store_path)
  end
end

#resolve(id) ⇒ Object

Mark a single error as resolved.



208
209
210
211
212
213
214
215
216
217
# File 'lib/tina4/dev_admin.rb', line 208

def resolve(id)
  @mutex.synchronize do
    load_unlocked
    return false unless @errors.key?(id)

    @errors[id][:resolved] = true
    save_unlocked
    true
  end
end

#unresolved_countObject

Count of unresolved errors.



189
190
191
192
193
194
# File 'lib/tina4/dev_admin.rb', line 189

def unresolved_count
  @mutex.synchronize do
    load_unlocked
    @errors.count { |_, e| !e[:resolved] }
  end
end