Class: RailsErrorDashboard::ErrorReporter

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

Instance Method Summary collapse

Instance Method Details

#report(error, handled:, severity:, context:, source: nil) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/rails_error_dashboard/error_reporter.rb', line 21

def report(error, handled:, severity:, context:, source: nil)
  # Skip low-severity warnings
  return if handled && severity == :warning

  # Prevent recursive error capture (issue #114).
  # If LogError.call itself triggers a new error (e.g., Redis down causes
  # perform_later to fail), Rails.error.report fires again for that failure.
  # Without this guard, each cycle double-escapes JSON → exponential payload growth.
  return if Thread.current[:rails_error_dashboard_logging]

  # CRITICAL: Wrap entire process in rescue to ensure failures don't break the app
  begin
    Thread.current[:rails_error_dashboard_logging] = true

    # Enrich context with request data from Thread.current when available.
    # Rails internals (ActionDispatch::Executor) report errors with
    # source: "application.action_dispatch" but pass NO request object,
    # resulting in placeholder values ("Rails Application", "{}", etc.).
    # Our middleware stores the Rack env in Thread.current so we can
    # build a proper request here — fixing issue #106.
    if context[:request].nil? && Thread.current[:rails_error_dashboard_request_env]
      env = Thread.current[:rails_error_dashboard_request_env]
      context = context.merge(request: ActionDispatch::Request.new(env))
    end

    # Skip duplicate reports from our own middleware when the subscriber
    # already captured this error with full request context above.
    # Without this, async logging enqueues two jobs for one exception —
    # and non-deterministic job ordering can overwrite good data.
    if source == "rack.middleware" &&
       Thread.current[:rails_error_dashboard_reported_errors]&.include?(error.object_id)
      return
    end

    # Track that we've reported this error (for dedup with middleware)
    if Thread.current[:rails_error_dashboard_request_env]
      Thread.current[:rails_error_dashboard_reported_errors] ||= Set.new
      Thread.current[:rails_error_dashboard_reported_errors].add(error.object_id)
    end

    # Extract context information
    error_context = ValueObjects::ErrorContext.new(context, source)

    # Log to our error dashboard using Command
    Commands::LogError.call(error, error_context.to_h.merge(source: source))
  rescue => e
    # Don't let error logging cause more errors - fail silently
    # Log failure for debugging but NEVER propagate exception
    RailsErrorDashboard::Logger.error("[RailsErrorDashboard] ErrorReporter failed: #{e.class} - #{e.message}")
    RailsErrorDashboard::Logger.error("Original error: #{error.class} - #{error.message}") if error
    RailsErrorDashboard::Logger.error("Context: #{context.inspect.truncate(500)}") if context
    RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
    nil # Explicitly return nil, never raise
  ensure
    Thread.current[:rails_error_dashboard_logging] = nil
  end
end