Class: RailsErrorDashboard::Services::NotificationThrottler

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

Overview

Pure algorithm: Throttle error notifications to prevent alert fatigue

Checks severity minimum, per-error cooldown, and threshold milestones. Uses in-memory cache (same pattern as BaselineAlertThrottler). Thread-safe via Mutex. Fail-open: returns true on any error.

Constant Summary collapse

SEVERITY_RANK =

Severity levels ranked from lowest to highest

{ low: 0, medium: 1, high: 2, critical: 3 }.freeze

Class Method Summary collapse

Class Method Details

.cleanup!(max_age_hours: 24) ⇒ Object

Remove old entries to prevent memory growth

Parameters:

  • max_age_hours (Integer) (defaults to: 24)

    Remove entries older than this (default: 24)



103
104
105
106
107
108
109
# File 'lib/rails_error_dashboard/services/notification_throttler.rb', line 103

def cleanup!(max_age_hours: 24)
  cutoff_time = max_age_hours.hours.ago

  @mutex.synchronize do
    @last_notification_times.delete_if { |_, time| time < cutoff_time }
  end
end

.clear!Object

Clear all throttle state (for testing)



95
96
97
98
99
# File 'lib/rails_error_dashboard/services/notification_throttler.rb', line 95

def clear!
  @mutex.synchronize do
    @last_notification_times.clear
  end
end

.environment_allowed?(subject = nil) ⇒ Boolean

Does this error's environment pass config.notification_environments?

nil list → everything notifies (the pre-0.11 behaviour). Accepts an ErrorLog, a bare environment name, or nothing (the process environment) — the storm and baseline paths have no row to hand over. A legacy row with a NULL environment is judged as the process environment, so upgrading never silences an error that was notifying before. Fails open: an allowlist bug must not lose a page.

Parameters:

  • subject (ErrorLog, String, nil) (defaults to: nil)

Returns:

  • (Boolean)


43
44
45
46
47
48
49
50
51
52
53
# File 'lib/rails_error_dashboard/services/notification_throttler.rb', line 43

def environment_allowed?(subject = nil)
  allowed = RailsErrorDashboard.configuration.notification_environments
  return true if allowed.nil?

  name = subject.respond_to?(:environment) ? subject.environment : subject
  name = RailsErrorDashboard.configuration.current_environment if name.blank?
  allowed.include?(name.to_s)
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] NotificationThrottler.environment_allowed? failed: #{e.message}")
  true
end

.record_notification(error_log) ⇒ Object

Record that a notification was sent for this error

Parameters:

  • error_log (ErrorLog)

    The error that was notified about



84
85
86
87
88
89
90
91
92
# File 'lib/rails_error_dashboard/services/notification_throttler.rb', line 84

def record_notification(error_log)
  key = error_log.error_hash

  @mutex.synchronize do
    @last_notification_times[key] = Time.current
  end
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] NotificationThrottler.record_notification failed: #{e.message}")
end

.severity_meets_minimum?(error_log) ⇒ Boolean

Does the error's severity meet the configured minimum?

Parameters:

  • error_log (ErrorLog)

    The error to check

Returns:

  • (Boolean)

    true if severity is at or above minimum



58
59
60
61
62
63
64
65
66
67
# File 'lib/rails_error_dashboard/services/notification_throttler.rb', line 58

def severity_meets_minimum?(error_log)
  config = RailsErrorDashboard.configuration
  minimum = config.notification_minimum_severity || :low
  severity = SeverityClassifier.classify(error_log.error_type)

  (SEVERITY_RANK[severity] || 0) >= (SEVERITY_RANK[minimum] || 0)
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] NotificationThrottler.severity_meets_minimum? failed: #{e.message}")
  true
end

.should_notify?(error_log) ⇒ Boolean

Should we send a notification for this error? Checks: severity minimum + cooldown period

Parameters:

  • error_log (ErrorLog)

    The error to check

Returns:

  • (Boolean)

    true if notification should be sent



22
23
24
25
26
27
28
29
30
# File 'lib/rails_error_dashboard/services/notification_throttler.rb', line 22

def should_notify?(error_log)
  return false unless severity_meets_minimum?(error_log)

  cooldown_ok?(error_log)
rescue => e
  # Fail-open: if throttler breaks, allow notification
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] NotificationThrottler.should_notify? failed: #{e.message}")
  true
end

.threshold_reached?(error_log) ⇒ Boolean

Has the error's occurrence count reached a configured threshold milestone?

Parameters:

  • error_log (ErrorLog)

    The error to check

Returns:

  • (Boolean)

    true if occurrence_count matches a threshold



72
73
74
75
76
77
78
79
80
# File 'lib/rails_error_dashboard/services/notification_throttler.rb', line 72

def threshold_reached?(error_log)
  thresholds = RailsErrorDashboard.configuration.notification_threshold_alerts
  return false if thresholds.nil? || thresholds.empty?

  thresholds.include?(error_log.occurrence_count)
rescue => e
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] NotificationThrottler.threshold_reached? failed: #{e.message}")
  false
end