Module: ErrorRadar::Notifier

Defined in:
lib/error_radar/notifier.rb

Overview

Dispatches alert notifications (Slack, Discord, email, webhooks, custom callbacks) after an ErrorLog is persisted. Called from Tracking.capture and Tracking.notify. Never raises — a broken notification must not affect the host application.

Notification events:

:new_error  — first time a fingerprint is seen (no throttle)
:spike      — error rate exceeds config.spike_threshold in the window
:recurring  — matches :critical or :all rule, throttled to 1/hour

Constant Summary collapse

THROTTLE_MUTEX =
Mutex.new
THROTTLE =

key => Time of last notification (in-memory, resets on restart) Keys: fingerprint (for :recurring) or "spike:fingerprint" (for :spike)

{}
THROTTLE_INTERVAL =

seconds between repeat alerts per fingerprint

3600

Class Method Summary collapse

Class Method Details

.app_nameObject



124
125
126
127
# File 'lib/error_radar/notifier.rb', line 124

def self.app_name
  ErrorRadar.config.app_name ||
    (defined?(Rails) && Rails.application ? Rails.application.class.module_parent_name : 'App')
end

.determine_event(log, rules) ⇒ Object

── Fire decision ──────────────────────────────────────────────────────



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
# File 'lib/error_radar/notifier.rb', line 42

def self.determine_event(log, rules)
  # :new_error fires exactly once per fingerprint, no throttle
  return :new_error if rules.include?(:new_error) && log.new_fingerprint?

  # :spike — rate-based alert when occurrences in window exceed threshold
  if rules.include?(:spike)
    spike_count = SpikeDetector.check(log)
    if spike_count
      window_secs = ErrorRadar.config.spike_window_minutes * 60
      if throttle_ok?("spike:#{log.fingerprint}", window_secs)
        log.instance_variable_set(:@spike_data, {
          count:          spike_count,
          window_minutes: ErrorRadar.config.spike_window_minutes
        })
        return :spike
      end
    end
  end

  # :critical / :all — throttled recurring alerts
  matches = rules.include?(:all) ||
            (rules.include?(:critical) && log.severity_critical?)
  return :recurring if matches && throttle_ok?(log.fingerprint, THROTTLE_INTERVAL)

  nil
end

.dispatch(log) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/error_radar/notifier.rb', line 20

def self.dispatch(log)
  return unless ErrorRadar.config.enabled

  rules = Array(ErrorRadar.config.notify_on).map(&:to_sym)
  return if rules.empty?

  # Record the hit for in-memory spike tracking before we test the count.
  if rules.include?(:spike)
    require 'error_radar/spike_detector'
    SpikeDetector.record_hit(log.fingerprint)
  end

  event = determine_event(log, rules)
  return unless event

  fire_all(log, event)
rescue StandardError => e
  ErrorRadar::Tracking.warn_internal("Notifier.dispatch failed: #{e.message}")
end

.error_url(log) ⇒ Object

── Helpers ───────────────────────────────────────────────────────────



117
118
119
120
121
122
# File 'lib/error_radar/notifier.rb', line 117

def self.error_url(log)
  host = ErrorRadar.config.app_host.to_s.chomp('/')
  return nil if host.empty?

  "#{host}/error_radar/errors/#{log.id}"
end

.fire_all(log, event) ⇒ Object

── Channel dispatchers ────────────────────────────────────────────────



80
81
82
83
84
85
86
87
# File 'lib/error_radar/notifier.rb', line 80

def self.fire_all(log, event)
  cfg = ErrorRadar.config
  send_slack(log, event)                     if cfg.slack_webhook_url.to_s.start_with?('http')
  send_discord(log, event)                   if cfg.discord_webhook_url.to_s.start_with?('http')
  send_email(log, event)                     if cfg.email_recipients.any?
  cfg.webhook_urls.each { |url| send_webhook(log, url, event) }
  cfg.error_callbacks.each { |cb| safe_call(cb, log) }
end

.safe_call(cb, log) ⇒ Object



109
110
111
112
113
# File 'lib/error_radar/notifier.rb', line 109

def self.safe_call(cb, log)
  cb.call(log)
rescue StandardError => e
  ErrorRadar::Tracking.warn_internal("on_error callback failed: #{e.message}")
end

.send_discord(log, event) ⇒ Object



94
95
96
97
# File 'lib/error_radar/notifier.rb', line 94

def self.send_discord(log, event)
  require 'error_radar/notifications/discord'
  Notifications::Discord.deliver(log, event)
end

.send_email(log, event) ⇒ Object



99
100
101
102
# File 'lib/error_radar/notifier.rb', line 99

def self.send_email(log, event)
  require 'error_radar/notifications/email'
  Notifications::Email.deliver(log, event)
end

.send_slack(log, event) ⇒ Object



89
90
91
92
# File 'lib/error_radar/notifier.rb', line 89

def self.send_slack(log, event)
  require 'error_radar/notifications/slack'
  Notifications::Slack.deliver(log, event)
end

.send_webhook(log, url, event) ⇒ Object



104
105
106
107
# File 'lib/error_radar/notifier.rb', line 104

def self.send_webhook(log, url, event)
  require 'error_radar/notifications/webhook'
  Notifications::Webhook.deliver(log, url: url, event: event)
end

.throttle_ok?(key, interval = THROTTLE_INTERVAL) ⇒ Boolean

Returns:

  • (Boolean)


69
70
71
72
73
74
75
76
# File 'lib/error_radar/notifier.rb', line 69

def self.throttle_ok?(key, interval = THROTTLE_INTERVAL)
  THROTTLE_MUTEX.synchronize do
    last = THROTTLE[key]
    ok   = last.nil? || (Time.current - last) >= interval
    THROTTLE[key] = Time.current if ok
    ok
  end
end