error_radar 0.8.0 → 0.9.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: 1522c6f867cba16786c499703bf76e18d8d7134109f8c925387a628fd0b7f420
4
- data.tar.gz: f9bfc6fe5a5de9e8394b3c81a4b41913b82ed22e5f56ef72980936dc9ecc59f9
3
+ metadata.gz: e3337550bf775b5f8898cd083bf92b679e913a68166e03064614a16949808bbd
4
+ data.tar.gz: dd37cf03eaa472383509efb76aa77e9760dcbb38a06bf9eb380b73080888671f
5
5
  SHA512:
6
- metadata.gz: 13ae125f05a92939e320a4af5a55521c4f8bddde93fe702b44a2cc0919f2f47c754ed39f440787cb94040676e22d1efaa5a65195e306c955ca2be3cce70e4e8b
7
- data.tar.gz: 782931704d0808b5c527a542ab2019b6f49726d9d8b7321ab63931c0788672edaa65eac04a5790f768a35c12296ae12233a38eb60dcfbb668c91535663c5e1cc
6
+ metadata.gz: 872ec9263dffcdb9c4df391c4259577a0a15805a54cc9e0f8116f0942bee6b05e289b072764e950028dc4fc5dfb786831e864c86d7284f2f88d1b4b71bb20c93
7
+ data.tar.gz: 7e23681c9968c2b2248e3f7ee93e487f2570cce77139acff21e7e1f6f5d2236c58cc09506fff434dd6cec7401cdce5cc26bd9af3321aa5cc43ad3371e12f12b9
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.9.0] - 2026-07-03
6
+
7
+ ### Added
8
+ - **Digest email**: a periodic summary email (HTML + plain text) with:
9
+ - Summary row: new errors, open + in progress, resolved this period, total in DB
10
+ - Unresolved-by-severity bar chart (inline table)
11
+ - Top 10 unresolved errors (linked to detail page when `app_host` is set)
12
+ - New errors first seen in the period
13
+ - Unresolved-by-category breakdown
14
+ - "Open Error Radar →" button
15
+ - **`config.digest_enabled`** (default: `false`) — gates delivery; set `true`
16
+ then schedule the rake task.
17
+ - **`config.digest_recipients`** — separate recipient list for digests; falls
18
+ back to `config.email_recipients` if empty.
19
+ - **`ErrorRadar::Digest.deliver`** — programmatic delivery:
20
+ `ErrorRadar::Digest.deliver(since: 24.hours.ago, period: :daily)`
21
+ - **Rake tasks**:
22
+ - `rake error_radar:digest` — last 24 hours (daily digest)
23
+ - `rake error_radar:digest:weekly` — last 7 days (weekly digest)
24
+ - Both accept `SINCE="2024-01-01 08:00"` env override for custom windows
25
+ - **`DigestMailer`** (`app/mailers/error_radar/digest_mailer.rb`) —
26
+ ActionMailer class with HTML + text templates.
27
+
28
+ ### Notes
29
+ - Requires ActionMailer configured in the host app (same requirement as the
30
+ existing `new_error` notification email).
31
+ - Scheduling is left to the host app's cron/scheduler — no in-process scheduler
32
+ dependency. Heroku Scheduler, whenever, clockwork, solid-cron all work.
33
+ - Subject line: `[App] Daily Digest — N new · N open · N resolved`
34
+
5
35
  ## [0.8.0] - 2026-07-03
6
36
 
7
37
  ### Added
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ class DigestMailer < ActionMailer::Base
5
+ layout false
6
+
7
+ def digest(data, period: :daily)
8
+ @data = data
9
+ @period = period
10
+ @app_name = ErrorRadar.config.app_name ||
11
+ (defined?(Rails) && Rails.application ? Rails.application.class.module_parent_name : 'App')
12
+ @app_host = ErrorRadar.config.app_host.to_s.chomp('/')
13
+
14
+ period_label = period == :weekly ? 'Weekly' : 'Daily'
15
+ subject = "[#{@app_name}] #{period_label} Digest — " \
16
+ "#{data[:new_this_period]} new · " \
17
+ "#{data[:unresolved]} open · " \
18
+ "#{data[:resolved_this_period]} resolved"
19
+
20
+ recipients = ErrorRadar.config.digest_recipients.any? ?
21
+ ErrorRadar.config.digest_recipients :
22
+ ErrorRadar.config.email_recipients
23
+
24
+ mail(
25
+ to: recipients,
26
+ from: ErrorRadar.config.email_from || 'noreply@localhost',
27
+ subject: subject
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,168 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%
7
+ sev_colors = { 'critical' => '#7b001c', 'error' => '#dc3545', 'warning' => '#fd7e14', 'info' => '#17a2b8' }
8
+ period_label = @period == :weekly ? 'Weekly' : 'Daily'
9
+ fmt = ->(t) { t&.strftime('%Y-%m-%d %H:%M') }
10
+ %>
11
+ </head>
12
+ <body style="margin:0;padding:24px 8px;background:#f3f4f6;font-family:-apple-system,'Segoe UI',Roboto,Arial,sans-serif;color:#1f2937">
13
+ <div style="max-width:640px;margin:0 auto">
14
+
15
+ <%# ── Header ── %>
16
+ <div style="background:#1f2937;color:#fff;padding:20px 24px;border-radius:10px 10px 0 0">
17
+ <h1 style="margin:0 0 4px;font-size:18px">📊 <%= @app_name %> — <%= period_label %> Error Digest</h1>
18
+ <div style="font-size:12px;color:#9ca3af">
19
+ <%= fmt.call(@data[:period_start]) %> → <%= fmt.call(@data[:period_end]) %>
20
+ </div>
21
+ </div>
22
+
23
+ <%# ── Summary cards ── %>
24
+ <div style="background:#fff;padding:20px 24px">
25
+ <table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px">
26
+ <tr>
27
+ <td width="25%" style="padding:4px">
28
+ <div style="background:#fef3c7;border-radius:8px;padding:14px 10px;text-align:center">
29
+ <div style="font-size:26px;font-weight:700;color:#92400e"><%= @data[:new_this_period] %></div>
30
+ <div style="font-size:11px;color:#78350f;margin-top:2px">New errors</div>
31
+ </div>
32
+ </td>
33
+ <td width="25%" style="padding:4px">
34
+ <div style="background:#fee2e2;border-radius:8px;padding:14px 10px;text-align:center">
35
+ <div style="font-size:26px;font-weight:700;color:#991b1b"><%= @data[:unresolved] %></div>
36
+ <div style="font-size:11px;color:#7f1d1d;margin-top:2px">Open + in progress</div>
37
+ </div>
38
+ </td>
39
+ <td width="25%" style="padding:4px">
40
+ <div style="background:#d1fae5;border-radius:8px;padding:14px 10px;text-align:center">
41
+ <div style="font-size:26px;font-weight:700;color:#065f46"><%= @data[:resolved_this_period] %></div>
42
+ <div style="font-size:11px;color:#064e3b;margin-top:2px">Resolved</div>
43
+ </div>
44
+ </td>
45
+ <td width="25%" style="padding:4px">
46
+ <div style="background:#f3f4f6;border-radius:8px;padding:14px 10px;text-align:center">
47
+ <div style="font-size:26px;font-weight:700;color:#374151"><%= @data[:total] %></div>
48
+ <div style="font-size:11px;color:#6b7280;margin-top:2px">Total in DB</div>
49
+ </div>
50
+ </td>
51
+ </tr>
52
+ </table>
53
+
54
+ <%# ── Severity breakdown ── %>
55
+ <% if @data[:by_severity].any? %>
56
+ <div style="margin-bottom:24px">
57
+ <h2 style="margin:0 0 10px;font-size:14px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.05em">Unresolved by severity</h2>
58
+ <table width="100%" cellpadding="0" cellspacing="0">
59
+ <% %w[critical error warning info].each do |sev| %>
60
+ <% count = @data[:by_severity][sev].to_i %>
61
+ <% next if count.zero? %>
62
+ <% pct = (@data[:unresolved] > 0 ? count.to_f / @data[:unresolved] * 100 : 0).round %>
63
+ <tr style="margin-bottom:6px">
64
+ <td width="80" style="padding:3px 0">
65
+ <span style="display:inline-block;padding:2px 10px;border-radius:10px;color:#fff;font-size:11px;font-weight:600;background:<%= sev_colors[sev] || '#6c757d' %>"><%= sev %></span>
66
+ </td>
67
+ <td style="padding:3px 8px">
68
+ <div style="background:#e5e7eb;border-radius:4px;height:10px;overflow:hidden">
69
+ <div style="background:<%= sev_colors[sev] || '#6c757d' %>;height:10px;width:<%= pct %>%"></div>
70
+ </div>
71
+ </td>
72
+ <td width="40" style="padding:3px 0;text-align:right;font-size:13px;font-weight:600"><%= count %></td>
73
+ </tr>
74
+ <% end %>
75
+ </table>
76
+ </div>
77
+ <% end %>
78
+
79
+ <%# ── Top unresolved ── %>
80
+ <% if @data[:top_unresolved].any? %>
81
+ <h2 style="margin:0 0 10px;font-size:14px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.05em">Top 10 unresolved (by occurrences)</h2>
82
+ <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin-bottom:24px">
83
+ <thead>
84
+ <tr style="background:#f9fafb;border-bottom:2px solid #e5e7eb">
85
+ <th style="text-align:left;padding:7px 8px;font-size:11px;color:#6b7280;font-weight:600">Error</th>
86
+ <th style="text-align:left;padding:7px 8px;font-size:11px;color:#6b7280;font-weight:600;width:70px">Severity</th>
87
+ <th style="text-align:right;padding:7px 8px;font-size:11px;color:#6b7280;font-weight:600;width:60px">Hits</th>
88
+ <th style="text-align:left;padding:7px 8px;font-size:11px;color:#6b7280;font-weight:600;width:100px">Last seen</th>
89
+ </tr>
90
+ </thead>
91
+ <tbody>
92
+ <% @data[:top_unresolved].each do |e| %>
93
+ <% url = @app_host.present? ? "#{@app_host}/error_radar/errors/#{e.id}" : nil %>
94
+ <tr style="border-bottom:1px solid #f3f4f6">
95
+ <td style="padding:8px 8px">
96
+ <div style="font-size:12px;font-family:monospace;font-weight:600;color:#1f2937">
97
+ <% if url %><a href="<%= url %>" style="color:#2563eb;text-decoration:none"><%= e.error_class %></a><% else %><%= e.error_class %><% end %>
98
+ </div>
99
+ <div style="font-size:11px;color:#6b7280;margin-top:2px"><%= e.short_message.truncate(80) %></div>
100
+ </td>
101
+ <td style="padding:8px 8px">
102
+ <span style="display:inline-block;padding:2px 8px;border-radius:8px;color:#fff;font-size:10px;font-weight:600;background:<%= sev_colors[e.severity] || '#6c757d' %>"><%= e.severity %></span>
103
+ </td>
104
+ <td style="padding:8px 8px;text-align:right;font-size:13px;font-weight:700;color:#1f2937"><%= e.occurrences %></td>
105
+ <td style="padding:8px 8px;font-size:11px;color:#6b7280"><%= e.last_seen_at&.strftime('%m/%d %H:%M') %></td>
106
+ </tr>
107
+ <% end %>
108
+ </tbody>
109
+ </table>
110
+ <% end %>
111
+
112
+ <%# ── New errors this period ── %>
113
+ <% if @data[:recent_new].any? %>
114
+ <h2 style="margin:0 0 10px;font-size:14px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.05em">🆕 New this period (<%= @data[:new_this_period] %> total)</h2>
115
+ <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;margin-bottom:24px">
116
+ <% @data[:recent_new].each do |e| %>
117
+ <% url = @app_host.present? ? "#{@app_host}/error_radar/errors/#{e.id}" : nil %>
118
+ <tr style="border-bottom:1px solid #f3f4f6">
119
+ <td style="padding:7px 8px">
120
+ <span style="display:inline-block;padding:2px 8px;border-radius:8px;color:#fff;font-size:10px;font-weight:600;background:<%= sev_colors[e.severity] || '#6c757d' %>"><%= e.severity %></span>
121
+ </td>
122
+ <td style="padding:7px 8px">
123
+ <div style="font-size:12px;font-family:monospace;font-weight:600">
124
+ <% if url %><a href="<%= url %>" style="color:#2563eb;text-decoration:none"><%= e.error_class %></a><% else %><%= e.error_class %><% end %>
125
+ </div>
126
+ <div style="font-size:11px;color:#6b7280"><%= e.short_message.truncate(90) %></div>
127
+ </td>
128
+ <td style="padding:7px 8px;font-size:11px;color:#6b7280;white-space:nowrap"><%= fmt.call(e.first_seen_at) %></td>
129
+ </tr>
130
+ <% end %>
131
+ </table>
132
+ <% end %>
133
+
134
+ <%# ── Category breakdown ── %>
135
+ <% if @data[:by_category].any? %>
136
+ <h2 style="margin:0 0 10px;font-size:14px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.05em">Unresolved by category</h2>
137
+ <table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:20px">
138
+ <% @data[:by_category].sort_by { |_, v| -v }.each do |cat, count| %>
139
+ <tr>
140
+ <td width="140" style="padding:3px 0;font-size:12px;color:#374151"><%= cat.to_s.humanize %></td>
141
+ <td style="padding:3px 8px">
142
+ <div style="background:#e5e7eb;border-radius:4px;height:8px;overflow:hidden">
143
+ <% pct = (@data[:unresolved] > 0 ? count.to_f / @data[:unresolved] * 100 : 0).round %>
144
+ <div style="background:#2563eb;height:8px;width:<%= pct %>%"></div>
145
+ </div>
146
+ </td>
147
+ <td width="36" style="padding:3px 0;text-align:right;font-size:12px;font-weight:600;color:#374151"><%= count %></td>
148
+ </tr>
149
+ <% end %>
150
+ </table>
151
+ <% end %>
152
+
153
+ <% if @app_host.present? %>
154
+ <div style="text-align:center;margin-top:16px">
155
+ <a href="<%= @app_host %>/error_radar" style="display:inline-block;background:#2563eb;color:#fff;padding:11px 24px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">Open Error Radar →</a>
156
+ </div>
157
+ <% end %>
158
+ </div>
159
+
160
+ <%# ── Footer ── %>
161
+ <div style="text-align:center;padding:16px 0;font-size:11px;color:#9ca3af">
162
+ Sent by <strong>Error Radar</strong> · <%= @app_name %> ·
163
+ Add <code>rake error_radar:digest</code> to your cron schedule.
164
+ </div>
165
+
166
+ </div>
167
+ </body>
168
+ </html>
@@ -0,0 +1,45 @@
1
+ <%= @period == :weekly ? 'Weekly' : 'Daily' %> Error Digest — <%= @app_name %>
2
+ <%= '=' * 60 %>
3
+ Period: <%= @data[:period_start]&.strftime('%Y-%m-%d %H:%M') %> → <%= @data[:period_end]&.strftime('%Y-%m-%d %H:%M') %>
4
+
5
+ SUMMARY
6
+ New errors this period : <%= @data[:new_this_period] %>
7
+ Resolved this period : <%= @data[:resolved_this_period] %>
8
+ Open + in progress : <%= @data[:unresolved] %>
9
+ Total in DB : <%= @data[:total] %>
10
+
11
+ UNRESOLVED BY SEVERITY
12
+ <% %w[critical error warning info].each do |sev| %>
13
+ <% count = @data[:by_severity][sev].to_i %><% next if count.zero? %>
14
+ <%= sev.ljust(10) %> <%= count %>
15
+ <% end %>
16
+
17
+ TOP 10 UNRESOLVED (by occurrences)
18
+ <% @data[:top_unresolved].each_with_index do |e, i| %>
19
+ <%= (i + 1).to_s.rjust(2) %>. [<%= e.severity.upcase %>] <%= e.error_class %> (×<%= e.occurrences %>)
20
+ <%= e.short_message.truncate(100) %>
21
+ Last seen: <%= e.last_seen_at&.strftime('%Y-%m-%d %H:%M') %>
22
+ <% if @app_host.present? %>
23
+ <%= @app_host %>/error_radar/errors/<%= e.id %>
24
+ <% end %>
25
+ <% end %>
26
+
27
+ <% if @data[:recent_new].any? %>
28
+ NEW ERRORS THIS PERIOD (<%= @data[:new_this_period] %> total, showing <%= [@data[:recent_new].size, 10].min %>)
29
+ <% @data[:recent_new].each do |e| %>
30
+ · [<%= e.severity.upcase %>] <%= e.error_class %> — <%= e.first_seen_at&.strftime('%Y-%m-%d %H:%M') %>
31
+ <%= e.short_message.truncate(100) %>
32
+ <% end %>
33
+ <% end %>
34
+
35
+ UNRESOLVED BY CATEGORY
36
+ <% @data[:by_category].sort_by { |_, v| -v }.each do |cat, count| %>
37
+ <%= cat.to_s.humanize.ljust(20) %> <%= count %>
38
+ <% end %>
39
+
40
+ <% if @app_host.present? %>
41
+ Open dashboard: <%= @app_host %>/error_radar
42
+ <% end %>
43
+
44
+ --
45
+ Sent by Error Radar · Add `rake error_radar:digest` to your cron schedule.
@@ -69,6 +69,12 @@ module ErrorRadar
69
69
  @error_callbacks << block
70
70
  end
71
71
 
72
+ # Digest email ────────────────────────────────────────────────────────────
73
+ # Set to true to enable digest delivery (rake error_radar:digest).
74
+ attr_accessor :digest_enabled
75
+ # Separate recipient list for digests. Falls back to email_recipients if empty.
76
+ attr_accessor :digest_recipients
77
+
72
78
  # Spike detection ─────────────────────────────────────────────────────────
73
79
  # Number of occurrences within spike_window_minutes that triggers a spike alert.
74
80
  # Add :spike to notify_on to enable: config.notify_on = [:new_error, :spike]
@@ -163,7 +169,10 @@ module ErrorRadar
163
169
  @app_host = nil
164
170
  @error_callbacks = []
165
171
 
166
- @spike_threshold = 10
172
+ @digest_enabled = false
173
+ @digest_recipients = []
174
+
175
+ @spike_threshold = 10
167
176
  @spike_window_minutes = 5
168
177
 
169
178
  @track_occurrences = false
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ # Gathers error statistics for a time window and delivers a digest email.
5
+ # Intended to be called from a cron job or scheduled task:
6
+ #
7
+ # # Daily (runs every morning via cron / Heroku Scheduler):
8
+ # rake error_radar:digest
9
+ #
10
+ # # Weekly:
11
+ # rake error_radar:digest:weekly
12
+ #
13
+ # # From code:
14
+ # ErrorRadar::Digest.deliver(since: 24.hours.ago, period: :daily)
15
+ module Digest
16
+ def self.deliver(since: nil, period: :daily)
17
+ return unless ErrorRadar.config.digest_enabled
18
+
19
+ since ||= period == :weekly ? 7.days.ago : 24.hours.ago
20
+ data = build(since: since)
21
+
22
+ require 'error_radar/mailers/digest_mailer'
23
+ DigestMailer.digest(data, period: period).deliver_now
24
+ rescue StandardError => e
25
+ ErrorRadar::Tracking.warn_internal("Digest.deliver failed: #{e.class}: #{e.message}")
26
+ end
27
+
28
+ def self.build(since:)
29
+ now = Time.current
30
+ counts = ErrorLog.group(:status).count
31
+ inv = ErrorLog.statuses.invert
32
+
33
+ open_count = counts[ErrorLog.statuses['open']] || 0
34
+ in_prog_count = counts[ErrorLog.statuses['in_progress']] || 0
35
+ resolved = counts[ErrorLog.statuses['resolved']] || 0
36
+ ignored = counts[ErrorLog.statuses['ignored']] || 0
37
+ total = counts.values.sum
38
+ unresolved = open_count + in_prog_count
39
+
40
+ new_this_period = ErrorLog.where('first_seen_at >= ?', since).count
41
+ resolved_this_period = ErrorLog.where('resolved_at >= ?', since).count
42
+ reopened_this_period = ErrorLog.unresolved.where('resolved_at < ?', since)
43
+ .where('last_seen_at >= ?', since).count
44
+
45
+ top_unresolved = ErrorLog.unresolved.order(occurrences: :desc).limit(10).to_a
46
+ recent_new = ErrorLog.where('first_seen_at >= ?', since)
47
+ .order(first_seen_at: :desc).limit(10).to_a
48
+
49
+ by_severity = ErrorLog.unresolved.group(:severity).count
50
+ .transform_keys { |k| ErrorLog.severities.invert[k] || k.to_s }
51
+ by_category = ErrorLog.unresolved.group(:category).count
52
+ .transform_keys { |k| ErrorLog.categories.invert[k] || k.to_s }
53
+
54
+ trend = build_trend(since, now)
55
+
56
+ {
57
+ period_start: since,
58
+ period_end: now,
59
+ total: total,
60
+ open: open_count,
61
+ in_progress: in_prog_count,
62
+ unresolved: unresolved,
63
+ resolved_total: resolved,
64
+ ignored_total: ignored,
65
+ new_this_period: new_this_period,
66
+ resolved_this_period: resolved_this_period,
67
+ reopened_this_period: reopened_this_period,
68
+ top_unresolved: top_unresolved,
69
+ recent_new: recent_new,
70
+ by_severity: by_severity,
71
+ by_category: by_category,
72
+ trend: trend
73
+ }
74
+ end
75
+
76
+ def self.build_trend(since, now)
77
+ days = [((now - since) / 86_400).ceil, 30].min
78
+ ErrorLog.where(last_seen_at: days.days.ago..)
79
+ .pluck(:last_seen_at)
80
+ .group_by { |t| t.to_date }
81
+ .transform_values(&:size)
82
+ .sort.to_h
83
+ .transform_keys { |d| d.strftime('%m/%d') }
84
+ end
85
+ private_class_method :build_trend
86
+ end
87
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErrorRadar
4
- VERSION = '0.8.0'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/error_radar.rb CHANGED
@@ -6,6 +6,7 @@ require 'error_radar/tracking'
6
6
  require 'error_radar/notifier'
7
7
  require 'error_radar/cleanup'
8
8
  require 'error_radar/spike_detector'
9
+ require 'error_radar/digest'
9
10
  require 'error_radar/middleware'
10
11
  require 'error_radar/server_monitor'
11
12
  require 'error_radar/engine'
@@ -93,6 +93,15 @@ ErrorRadar.configure do |config|
93
93
  # config.github_token = ENV['GITHUB_TOKEN'] # PAT with repo scope
94
94
  # config.github_repo = 'myorg/myapp' # "owner/repo" format
95
95
 
96
+ # --- Digest Email ---
97
+ # Sends a periodic summary of errors via email. Requires ActionMailer.
98
+ # Run via cron / Heroku Scheduler:
99
+ # rake error_radar:digest # last 24 hours (daily)
100
+ # rake error_radar:digest:weekly # last 7 days
101
+ # SINCE="2024-01-01 08:00" rake error_radar:digest # custom window
102
+ # config.digest_enabled = true
103
+ # config.digest_recipients = ['team@myapp.com'] # falls back to email_recipients
104
+
96
105
  # --- Occurrence History ---
97
106
  # Store each individual error hit so you can see context/backtrace per occurrence.
98
107
  # Requires running: bin/rails generate error_radar:upgrade_v060 && bin/rails db:migrate
@@ -1,6 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :error_radar do
4
+ desc 'Send a daily digest email summarising the last 24 hours (requires config.digest_enabled = true)'
5
+ task digest: :environment do
6
+ require 'error_radar/digest'
7
+ since = ENV['SINCE'].presence ? Time.parse(ENV['SINCE']) : 24.hours.ago
8
+ ErrorRadar::Digest.deliver(since: since, period: :daily)
9
+ puts '[ErrorRadar] Daily digest sent.'
10
+ end
11
+
12
+ namespace :digest do
13
+ desc 'Send a weekly digest email summarising the last 7 days (requires config.digest_enabled = true)'
14
+ task weekly: :environment do
15
+ require 'error_radar/digest'
16
+ since = ENV['SINCE'].presence ? Time.parse(ENV['SINCE']) : 7.days.ago
17
+ ErrorRadar::Digest.deliver(since: since, period: :weekly)
18
+ puts '[ErrorRadar] Weekly digest sent.'
19
+ end
20
+ end
21
+
22
+
4
23
  desc 'Delete old resolved/ignored ErrorLogs per config.retention_days and config.max_records'
5
24
  task cleanup: :environment do
6
25
  result = ErrorRadar::Cleanup.run
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chienbn9x
@@ -63,12 +63,15 @@ files:
63
63
  - app/controllers/error_radar/application_controller.rb
64
64
  - app/controllers/error_radar/dashboard_controller.rb
65
65
  - app/controllers/error_radar/errors_controller.rb
66
+ - app/mailers/error_radar/digest_mailer.rb
66
67
  - app/mailers/error_radar/error_mailer.rb
67
68
  - app/models/error_radar/application_record.rb
68
69
  - app/models/error_radar/error_log.rb
69
70
  - app/models/error_radar/error_occurrence.rb
70
71
  - app/views/error_radar/dashboard/index.html.erb
71
72
  - app/views/error_radar/dashboard/show.html.erb
73
+ - app/views/error_radar/digest_mailer/digest.html.erb
74
+ - app/views/error_radar/digest_mailer/digest.text.erb
72
75
  - app/views/error_radar/error_mailer/new_error.html.erb
73
76
  - app/views/error_radar/error_mailer/new_error.text.erb
74
77
  - app/views/error_radar/errors/index.html.erb
@@ -79,6 +82,7 @@ files:
79
82
  - lib/error_radar/capture_job.rb
80
83
  - lib/error_radar/cleanup.rb
81
84
  - lib/error_radar/configuration.rb
85
+ - lib/error_radar/digest.rb
82
86
  - lib/error_radar/engine.rb
83
87
  - lib/error_radar/integrations/active_job.rb
84
88
  - lib/error_radar/integrations/github.rb