Class: RailsErrorDashboard::Services::CrashCapture

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_error_dashboard/services/crash_capture.rb

Overview

Last-resort crash capture via Ruby’s ‘at_exit` hook.

When the Rails process dies from an unhandled exception, the error never reaches the error subscriber or middleware. This service registers an ‘at_exit` hook that captures `$!` (the fatal exception) and writes it to disk as JSON. On the next boot, `import!` reads crash files and creates ErrorLog records with severity “fatal”.

Safety contract:

  • Default OFF (opt-in via config.enable_crash_capture)

  • Writes to tmpfile, NOT the database (connection pool may be closed)

  • Timeout: 1 second max for file write, then give up

  • Skips clean exits (SystemExit.success?, SignalException)

  • Every operation wrapped in rescue (crash capture must never itself crash)

  • Zero runtime overhead — hook only fires during process shutdown

Constant Summary collapse

FILE_PREFIX =
"red_crash_"

Class Method Summary collapse

Class Method Details

.capture!(exception) ⇒ Object

Capture a fatal exception to disk. Called from the ‘at_exit` hook.

Parameters:

  • exception (Exception, nil)

    the fatal exception ($!)



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/rails_error_dashboard/services/crash_capture.rb', line 54

def capture!(exception)
  return unless @enabled
  return unless exception
  return if exception.is_a?(SystemExit) && exception.success?
  return if exception.is_a?(SignalException)

  crash_data = build_crash_data(exception)
  path = crash_file_path

  Timeout.timeout(1) do
    FileUtils.mkdir_p(File.dirname(path))
    File.write(path, JSON.generate(crash_data))
  end
rescue => e
  # Crash capture must NEVER itself crash the exit.
  # Best-effort stderr warning (may not be visible).
  $stderr.puts "[RailsErrorDashboard] CrashCapture.capture! failed: #{e.class} - #{e.message}" rescue nil
end

.disable!Object

Disable crash capture. The ‘at_exit` hook remains registered but will no-op because `@enabled` is false.



43
44
45
# File 'lib/rails_error_dashboard/services/crash_capture.rb', line 43

def disable!
  @enabled = false
end

.enable!true

Enable crash capture. Registers the ‘at_exit` hook and records boot time.

Returns:

  • (true)


30
31
32
33
34
35
36
37
38
39
# File 'lib/rails_error_dashboard/services/crash_capture.rb', line 30

def enable!
  return true if enabled?

  @boot_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  @enabled = true

  at_exit { capture!($!) }

  true
end

.enabled?Boolean

Returns whether crash capture is enabled.

Returns:

  • (Boolean)

    whether crash capture is enabled



48
49
50
# File 'lib/rails_error_dashboard/services/crash_capture.rb', line 48

def enabled?
  @enabled == true
end

.import!Object

Import crash files from disk into the database. Called during boot (config.after_initialize) BEFORE enable! so old crashes are processed first.



75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/rails_error_dashboard/services/crash_capture.rb', line 75

def import!
  dir = crash_capture_dir
  return unless Dir.exist?(dir)

  pattern = File.join(dir, "#{FILE_PREFIX}*.json")
  Dir.glob(pattern).each do |file|
    import_crash_file(file)
  end
rescue => e
  RailsErrorDashboard::Logger.debug(
    "[RailsErrorDashboard] CrashCapture.import! failed: #{e.class} - #{e.message}"
  )
end

.reset!Object

Reset internal state (for testing)



90
91
92
93
# File 'lib/rails_error_dashboard/services/crash_capture.rb', line 90

def reset!
  @enabled = false
  @boot_time = nil
end