error_radar 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57b3fa6a7576ae08baeb699b180048363846c84ecbb489fc93e16ea537939898
4
- data.tar.gz: 67008613bd52755dad89b7ed1abbc9a86eb92c138f95cae8005f5272e8336594
3
+ metadata.gz: 1522c6f867cba16786c499703bf76e18d8d7134109f8c925387a628fd0b7f420
4
+ data.tar.gz: f9bfc6fe5a5de9e8394b3c81a4b41913b82ed22e5f56ef72980936dc9ecc59f9
5
5
  SHA512:
6
- metadata.gz: 2882f7be2a07a38bdafd4b44c8251774a470f37d44fcd3dc4403046cd2998eb1077ac576c97cd8aed6a43ab4a86153440598bafa5a6a47576f06bbc137a2528d
7
- data.tar.gz: 8717757580e628dc988422df68c3a806bd7d9727f297439b5e7303cc049a48371f37035442dbfd12f7789cb8b480221b093720dda965f6ebeffc2c75ef3bcb57
6
+ metadata.gz: 13ae125f05a92939e320a4af5a55521c4f8bddde93fe702b44a2cc0919f2f47c754ed39f440787cb94040676e22d1efaa5a65195e306c955ca2be3cce70e4e8b
7
+ data.tar.gz: 782931704d0808b5c527a542ab2019b6f49726d9d8b7321ab63931c0788672edaa65eac04a5790f768a35c12296ae12233a38eb60dcfbb668c91535663c5e1cc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.8.0] - 2026-07-03
6
+
7
+ ### Added
8
+ - **Rate-based spike alerts**: add `:spike` to `config.notify_on` to fire a
9
+ notification when a single error exceeds `config.spike_threshold` (default: 10)
10
+ occurrences within `config.spike_window_minutes` (default: 5). Re-alerts once
11
+ per window after the first trigger.
12
+ - **`ErrorRadar::SpikeDetector`**: detects spikes using two strategies — if
13
+ `track_occurrences` is enabled it queries `error_radar_occurrences` for an
14
+ exact cross-process count; otherwise falls back to an in-memory ring buffer
15
+ (per-process, resets on restart). Memory is capped at 1 000 timestamps per
16
+ fingerprint.
17
+ - **`config.spike_threshold`** (default: 10) and **`config.spike_window_minutes`**
18
+ (default: 5) — tune the spike trigger.
19
+ - **Spike throttle**: a spike alert re-fires at most once per window
20
+ (e.g. every 5 minutes), using a separate throttle key from `:critical`/`:all`.
21
+ - **Event-aware notification channels**: all channels (Slack, Discord, email,
22
+ webhook) now receive the notification event (`:new_error`, `:spike`,
23
+ `:recurring`) and format accordingly:
24
+ - Slack: `:warning:` emoji, "danger" button colour, hit-count in title
25
+ - Discord: orange embed colour (`#ff6600`) for spikes
26
+ - Email subject: `[App] SPIKE ErrorClass: N hits in M min`
27
+ - Webhook payload: `event: "spike"` + `spike: { count:, window_minutes: }`
28
+
29
+ ### Changed
30
+ - `Notifier.dispatch` now determines a single *event type* before dispatching,
31
+ preventing a log from matching both `:spike` and `:critical` simultaneously.
32
+ - All channel `deliver` methods accept an optional `event` argument (default
33
+ `:recurring`) — backwards-compatible if called directly.
34
+
5
35
  ## [0.7.0] - 2026-07-03
6
36
 
7
37
  ### Added
@@ -4,14 +4,21 @@ module ErrorRadar
4
4
  class ErrorMailer < ActionMailer::Base
5
5
  layout false
6
6
 
7
- def new_error(error_log)
8
- @error = error_log
9
- @is_new = error_log.new_fingerprint?
10
- @url = build_url
11
- @app_name = ErrorRadar::Notifier.app_name
7
+ def new_error(error_log, event = :recurring)
8
+ @error = error_log
9
+ @event = event
10
+ @spike_data = error_log.instance_variable_get(:@spike_data)
11
+ @url = build_url
12
+ @app_name = ErrorRadar::Notifier.app_name
12
13
 
13
- prefix = @is_new ? 'New' : 'Critical'
14
- subject = "[#{@app_name}] #{prefix} #{@error.severity}: #{@error.error_class}"
14
+ subject = case event
15
+ when :spike
16
+ "[#{@app_name}] SPIKE #{@error.error_class}: #{@spike_data[:count]} hits in #{@spike_data[:window_minutes]} min"
17
+ when :new_error
18
+ "[#{@app_name}] New #{@error.severity}: #{@error.error_class}"
19
+ else
20
+ "[#{@app_name}] #{@error.severity.capitalize}: #{@error.error_class}"
21
+ end
15
22
 
16
23
  mail(
17
24
  to: ErrorRadar.config.email_recipients,
@@ -69,6 +69,13 @@ module ErrorRadar
69
69
  @error_callbacks << block
70
70
  end
71
71
 
72
+ # Spike detection ─────────────────────────────────────────────────────────
73
+ # Number of occurrences within spike_window_minutes that triggers a spike alert.
74
+ # Add :spike to notify_on to enable: config.notify_on = [:new_error, :spike]
75
+ attr_accessor :spike_threshold
76
+ # Rolling time window in minutes for counting occurrences.
77
+ attr_accessor :spike_window_minutes
78
+
72
79
  # Occurrences ─────────────────────────────────────────────────────────────
73
80
  # When true, each individual error hit is stored in error_radar_occurrences.
74
81
  # Requires running: bin/rails generate error_radar:upgrade_v060 && bin/rails db:migrate
@@ -156,6 +163,9 @@ module ErrorRadar
156
163
  @app_host = nil
157
164
  @error_callbacks = []
158
165
 
166
+ @spike_threshold = 10
167
+ @spike_window_minutes = 5
168
+
159
169
  @track_occurrences = false
160
170
  @max_occurrences_per_error = 200
161
171
 
@@ -13,10 +13,10 @@ module ErrorRadar
13
13
  'warning' => 0xfd7e14,
14
14
  'info' => 0x17a2b8
15
15
  }.freeze
16
+ SPIKE_COLOR = 0xff6600
16
17
 
17
- def self.deliver(log)
18
- url = URI(ErrorRadar.config.discord_webhook_url)
19
- payload = build_payload(log)
18
+ def self.deliver(log, event = :recurring)
19
+ url = URI(ErrorRadar.config.discord_webhook_url)
20
20
 
21
21
  http = Net::HTTP.new(url.host, url.port)
22
22
  http.use_ssl = url.scheme == 'https'
@@ -25,17 +25,26 @@ module ErrorRadar
25
25
 
26
26
  req = Net::HTTP::Post.new(url)
27
27
  req['Content-Type'] = 'application/json'
28
- req.body = payload.to_json
28
+ req.body = build_payload(log, event).to_json
29
29
 
30
30
  http.request(req)
31
31
  rescue StandardError => e
32
32
  ErrorRadar::Tracking.warn_internal("Discord notification failed: #{e.message}")
33
33
  end
34
34
 
35
- def self.build_payload(log)
36
- app = ErrorRadar::Notifier.app_name
37
- url = ErrorRadar::Notifier.error_url(log)
38
- prefix = log.new_fingerprint? ? '🆕 New error' : '🔁 Critical error'
35
+ def self.build_payload(log, event)
36
+ spike_data = log.instance_variable_get(:@spike_data)
37
+ app = ErrorRadar::Notifier.app_name
38
+ link = ErrorRadar::Notifier.error_url(log)
39
+
40
+ prefix, color = case event
41
+ when :spike
42
+ ["⚠️ Spike (#{spike_data[:count]} hits / #{spike_data[:window_minutes]} min)", SPIKE_COLOR]
43
+ when :new_error
44
+ ['🆕 New error', SEV_COLORS[log.severity] || 0x6c757d]
45
+ else
46
+ ['🔁 Recurring error', SEV_COLORS[log.severity] || 0x6c757d]
47
+ end
39
48
 
40
49
  fields = [
41
50
  { name: 'Source', value: (log.source || 'unknown').truncate(100), inline: true },
@@ -43,12 +52,12 @@ module ErrorRadar
43
52
  { name: 'Severity', value: log.severity.to_s, inline: true },
44
53
  { name: 'Occurrences', value: log.occurrences.to_s, inline: true }
45
54
  ]
46
- fields << { name: 'View', value: "[Error Radar](#{url})", inline: false } if url
55
+ fields << { name: 'View', value: "[Error Radar](#{link})", inline: false } if link
47
56
 
48
57
  embed = {
49
58
  title: "#{prefix}: #{log.error_class}",
50
59
  description: "```#{log.message.to_s.truncate(300)}```",
51
- color: SEV_COLORS[log.severity] || 0x6c757d,
60
+ color: color,
52
61
  fields: fields,
53
62
  footer: { text: "[#{app}] Error Radar" },
54
63
  timestamp: log.last_seen_at&.iso8601
@@ -5,11 +5,11 @@ module ErrorRadar
5
5
  # Delivers notification emails via ActionMailer (must be configured in
6
6
  # the host app). Uses deliver_later so it doesn't block the request cycle.
7
7
  module Email
8
- def self.deliver(log)
8
+ def self.deliver(log, event = :recurring)
9
9
  return unless defined?(::ActionMailer::Base)
10
10
 
11
11
  require 'error_radar/mailers/error_mailer'
12
- ErrorRadar::ErrorMailer.new_error(log).deliver_later
12
+ ErrorRadar::ErrorMailer.new_error(log, event).deliver_later
13
13
  rescue StandardError => e
14
14
  ErrorRadar::Tracking.warn_internal("Email notification failed: #{e.message}")
15
15
  end
@@ -14,40 +14,47 @@ module ErrorRadar
14
14
  'info' => ':large_blue_circle:'
15
15
  }.freeze
16
16
 
17
- def self.deliver(log)
18
- url = URI(ErrorRadar.config.slack_webhook_url)
19
- payload = build_payload(log)
17
+ def self.deliver(log, event = :recurring)
18
+ url = URI(ErrorRadar.config.slack_webhook_url)
20
19
 
21
20
  http = Net::HTTP.new(url.host, url.port)
22
21
  http.use_ssl = url.scheme == 'https'
23
22
  http.open_timeout = 5
24
23
  http.read_timeout = 5
25
24
 
26
- req = Net::HTTP::Post.new(url)
27
- req['Content-Type'] = 'application/json'
28
- req.body = payload.to_json
25
+ req = Net::HTTP::Post.new(url)
26
+ req['Content-Type'] = 'application/json'
27
+ req.body = build_payload(log, event).to_json
29
28
 
30
29
  http.request(req)
31
30
  rescue StandardError => e
32
31
  ErrorRadar::Tracking.warn_internal("Slack notification failed: #{e.message}")
33
32
  end
34
33
 
35
- def self.build_payload(log)
36
- emoji = SEV_EMOJI[log.severity] || ':white_circle:'
37
- title = "#{emoji} #{log.new_fingerprint? ? 'New error' : 'Critical error'}: *#{log.error_class}*"
38
- url = ErrorRadar::Notifier.error_url(log)
39
- app = ErrorRadar::Notifier.app_name
40
- channel = ErrorRadar.config.slack_channel
34
+ def self.build_payload(log, event)
35
+ spike_data = log.instance_variable_get(:@spike_data)
36
+ app = ErrorRadar::Notifier.app_name
37
+ link = ErrorRadar::Notifier.error_url(log)
38
+ channel = ErrorRadar.config.slack_channel
39
+
40
+ emoji, title = case event
41
+ when :spike
42
+ [':warning:', "Spike — #{spike_data[:count]} hits in #{spike_data[:window_minutes]} min: *#{log.error_class}*"]
43
+ when :new_error
44
+ [SEV_EMOJI[log.severity] || ':white_circle:', "New error: *#{log.error_class}*"]
45
+ else
46
+ [SEV_EMOJI[log.severity] || ':white_circle:', "#{log.severity.capitalize} error: *#{log.error_class}*"]
47
+ end
41
48
 
42
49
  blocks = [
43
- { type: 'section', text: { type: 'mrkdwn', text: "*[#{app}]* #{title}" } },
50
+ { type: 'section', text: { type: 'mrkdwn', text: "#{emoji} *[#{app}]* #{title}" } },
44
51
  {
45
52
  type: 'section',
46
53
  fields: [
47
54
  { type: 'mrkdwn', text: "*Source*\n#{(log.source || 'unknown').truncate(80)}" },
48
55
  { type: 'mrkdwn', text: "*Category*\n#{log.category}" },
49
56
  { type: 'mrkdwn', text: "*Severity*\n#{log.severity}" },
50
- { type: 'mrkdwn', text: "*Occurrences*\n#{log.occurrences}" }
57
+ { type: 'mrkdwn', text: "*Total occurrences*\n#{log.occurrences}" }
51
58
  ]
52
59
  },
53
60
  {
@@ -56,13 +63,14 @@ module ErrorRadar
56
63
  }
57
64
  ]
58
65
 
59
- if url
66
+ if link
60
67
  blocks << {
61
68
  type: 'actions',
62
69
  elements: [{
63
70
  type: 'button',
64
- text: { type: 'plain_text', text: 'View in Error Radar', emoji: true },
65
- url: url
71
+ text: { type: 'plain_text', text: 'View in Error Radar', emoji: true },
72
+ url: link,
73
+ style: event == :spike ? 'danger' : 'primary'
66
74
  }]
67
75
  }
68
76
  end
@@ -9,7 +9,7 @@ module ErrorRadar
9
9
  # Generic outbound webhook. POSTs a JSON payload to any URL.
10
10
  # Useful for PagerDuty, OpsGenie, custom scripts, etc.
11
11
  module Webhook
12
- def self.deliver(log, url:)
12
+ def self.deliver(log, url:, event: :recurring)
13
13
  uri = URI(url)
14
14
 
15
15
  http = Net::HTTP.new(uri.host, uri.port)
@@ -20,16 +20,18 @@ module ErrorRadar
20
20
  req = Net::HTTP::Post.new(uri)
21
21
  req['Content-Type'] = 'application/json'
22
22
  req['User-Agent'] = "ErrorRadar/#{ErrorRadar::VERSION}"
23
- req.body = build_payload(log).to_json
23
+ req.body = build_payload(log, event).to_json
24
24
 
25
25
  http.request(req)
26
26
  rescue StandardError => e
27
27
  ErrorRadar::Tracking.warn_internal("Webhook (#{url}) notification failed: #{e.message}")
28
28
  end
29
29
 
30
- def self.build_payload(log)
31
- {
32
- event: log.new_fingerprint? ? 'new_error' : 'recurring_error',
30
+ def self.build_payload(log, event)
31
+ spike_data = log.instance_variable_get(:@spike_data)
32
+
33
+ payload = {
34
+ event: event.to_s,
33
35
  app: ErrorRadar::Notifier.app_name,
34
36
  error_class: log.error_class,
35
37
  source: log.source,
@@ -42,6 +44,15 @@ module ErrorRadar
42
44
  last_seen_at: log.last_seen_at&.iso8601,
43
45
  url: ErrorRadar::Notifier.error_url(log)
44
46
  }
47
+
48
+ if spike_data
49
+ payload[:spike] = {
50
+ count: spike_data[:count],
51
+ window_minutes: spike_data[:window_minutes]
52
+ }
53
+ end
54
+
55
+ payload
45
56
  end
46
57
  end
47
58
  end
@@ -5,68 +5,105 @@ module ErrorRadar
5
5
  # callbacks) after an ErrorLog is persisted. Called from Tracking.capture and
6
6
  # Tracking.notify. Never raises — a broken notification must not affect the
7
7
  # host application.
8
+ #
9
+ # Notification events:
10
+ # :new_error — first time a fingerprint is seen (no throttle)
11
+ # :spike — error rate exceeds config.spike_threshold in the window
12
+ # :recurring — matches :critical or :all rule, throttled to 1/hour
8
13
  module Notifier
9
- THROTTLE_MUTEX = Mutex.new
10
- # fingerprint => Time of last notification (in-memory, resets on restart)
11
- THROTTLE = {}
14
+ THROTTLE_MUTEX = Mutex.new
15
+ # key => Time of last notification (in-memory, resets on restart)
16
+ # Keys: fingerprint (for :recurring) or "spike:fingerprint" (for :spike)
17
+ THROTTLE = {}
12
18
  THROTTLE_INTERVAL = 3600 # seconds between repeat alerts per fingerprint
13
19
 
14
20
  def self.dispatch(log)
15
21
  return unless ErrorRadar.config.enabled
16
- return unless should_fire?(log)
17
22
 
18
- cfg = ErrorRadar.config
19
- send_slack(log) if cfg.slack_webhook_url.to_s.start_with?('http')
20
- send_discord(log) if cfg.discord_webhook_url.to_s.start_with?('http')
21
- send_email(log) if cfg.email_recipients.any?
22
- cfg.webhook_urls.each { |url| send_webhook(log, url) }
23
- cfg.error_callbacks.each { |cb| safe_call(cb, log) }
23
+ rules = Array(ErrorRadar.config.notify_on).map(&:to_sym)
24
+ return if rules.empty?
25
+
26
+ # Record the hit for in-memory spike tracking before we test the count.
27
+ if rules.include?(:spike)
28
+ require 'error_radar/spike_detector'
29
+ SpikeDetector.record_hit(log.fingerprint)
30
+ end
31
+
32
+ event = determine_event(log, rules)
33
+ return unless event
34
+
35
+ fire_all(log, event)
24
36
  rescue StandardError => e
25
37
  ErrorRadar::Tracking.warn_internal("Notifier.dispatch failed: #{e.message}")
26
38
  end
27
39
 
28
40
  # ── Fire decision ──────────────────────────────────────────────────────
29
- def self.should_fire?(log)
30
- rules = Array(ErrorRadar.config.notify_on).map(&:to_sym)
31
- return false if rules.empty?
32
41
 
33
- # :new_error — fire exactly once per fingerprint (no throttle needed)
34
- return true if rules.include?(:new_error) && log.new_fingerprint?
42
+ def self.determine_event(log, rules)
43
+ # :new_error fires exactly once per fingerprint, no throttle
44
+ return :new_error if rules.include?(:new_error) && log.new_fingerprint?
35
45
 
36
- # :critical / :all fire for recurring events, subject to throttle
37
- matches_severity = rules.include?(:all) ||
38
- (rules.include?(:critical) && log.severity_critical?)
39
- matches_severity && throttle_ok?(log.fingerprint)
46
+ # :spike rate-based alert when occurrences in window exceed threshold
47
+ if rules.include?(:spike)
48
+ spike_count = SpikeDetector.check(log)
49
+ if spike_count
50
+ window_secs = ErrorRadar.config.spike_window_minutes * 60
51
+ if throttle_ok?("spike:#{log.fingerprint}", window_secs)
52
+ log.instance_variable_set(:@spike_data, {
53
+ count: spike_count,
54
+ window_minutes: ErrorRadar.config.spike_window_minutes
55
+ })
56
+ return :spike
57
+ end
58
+ end
59
+ end
60
+
61
+ # :critical / :all — throttled recurring alerts
62
+ matches = rules.include?(:all) ||
63
+ (rules.include?(:critical) && log.severity_critical?)
64
+ return :recurring if matches && throttle_ok?(log.fingerprint, THROTTLE_INTERVAL)
65
+
66
+ nil
40
67
  end
41
68
 
42
- def self.throttle_ok?(fingerprint)
69
+ def self.throttle_ok?(key, interval = THROTTLE_INTERVAL)
43
70
  THROTTLE_MUTEX.synchronize do
44
- last = THROTTLE[fingerprint]
45
- ok = last.nil? || (Time.current - last) >= THROTTLE_INTERVAL
46
- THROTTLE[fingerprint] = Time.current if ok
71
+ last = THROTTLE[key]
72
+ ok = last.nil? || (Time.current - last) >= interval
73
+ THROTTLE[key] = Time.current if ok
47
74
  ok
48
75
  end
49
76
  end
50
77
 
51
78
  # ── Channel dispatchers ────────────────────────────────────────────────
52
- def self.send_slack(log)
79
+
80
+ def self.fire_all(log, event)
81
+ cfg = ErrorRadar.config
82
+ send_slack(log, event) if cfg.slack_webhook_url.to_s.start_with?('http')
83
+ send_discord(log, event) if cfg.discord_webhook_url.to_s.start_with?('http')
84
+ send_email(log, event) if cfg.email_recipients.any?
85
+ cfg.webhook_urls.each { |url| send_webhook(log, url, event) }
86
+ cfg.error_callbacks.each { |cb| safe_call(cb, log) }
87
+ end
88
+
89
+ def self.send_slack(log, event)
53
90
  require 'error_radar/notifications/slack'
54
- Notifications::Slack.deliver(log)
91
+ Notifications::Slack.deliver(log, event)
55
92
  end
56
93
 
57
- def self.send_discord(log)
94
+ def self.send_discord(log, event)
58
95
  require 'error_radar/notifications/discord'
59
- Notifications::Discord.deliver(log)
96
+ Notifications::Discord.deliver(log, event)
60
97
  end
61
98
 
62
- def self.send_email(log)
99
+ def self.send_email(log, event)
63
100
  require 'error_radar/notifications/email'
64
- Notifications::Email.deliver(log)
101
+ Notifications::Email.deliver(log, event)
65
102
  end
66
103
 
67
- def self.send_webhook(log, url)
104
+ def self.send_webhook(log, url, event)
68
105
  require 'error_radar/notifications/webhook'
69
- Notifications::Webhook.deliver(log, url: url)
106
+ Notifications::Webhook.deliver(log, url: url, event: event)
70
107
  end
71
108
 
72
109
  def self.safe_call(cb, log)
@@ -75,7 +112,8 @@ module ErrorRadar
75
112
  ErrorRadar::Tracking.warn_internal("on_error callback failed: #{e.message}")
76
113
  end
77
114
 
78
- # Build a deep-link URL to the error detail page.
115
+ # ── Helpers ───────────────────────────────────────────────────────────
116
+
79
117
  def self.error_url(log)
80
118
  host = ErrorRadar.config.app_host.to_s.chomp('/')
81
119
  return nil if host.empty?
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ # Detects sudden error-rate spikes for a given fingerprint.
5
+ #
6
+ # Strategy (in order of preference):
7
+ # 1. If track_occurrences is on — query error_radar_occurrences for exact
8
+ # count in the window. Accurate across processes and survives restarts.
9
+ # 2. In-memory ring buffer per fingerprint. Fast, zero extra DB queries, but
10
+ # resets on process restart and is per-worker (not shared across Puma workers).
11
+ module SpikeDetector
12
+ MUTEX = Mutex.new
13
+ # fingerprint => Array<Time> of recent hit timestamps (in-memory fallback)
14
+ HITS = Hash.new { |h, k| h[k] = [] }
15
+ # Cap per-key buffer to avoid unbounded growth on very high-volume errors
16
+ MAX_HITS_PER_KEY = 1000
17
+
18
+ # Record a hit in the in-memory ring buffer.
19
+ # Called by Notifier before spike detection so the current hit is counted.
20
+ def self.record_hit(fingerprint)
21
+ MUTEX.synchronize do
22
+ arr = HITS[fingerprint]
23
+ arr << Time.current
24
+ arr.shift while arr.size > MAX_HITS_PER_KEY
25
+ end
26
+ end
27
+
28
+ # Returns the number of recent hits if >= config threshold, false otherwise.
29
+ def self.check(log)
30
+ threshold = ErrorRadar.config.spike_threshold.to_i
31
+ window = ErrorRadar.config.spike_window_minutes.to_i
32
+ return false unless threshold > 0 && window > 0
33
+
34
+ count = recent_count(log, window)
35
+ count >= threshold ? count : false
36
+ end
37
+
38
+ class << self
39
+ private
40
+
41
+ def recent_count(log, window_minutes)
42
+ if ErrorRadar.config.track_occurrences && defined?(ErrorRadar::ErrorOccurrence)
43
+ begin
44
+ return ErrorRadar::ErrorOccurrence
45
+ .where(error_log_id: log.id)
46
+ .where('occurred_at >= ?', window_minutes.minutes.ago)
47
+ .count
48
+ rescue ActiveRecord::StatementInvalid
49
+ # Table not yet created — fall through to in-memory
50
+ end
51
+ end
52
+
53
+ cutoff = window_minutes.minutes.ago
54
+ MUTEX.synchronize do
55
+ (HITS[log.fingerprint] || []).count { |t| t >= cutoff }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErrorRadar
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/error_radar.rb CHANGED
@@ -5,6 +5,7 @@ require 'error_radar/configuration'
5
5
  require 'error_radar/tracking'
6
6
  require 'error_radar/notifier'
7
7
  require 'error_radar/cleanup'
8
+ require 'error_radar/spike_detector'
8
9
  require 'error_radar/middleware'
9
10
  require 'error_radar/server_monitor'
10
11
  require 'error_radar/engine'
@@ -15,10 +15,19 @@ ErrorRadar.configure do |config|
15
15
  config.install_rails_admin = true # register the ErrorLog board (auto, if RailsAdmin present)
16
16
 
17
17
  # --- Notifications ---
18
- # When to fire: :new_error (first occurrence of a fingerprint, default),
19
- # :critical (any critical severity, throttled to 1/hour),
20
- # :all (every occurrence, throttled to 1/hour per fingerprint)
18
+ # When to fire:
19
+ # :new_error first occurrence of a fingerprint (default, no throttle)
20
+ # :critical — any critical-severity occurrence, throttled to 1/hour
21
+ # :all — every occurrence, throttled to 1/hour per fingerprint
22
+ # :spike — rate spike: N hits in M minutes (configure below)
21
23
  config.notify_on = [:new_error]
24
+ # config.notify_on = [:new_error, :spike]
25
+
26
+ # --- Spike detection (requires :spike in notify_on) ---
27
+ # Fire an alert when a single error hits >= spike_threshold times within
28
+ # spike_window_minutes. Re-alerts once per window after that.
29
+ # config.spike_threshold = 10 # occurrences …
30
+ # config.spike_window_minutes = 5 # … in this many minutes
22
31
 
23
32
  # Slack incoming webhook (https://api.slack.com/messaging/webhooks)
24
33
  # config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: error_radar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chienbn9x
@@ -92,6 +92,7 @@ files:
92
92
  - lib/error_radar/notifications/webhook.rb
93
93
  - lib/error_radar/notifier.rb
94
94
  - lib/error_radar/server_monitor.rb
95
+ - lib/error_radar/spike_detector.rb
95
96
  - lib/error_radar/tracking.rb
96
97
  - lib/error_radar/version.rb
97
98
  - lib/generators/error_radar/install/install_generator.rb