error_radar 0.5.0 → 0.6.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: 106881790c16306716c7588f317da722c0e589db0a0a2bbb6e7ad838c9a8d118
4
- data.tar.gz: d4c9ccf1363b88637dc625418daf8553e15aad0796f42f79aadc6eafb3a4f554
3
+ metadata.gz: bdfb13eb6566d150855376a07e28a5a76a758b23526d7cf31de5a6adc17e5d27
4
+ data.tar.gz: 713dcf3fa8c7867b971b6e7da6dd7dbf56a6e4057ec284dbadb6a8818b83f6d6
5
5
  SHA512:
6
- metadata.gz: c880963d54ef38274dbc226b970f848f7a9ced038c6d88ababffe1b3eb71ef2d8490695a0a256775a500e707a72b9af2a017a066e8af1f66b17ab842a18ac726
7
- data.tar.gz: ba612e7cc8a547cd1bf7ffb8704347636bccc7f763fba3a73fbfe1919b86573421873b61c88f6c8b44da0ed661cf8cf73d4da62a54998828dfe1a767642580ad
6
+ metadata.gz: b71979e7ef0ca2737d789f3f047a024c9933d8b2149e83e5d88fc734144213226dfb1a160b4080ee008104dd0c19dd35a1da06ce0fbeb2ae61a0a65455a65d0f
7
+ data.tar.gz: 1838775fea5865d76bc746c49bc51efb0a557d6f6caf0924bdcd489b081570ff8667715690e3ef1b8a32ca7af219d7a26a1fe312e17304c27e4a9025d0632bf4
data/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.6.0] - 2026-07-03
6
+
7
+ ### Added
8
+ - **Async capture** via ActiveJob: set `config.async_capture = true` to enqueue
9
+ error writes through `CaptureJob` so exceptions don't block the request/job
10
+ thread. Falls back to synchronous capture if ActiveJob is unavailable.
11
+ Configure the queue with `config.capture_job_queue` (default `:default`).
12
+ - **Age-based retention**: set `config.retention_days` to automatically prune
13
+ resolved/ignored records older than N days.
14
+ - **Count-based retention**: set `config.max_records` to cap the table size;
15
+ oldest resolved/ignored records are deleted first when the limit is exceeded.
16
+ - **`ErrorRadar::Cleanup.run`**: underlying cleanup logic, callable from Ruby or
17
+ Rake. Supports `older_than_days:` override and `dry_run: true` for previewing
18
+ without deleting.
19
+ - **Rake tasks**:
20
+ - `rake error_radar:cleanup` — apply `retention_days` + `max_records`
21
+ - `rake error_radar:cleanup:dry_run` — preview without deleting
22
+ - `rake error_radar:cleanup:older_than` — one-off: `DAYS=30 rake …`
23
+ - `rake error_radar:stats` — print table summary (total, by status, oldest)
24
+ - **Dashboard maintenance panel**: data summary (total / purgeable / open),
25
+ oldest record date, and an inline "Purge / Dry run" form with adjustable days.
26
+ - **`POST /maintenance/purge`** endpoint: called by the dashboard's Purge button;
27
+ accepts `days` and `dry_run` params, returns JSON `{ deleted:, dry_run: }`.
28
+ - **Dashboard stat query optimization**: 5 separate COUNT queries replaced by one
29
+ `group(:status).count` query.
30
+
31
+ ### Notes
32
+ - `async_capture` is `false` by default to avoid surprising behaviour for apps
33
+ that do not have a queue adapter configured.
34
+ - Cleanup only touches `resolved` and `ignored` records — open/in-progress
35
+ records are never auto-deleted.
36
+
5
37
  ## [0.5.0] - 2026-07-03
6
38
 
7
39
  ### Added
@@ -10,13 +10,18 @@ module ErrorRadar
10
10
  SEVERITY_ORDER = { 'critical' => 0, 'error' => 1, 'warning' => 2, 'info' => 3 }.freeze
11
11
 
12
12
  def index
13
- @total = ErrorLog.count
14
- @open_count = ErrorLog.status_open.count
15
- @in_progress = ErrorLog.status_in_progress.count
16
- @resolved_count = ErrorLog.status_resolved.count
17
- @ignored_count = ErrorLog.status_ignored.count
13
+ counts_by_status = ErrorLog.group(:status).count
14
+ # statuses returns {"open"=>0, "in_progress"=>1, "resolved"=>2, "ignored"=>3}
15
+ inv = ErrorLog.statuses.invert
16
+ @open_count = counts_by_status[ErrorLog.statuses['open']] || 0
17
+ @in_progress = counts_by_status[ErrorLog.statuses['in_progress']] || 0
18
+ @resolved_count = counts_by_status[ErrorLog.statuses['resolved']] || 0
19
+ @ignored_count = counts_by_status[ErrorLog.statuses['ignored']] || 0
20
+ @total = counts_by_status.values.sum
18
21
  @unresolved = @open_count + @in_progress
19
22
 
23
+ @oldest_record = ErrorLog.minimum(:first_seen_at)
24
+
20
25
  @by_category = ErrorLog.unresolved.group(:category).count
21
26
  @by_severity = ErrorLog.unresolved.group(:severity).count
22
27
 
@@ -46,6 +51,28 @@ module ErrorRadar
46
51
  @external_links = build_external_links
47
52
  end
48
53
 
54
+ def purge
55
+ days = params[:days].presence&.to_i
56
+ dry_run = params[:dry_run] == '1'
57
+
58
+ result = ErrorRadar::Cleanup.run(
59
+ older_than_days: (days if days&.positive?),
60
+ dry_run: dry_run
61
+ )
62
+
63
+ respond_to do |format|
64
+ format.json { render json: result }
65
+ format.html do
66
+ msg = if dry_run
67
+ "Dry run: would delete #{result[:deleted]} record(s)."
68
+ else
69
+ "Purged #{result[:deleted]} record(s)."
70
+ end
71
+ redirect_to root_path, notice: msg
72
+ end
73
+ end
74
+ end
75
+
49
76
  private
50
77
 
51
78
  def build_external_links
@@ -101,6 +101,52 @@
101
101
  </div>
102
102
  </div>
103
103
 
104
+ <div class="panel" style="margin-bottom:20px" id="maintenance-panel">
105
+ <h2>Maintenance &amp; Data Retention</h2>
106
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:12px;margin-bottom:16px">
107
+ <div style="background:#f9fafb;border-radius:8px;padding:12px;text-align:center">
108
+ <div style="font-size:22px;font-weight:700;color:#111827"><%= @total %></div>
109
+ <div style="font-size:11px;color:#6b7280">Total records</div>
110
+ </div>
111
+ <div style="background:#f9fafb;border-radius:8px;padding:12px;text-align:center">
112
+ <div style="font-size:22px;font-weight:700;color:#28a745"><%= @resolved_count + @ignored_count %></div>
113
+ <div style="font-size:11px;color:#6b7280">Resolved / Ignored (purgeable)</div>
114
+ </div>
115
+ <div style="background:#f9fafb;border-radius:8px;padding:12px;text-align:center">
116
+ <div style="font-size:22px;font-weight:700;color:#fd7e14"><%= @unresolved %></div>
117
+ <div style="font-size:11px;color:#6b7280">Open / In progress</div>
118
+ </div>
119
+ <div style="background:#f9fafb;border-radius:8px;padding:12px;text-align:center">
120
+ <div style="font-size:13px;font-weight:600;color:#111827">
121
+ <%= @oldest_record ? @oldest_record.strftime('%Y-%m-%d') : '—' %>
122
+ </div>
123
+ <div style="font-size:11px;color:#6b7280">Oldest record</div>
124
+ </div>
125
+ </div>
126
+ <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
127
+ <label style="font-size:13px;font-weight:500">Delete resolved/ignored older than</label>
128
+ <input id="purge-days" type="number" min="1" placeholder="days" value="90"
129
+ style="width:80px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px">
130
+ <span style="font-size:13px;color:#6b7280">days</span>
131
+ <button onclick="doPurge(false)"
132
+ style="padding:6px 14px;background:#dc3545;color:#fff;border:none;border-radius:6px;font-size:13px;cursor:pointer">
133
+ Purge
134
+ </button>
135
+ <button onclick="doPurge(true)"
136
+ style="padding:6px 14px;background:#6c757d;color:#fff;border:none;border-radius:6px;font-size:13px;cursor:pointer">
137
+ Dry run
138
+ </button>
139
+ <span id="purge-result" style="font-size:13px;color:#16a34a"></span>
140
+ </div>
141
+ <% if ErrorRadar.config.retention_days || ErrorRadar.config.max_records %>
142
+ <div style="margin-top:10px;font-size:12px;color:#6b7280">
143
+ Config: retention_days=<strong><%= ErrorRadar.config.retention_days || '—' %></strong>,
144
+ max_records=<strong><%= ErrorRadar.config.max_records || '—' %></strong>
145
+ (applies automatically when you run <code>rake error_radar:cleanup</code>)
146
+ </div>
147
+ <% end %>
148
+ </div>
149
+
104
150
  <div class="panel" style="margin-bottom:20px">
105
151
  <h2>Kanban — drag a card to change status</h2>
106
152
  <div class="kanban">
@@ -127,6 +173,28 @@
127
173
  </div>
128
174
 
129
175
  <script>
176
+ function doPurge(dryRun) {
177
+ var days = document.getElementById('purge-days').value;
178
+ var result = document.getElementById('purge-result');
179
+ var token = document.querySelector('meta[name="csrf-token"]').content;
180
+ result.textContent = 'Running…';
181
+ result.style.color = '#6b7280';
182
+ fetch('<%= maintenance_purge_path %>', {
183
+ method: 'POST',
184
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token, 'Accept': 'application/json' },
185
+ body: JSON.stringify({ days: days, dry_run: dryRun ? '1' : '0' })
186
+ }).then(function(r) { return r.json(); })
187
+ .then(function(d) {
188
+ var label = dryRun ? 'Would delete' : 'Deleted';
189
+ result.textContent = label + ' ' + d.deleted + ' record(s).';
190
+ result.style.color = dryRun ? '#fd7e14' : '#16a34a';
191
+ })
192
+ .catch(function() {
193
+ result.textContent = 'Request failed.';
194
+ result.style.color = '#dc3545';
195
+ });
196
+ }
197
+
130
198
  (function () {
131
199
  var token = document.querySelector('meta[name="csrf-token"]').content;
132
200
 
data/config/routes.rb CHANGED
@@ -16,4 +16,6 @@ ErrorRadar::Engine.routes.draw do
16
16
  get 'errors/:id', to: 'errors#show', as: :error
17
17
  patch 'errors/:id/status', to: 'errors#update_status', as: :error_status
18
18
  delete 'errors/:id', to: 'errors#destroy'
19
+
20
+ post 'maintenance/purge', to: 'dashboard#purge', as: :maintenance_purge
19
21
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ # Background job for async exception capture. Used when config.async_capture
5
+ # is true. Failures are logged to Rails.logger and swallowed — a broken
6
+ # capture job must not cause retry storms or affect the application.
7
+ class CaptureJob < ActiveJob::Base
8
+ queue_as { ErrorRadar.config.capture_job_queue }
9
+
10
+ def perform(attrs_json)
11
+ attrs = JSON.parse(attrs_json, symbolize_names: true)
12
+ log = ErrorLog.record(**attrs)
13
+ Notifier.dispatch(log) if log
14
+ rescue StandardError => e
15
+ ErrorRadar::Tracking.warn_internal("CaptureJob failed: #{e.class}: #{e.message}")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ # Prunes stale ErrorLog records. Two independent strategies run in sequence:
5
+ #
6
+ # 1. Age-based: deletes resolved/ignored records whose last_seen_at is
7
+ # older than `older_than_days` (or config.retention_days).
8
+ # 2. Count-based: when the total exceeds config.max_records, deletes the
9
+ # oldest resolved/ignored records until the limit is satisfied.
10
+ #
11
+ # Open and in_progress records are never deleted automatically.
12
+ module Cleanup
13
+ def self.run(older_than_days: nil, dry_run: false)
14
+ deleted = 0
15
+
16
+ # ── Age-based pruning ───────────────────────────────────────────────
17
+ days = (older_than_days || ErrorRadar.config.retention_days)&.to_i
18
+ if days && days > 0
19
+ cutoff = days.days.ago
20
+ scope = stale_scope.where('last_seen_at < ?', cutoff)
21
+ deleted += dry_run ? scope.count : scope.delete_all
22
+ end
23
+
24
+ # ── Count-based pruning ────────────────────────────────────────────
25
+ max = ErrorRadar.config.max_records&.to_i
26
+ if max && max > 0
27
+ total = ErrorLog.count
28
+ if total > max
29
+ excess = total - max
30
+ ids = stale_scope.order(last_seen_at: :asc).limit(excess).pluck(:id)
31
+ deleted += dry_run ? ids.size : ErrorLog.where(id: ids).delete_all
32
+ end
33
+ end
34
+
35
+ { deleted: deleted, dry_run: dry_run }
36
+ end
37
+
38
+ def self.stale_scope
39
+ ErrorLog.where(status: [ErrorLog.statuses[:resolved], ErrorLog.statuses[:ignored]])
40
+ end
41
+ private_class_method :stale_scope
42
+ end
43
+ end
@@ -69,6 +69,19 @@ module ErrorRadar
69
69
  @error_callbacks << block
70
70
  end
71
71
 
72
+ # Performance ─────────────────────────────────────────────────────────────
73
+ # When true, ErrorLog.record is enqueued via ActiveJob (non-blocking).
74
+ # Requires ActiveJob to be configured. Falls back to sync if ActiveJob is absent.
75
+ attr_accessor :async_capture
76
+ # Queue name for CaptureJob when async_capture is enabled.
77
+ attr_accessor :capture_job_queue
78
+
79
+ # Retention ───────────────────────────────────────────────────────────────
80
+ # Auto-delete resolved/ignored records older than this many days (nil = keep forever).
81
+ attr_accessor :retention_days
82
+ # Keep at most this many total records; deletes oldest resolved/ignored first (nil = unlimited).
83
+ attr_accessor :max_records
84
+
72
85
  # REST API ────────────────────────────────────────────────────────────────
73
86
  # Bearer token for /api/* endpoints. nil = unauthenticated (not for prod).
74
87
  attr_accessor :api_token
@@ -135,6 +148,11 @@ module ErrorRadar
135
148
  @app_host = nil
136
149
  @error_callbacks = []
137
150
 
151
+ @async_capture = false
152
+ @capture_job_queue = :default
153
+ @retention_days = nil
154
+ @max_records = nil
155
+
138
156
  @api_token = nil
139
157
  @github_token = nil
140
158
  @github_repo = nil
@@ -52,5 +52,9 @@ module ErrorRadar
52
52
  ErrorRadar::Integrations::RailsAdmin.install!
53
53
  end
54
54
  end
55
+
56
+ rake_tasks do
57
+ load File.expand_path('../tasks/error_radar.rake', __dir__)
58
+ end
55
59
  end
56
60
  end
@@ -27,6 +27,17 @@ module ErrorRadar
27
27
  attrs.merge!(extra.compact) if extra.is_a?(Hash)
28
28
  end
29
29
 
30
+ if ErrorRadar.config.async_capture && defined?(::ActiveJob::Base)
31
+ require 'error_radar/capture_job'
32
+ job_attrs = attrs.merge(
33
+ category: attrs[:category].to_s,
34
+ severity: attrs[:severity].to_s,
35
+ context: safe_json(attrs[:context])
36
+ )
37
+ ErrorRadar::CaptureJob.perform_later(job_attrs.to_json)
38
+ return nil
39
+ end
40
+
30
41
  log = ErrorRadar::ErrorLog.record(**attrs)
31
42
  ErrorRadar::Notifier.dispatch(log) if log
32
43
  log
@@ -112,6 +123,12 @@ module ErrorRadar
112
123
  nil
113
124
  end
114
125
 
126
+ def safe_json(value)
127
+ JSON.parse(value.to_json)
128
+ rescue StandardError
129
+ {}
130
+ end
131
+
115
132
  def warn_internal(message)
116
133
  logger = defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil
117
134
  logger ? logger.error("[ErrorRadar] #{message}") : warn("[ErrorRadar] #{message}")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErrorRadar
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/error_radar.rb CHANGED
@@ -4,6 +4,7 @@ require 'error_radar/version'
4
4
  require 'error_radar/configuration'
5
5
  require 'error_radar/tracking'
6
6
  require 'error_radar/notifier'
7
+ require 'error_radar/cleanup'
7
8
  require 'error_radar/middleware'
8
9
  require 'error_radar/server_monitor'
9
10
  require 'error_radar/engine'
@@ -83,6 +83,25 @@ ErrorRadar.configure do |config|
83
83
  # Requires running: bin/rails generate error_radar:upgrade_v050 && bin/rails db:migrate
84
84
  # config.github_token = ENV['GITHUB_TOKEN'] # PAT with repo scope
85
85
  # config.github_repo = 'myorg/myapp' # "owner/repo" format
86
+
87
+ # --- Performance & Async Capture ---
88
+ # Write ErrorLog records via ActiveJob so exceptions don't block the request.
89
+ # Requires ActiveJob + a queue adapter (Sidekiq, Solid Queue, etc.).
90
+ # Falls back to synchronous capture if ActiveJob is unavailable.
91
+ # config.async_capture = true
92
+ # config.capture_job_queue = :default # queue name for CaptureJob
93
+
94
+ # --- Retention / Cleanup ---
95
+ # Auto-prune old resolved/ignored records to keep the table lean.
96
+ # Run `rake error_radar:cleanup` from a cron job or Heroku Scheduler.
97
+ # config.retention_days = 90 # delete resolved/ignored records older than 90 days
98
+ # config.max_records = 50000 # hard cap; oldest resolved/ignored purged first
99
+ #
100
+ # Rake tasks available:
101
+ # rake error_radar:cleanup # apply retention_days + max_records
102
+ # rake error_radar:cleanup:dry_run # preview without deleting
103
+ # rake error_radar:cleanup:older_than # DAYS=30 rake error_radar:cleanup:older_than
104
+ # rake error_radar:stats # print table summary
86
105
  end
87
106
 
88
107
  # ActiveJob is now captured automatically via install_active_job = true above.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :error_radar do
4
+ desc 'Delete old resolved/ignored ErrorLogs per config.retention_days and config.max_records'
5
+ task cleanup: :environment do
6
+ result = ErrorRadar::Cleanup.run
7
+ puts "[ErrorRadar] Cleanup complete — #{result[:deleted]} record(s) deleted."
8
+ end
9
+
10
+ namespace :cleanup do
11
+ desc 'Preview what error_radar:cleanup would delete without actually deleting'
12
+ task dry_run: :environment do
13
+ result = ErrorRadar::Cleanup.run(dry_run: true)
14
+ puts "[ErrorRadar] Dry run — would delete #{result[:deleted]} record(s)."
15
+ end
16
+
17
+ desc 'Delete resolved/ignored ErrorLogs older than DAYS (e.g. DAYS=30 rake error_radar:cleanup:older_than)'
18
+ task older_than: :environment do
19
+ days = ENV.fetch('DAYS', nil)&.to_i
20
+ abort '[ErrorRadar] Set DAYS env var (e.g. DAYS=30)' unless days&.positive?
21
+
22
+ result = ErrorRadar::Cleanup.run(older_than_days: days)
23
+ puts "[ErrorRadar] Deleted #{result[:deleted]} record(s) older than #{days} days."
24
+ end
25
+ end
26
+
27
+ desc 'Print a summary of ErrorLog table stats'
28
+ task stats: :environment do
29
+ counts = ErrorRadar::ErrorLog.group(:status).count
30
+ .transform_keys { |k| ErrorRadar::ErrorLog.statuses.key(k) || k }
31
+ total = counts.values.sum
32
+
33
+ puts "\n[ErrorRadar] Error log summary"
34
+ puts " Total : #{total}"
35
+ puts " Open : #{counts['open'] || 0}"
36
+ puts " In progress: #{counts['in_progress'] || 0}"
37
+ puts " Resolved : #{counts['resolved'] || 0}"
38
+ puts " Ignored : #{counts['ignored'] || 0}"
39
+
40
+ oldest = ErrorRadar::ErrorLog.minimum(:first_seen_at)
41
+ puts " Oldest : #{oldest&.strftime('%Y-%m-%d') || '—'}"
42
+ puts ''
43
+ end
44
+ end
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chienbn9x
@@ -75,6 +75,8 @@ files:
75
75
  - app/views/layouts/error_radar/application.html.erb
76
76
  - config/routes.rb
77
77
  - lib/error_radar.rb
78
+ - lib/error_radar/capture_job.rb
79
+ - lib/error_radar/cleanup.rb
78
80
  - lib/error_radar/configuration.rb
79
81
  - lib/error_radar/engine.rb
80
82
  - lib/error_radar/integrations/active_job.rb
@@ -96,6 +98,7 @@ files:
96
98
  - lib/generators/error_radar/install/templates/initializer.rb
97
99
  - lib/generators/error_radar/upgrade_v050/templates/add_github_issue_to_error_radar_error_logs.rb.tt
98
100
  - lib/generators/error_radar/upgrade_v050/upgrade_v050_generator.rb
101
+ - lib/tasks/error_radar.rake
99
102
  homepage: https://github.com/chienbn9x/error_radar
100
103
  licenses:
101
104
  - MIT