error_radar 0.4.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 +4 -4
- data/CHANGELOG.md +61 -0
- data/app/controllers/error_radar/api/base_controller.rb +20 -0
- data/app/controllers/error_radar/api/errors_controller.rb +112 -0
- data/app/controllers/error_radar/api/stats_controller.rb +22 -0
- data/app/controllers/error_radar/dashboard_controller.rb +32 -5
- data/app/controllers/error_radar/errors_controller.rb +34 -1
- data/app/views/error_radar/dashboard/index.html.erb +68 -0
- data/app/views/error_radar/errors/show.html.erb +55 -0
- data/config/routes.rb +15 -5
- data/lib/error_radar/capture_job.rb +18 -0
- data/lib/error_radar/cleanup.rb +43 -0
- data/lib/error_radar/configuration.rb +32 -0
- data/lib/error_radar/engine.rb +4 -0
- data/lib/error_radar/integrations/github.rb +74 -0
- data/lib/error_radar/tracking.rb +17 -0
- data/lib/error_radar/version.rb +1 -1
- data/lib/error_radar.rb +1 -0
- data/lib/generators/error_radar/install/templates/initializer.rb +30 -0
- data/lib/generators/error_radar/upgrade_v050/templates/add_github_issue_to_error_radar_error_logs.rb.tt +7 -0
- data/lib/generators/error_radar/upgrade_v050/upgrade_v050_generator.rb +27 -0
- data/lib/tasks/error_radar.rake +44 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdfb13eb6566d150855376a07e28a5a76a758b23526d7cf31de5a6adc17e5d27
|
|
4
|
+
data.tar.gz: 713dcf3fa8c7867b971b6e7da6dd7dbf56a6e4057ec284dbadb6a8818b83f6d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b71979e7ef0ca2737d789f3f047a024c9933d8b2149e83e5d88fc734144213226dfb1a160b4080ee008104dd0c19dd35a1da06ce0fbeb2ae61a0a65455a65d0f
|
|
7
|
+
data.tar.gz: 1838775fea5865d76bc746c49bc51efb0a557d6f6caf0924bdcd489b081570ff8667715690e3ef1b8a32ca7af219d7a26a1fe312e17304c27e4a9025d0632bf4
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,67 @@
|
|
|
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
|
+
|
|
37
|
+
## [0.5.0] - 2026-07-03
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- **REST API** at `/api/*` for external integrations (CI/CD, dashboards, scripts):
|
|
41
|
+
- `GET /api/errors` — paginated list with the same filters as the web UI
|
|
42
|
+
(`status`, `severity`, `category`, `q`, `from`, `to`, `sort`, `order`, `page`)
|
|
43
|
+
- `GET /api/errors/:id` — full detail including context, backtrace, and
|
|
44
|
+
`github_issue_url` (if column is present)
|
|
45
|
+
- `PATCH /api/errors/:id` — update status; resolve accepts optional `note`
|
|
46
|
+
and `resolved_by` params
|
|
47
|
+
- `GET /api/stats` — summary counts by status, severity, and category
|
|
48
|
+
- **Bearer-token API auth**: set `config.api_token` to protect all `/api/*`
|
|
49
|
+
endpoints with `Authorization: Bearer <token>`.
|
|
50
|
+
- **GitHub Issue integration**: "Create GitHub Issue" button on the error detail
|
|
51
|
+
page opens a pre-filled issue with error class, source, message, backtrace,
|
|
52
|
+
and a deep-link back to Error Radar. Requires `config.github_token` and
|
|
53
|
+
`config.github_repo`.
|
|
54
|
+
- **`github_issue_url` column**: stored on the error row so the button becomes
|
|
55
|
+
"View GitHub Issue" once an issue exists. Requires running the upgrade
|
|
56
|
+
migration: `bin/rails generate error_radar:upgrade_v050 && bin/rails db:migrate`.
|
|
57
|
+
- **`error_radar:upgrade_v050` generator**: generates the migration that adds
|
|
58
|
+
`github_issue_url` to `error_radar_error_logs`.
|
|
59
|
+
|
|
60
|
+
### Notes
|
|
61
|
+
- The GitHub column is optional — the integration is gracefully degraded when the
|
|
62
|
+
migration has not been run (button appears but URL is not persisted).
|
|
63
|
+
- The API controllers live in `ErrorRadar::Api::*` to avoid polluting the host
|
|
64
|
+
app's controller namespace.
|
|
65
|
+
|
|
5
66
|
## [0.4.0] - 2026-07-03
|
|
6
67
|
|
|
7
68
|
### Added
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ErrorRadar
|
|
4
|
+
module Api
|
|
5
|
+
class BaseController < ActionController::Base
|
|
6
|
+
skip_before_action :verify_authenticity_token
|
|
7
|
+
before_action :authenticate_api!
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def authenticate_api!
|
|
12
|
+
token = ErrorRadar.config.api_token
|
|
13
|
+
return if token.nil?
|
|
14
|
+
|
|
15
|
+
provided = request.headers['Authorization'].to_s.delete_prefix('Bearer ').strip
|
|
16
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized unless provided == token
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ErrorRadar
|
|
4
|
+
module Api
|
|
5
|
+
class ErrorsController < BaseController
|
|
6
|
+
PER_PAGE = 50
|
|
7
|
+
|
|
8
|
+
# GET /api/errors
|
|
9
|
+
def index
|
|
10
|
+
scope = build_scope
|
|
11
|
+
total = scope.count
|
|
12
|
+
page = [params[:page].to_i, 1].max
|
|
13
|
+
total_pages = [(total.to_f / PER_PAGE).ceil, 1].max
|
|
14
|
+
page = [page, total_pages].min
|
|
15
|
+
|
|
16
|
+
errors = scope.limit(PER_PAGE).offset((page - 1) * PER_PAGE)
|
|
17
|
+
|
|
18
|
+
render json: {
|
|
19
|
+
data: errors.map { |e| serialize(e) },
|
|
20
|
+
meta: { total: total, page: page, per_page: PER_PAGE, total_pages: total_pages }
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /api/errors/:id
|
|
25
|
+
def show
|
|
26
|
+
log = ErrorLog.find(params[:id])
|
|
27
|
+
render json: { data: serialize(log, detail: true) }
|
|
28
|
+
rescue ActiveRecord::RecordNotFound
|
|
29
|
+
render json: { error: 'Not found' }, status: :not_found
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# PATCH /api/errors/:id
|
|
33
|
+
def update
|
|
34
|
+
log = ErrorLog.find(params[:id])
|
|
35
|
+
new_status = params[:status].to_s
|
|
36
|
+
|
|
37
|
+
unless ErrorLog.statuses.key?(new_status)
|
|
38
|
+
return render json: { error: 'invalid status' }, status: :unprocessable_entity
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if new_status == 'resolved'
|
|
42
|
+
log.resolve!(by: params[:resolved_by].presence, note: params[:note].presence)
|
|
43
|
+
else
|
|
44
|
+
log.update!(status: new_status, resolved_at: nil)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
render json: { data: serialize(log) }
|
|
48
|
+
rescue ActiveRecord::RecordNotFound
|
|
49
|
+
render json: { error: 'Not found' }, status: :not_found
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def build_scope
|
|
55
|
+
scope = ErrorLog.all
|
|
56
|
+
scope = scope.where(status: params[:status]) if params[:status].present? && ErrorLog.statuses.key?(params[:status])
|
|
57
|
+
scope = scope.where(severity: params[:severity]) if params[:severity].present? && ErrorLog.severities.key?(params[:severity])
|
|
58
|
+
scope = scope.where(category: params[:category]) if params[:category].present? && ErrorLog.categories.key?(params[:category])
|
|
59
|
+
|
|
60
|
+
if params[:q].present?
|
|
61
|
+
q = "%#{params[:q].downcase}%"
|
|
62
|
+
scope = scope.where('lower(message) LIKE :q OR lower(error_class) LIKE :q OR lower(source) LIKE :q', q: q)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
from = params[:from].present? ? (Date.parse(params[:from]) rescue nil) : nil
|
|
66
|
+
to = params[:to].present? ? (Date.parse(params[:to]) rescue nil) : nil
|
|
67
|
+
scope = scope.where('last_seen_at >= ?', from) if from
|
|
68
|
+
scope = scope.where('last_seen_at <= ?', to.end_of_day) if to
|
|
69
|
+
|
|
70
|
+
sort = %w[last_seen_at first_seen_at occurrences].include?(params[:sort]) ? params[:sort] : 'last_seen_at'
|
|
71
|
+
dir = params[:order] == 'asc' ? :asc : :desc
|
|
72
|
+
scope.order(sort => dir)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def serialize(log, detail: false)
|
|
76
|
+
data = {
|
|
77
|
+
id: log.id,
|
|
78
|
+
error_class: log.error_class,
|
|
79
|
+
source: log.source,
|
|
80
|
+
message: log.message,
|
|
81
|
+
category: log.category,
|
|
82
|
+
severity: log.severity,
|
|
83
|
+
status: log.status,
|
|
84
|
+
occurrences: log.occurrences,
|
|
85
|
+
first_seen_at: log.first_seen_at&.iso8601,
|
|
86
|
+
last_seen_at: log.last_seen_at&.iso8601,
|
|
87
|
+
resolved_at: log.resolved_at&.iso8601,
|
|
88
|
+
resolved_by: log.resolved_by
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if detail
|
|
92
|
+
data.merge!(
|
|
93
|
+
fingerprint: log.fingerprint,
|
|
94
|
+
resolution_note: log.resolution_note,
|
|
95
|
+
http_status: log.http_status,
|
|
96
|
+
request_url: log.request_url,
|
|
97
|
+
api_code: log.api_code,
|
|
98
|
+
api_subcode: log.api_subcode,
|
|
99
|
+
context: log.context,
|
|
100
|
+
backtrace: log.backtrace
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if log.class.column_names.include?('github_issue_url')
|
|
105
|
+
data[:github_issue_url] = log.github_issue_url
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
data
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ErrorRadar
|
|
4
|
+
module Api
|
|
5
|
+
# GET /api/stats — summary counts for dashboards, CI gates, uptime monitors.
|
|
6
|
+
class StatsController < BaseController
|
|
7
|
+
def show
|
|
8
|
+
render json: {
|
|
9
|
+
total: ErrorLog.count,
|
|
10
|
+
open: ErrorLog.status_open.count,
|
|
11
|
+
in_progress: ErrorLog.status_in_progress.count,
|
|
12
|
+
resolved: ErrorLog.status_resolved.count,
|
|
13
|
+
ignored: ErrorLog.status_ignored.count,
|
|
14
|
+
unresolved: ErrorLog.unresolved.count,
|
|
15
|
+
by_severity: ErrorLog.group(:severity).count,
|
|
16
|
+
by_category: ErrorLog.group(:category).count,
|
|
17
|
+
generated_at: Time.current.iso8601
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@
|
|
17
|
-
@
|
|
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
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module ErrorRadar
|
|
4
4
|
class ErrorsController < ApplicationController
|
|
5
5
|
before_action :authenticate_request!
|
|
6
|
-
before_action :set_error, only: %i[show update_status destroy]
|
|
6
|
+
before_action :set_error, only: %i[show update_status destroy create_github_issue]
|
|
7
7
|
|
|
8
8
|
rescue_from ActiveRecord::RecordNotFound do
|
|
9
9
|
redirect_to errors_path, alert: 'Error not found.'
|
|
@@ -44,6 +44,35 @@ module ErrorRadar
|
|
|
44
44
|
render json: { ok: true }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def create_github_issue
|
|
48
|
+
unless github_configured?
|
|
49
|
+
return render json: { ok: false, error: 'GitHub not configured (set config.github_token and config.github_repo)' },
|
|
50
|
+
status: :unprocessable_entity
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if ErrorLog.column_names.include?('github_issue_url') && @error.github_issue_url.present?
|
|
54
|
+
return render json: { ok: false, error: 'Issue already created', url: @error.github_issue_url },
|
|
55
|
+
status: :unprocessable_entity
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
require 'error_radar/integrations/github'
|
|
59
|
+
result = ErrorRadar::Integrations::Github.create_issue(
|
|
60
|
+
@error,
|
|
61
|
+
token: ErrorRadar.config.github_token,
|
|
62
|
+
repo: ErrorRadar.config.github_repo
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if result['html_url']
|
|
66
|
+
@error.update!(github_issue_url: result['html_url']) if ErrorLog.column_names.include?('github_issue_url')
|
|
67
|
+
render json: { ok: true, url: result['html_url'], number: result['number'] }
|
|
68
|
+
else
|
|
69
|
+
render json: { ok: false, error: result['message'] || 'GitHub API error' },
|
|
70
|
+
status: :unprocessable_entity
|
|
71
|
+
end
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
render json: { ok: false, error: e.message }, status: :internal_server_error
|
|
74
|
+
end
|
|
75
|
+
|
|
47
76
|
def bulk
|
|
48
77
|
ids = Array(params[:ids]).map(&:to_i).select(&:positive?)
|
|
49
78
|
action = params[:bulk_action].to_s
|
|
@@ -115,6 +144,10 @@ module ErrorRadar
|
|
|
115
144
|
ids.size
|
|
116
145
|
end
|
|
117
146
|
|
|
147
|
+
def github_configured?
|
|
148
|
+
ErrorRadar.config.github_token.present? && ErrorRadar.config.github_repo.present?
|
|
149
|
+
end
|
|
150
|
+
|
|
118
151
|
def active_filter_params
|
|
119
152
|
params.permit(:q, :status, :severity, :category, :from, :to, :sort, :order).to_h
|
|
120
153
|
end
|
|
@@ -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 & 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
|
|
|
@@ -104,11 +104,37 @@
|
|
|
104
104
|
</div>
|
|
105
105
|
</div>
|
|
106
106
|
|
|
107
|
+
<%# ── GitHub Issue ── %>
|
|
108
|
+
<% if ErrorRadar.config.github_token.present? && ErrorRadar.config.github_repo.present? %>
|
|
109
|
+
<div class="panel" style="margin-top:16px">
|
|
110
|
+
<h2>GitHub</h2>
|
|
111
|
+
<% has_col = @error.class.column_names.include?('github_issue_url') %>
|
|
112
|
+
<% if has_col && @error.github_issue_url.present? %>
|
|
113
|
+
<a href="<%= @error.github_issue_url %>" target="_blank" rel="noopener"
|
|
114
|
+
class="er-btn" style="background:#24292e;display:inline-block;text-decoration:none">
|
|
115
|
+
△ View GitHub Issue
|
|
116
|
+
</a>
|
|
117
|
+
<% else %>
|
|
118
|
+
<button id="github-btn" class="er-btn" style="background:#24292e" onclick="createGithubIssue()">
|
|
119
|
+
△ Create GitHub Issue
|
|
120
|
+
</button>
|
|
121
|
+
<span id="github-status" style="font-size:13px;color:#6b7280;margin-left:10px"></span>
|
|
122
|
+
<% unless has_col %>
|
|
123
|
+
<p style="font-size:12px;color:#9ca3af;margin:10px 0 0">
|
|
124
|
+
Run <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px">bin/rails generate error_radar:upgrade_v050 && bin/rails db:migrate</code>
|
|
125
|
+
to enable issue tracking.
|
|
126
|
+
</p>
|
|
127
|
+
<% end %>
|
|
128
|
+
<% end %>
|
|
129
|
+
</div>
|
|
130
|
+
<% end %>
|
|
131
|
+
|
|
107
132
|
<script>
|
|
108
133
|
(function () {
|
|
109
134
|
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
|
|
110
135
|
var STATUS_URL = '<%= error_status_path(@error) %>';
|
|
111
136
|
var DELETE_URL = '<%= error_path(@error) %>';
|
|
137
|
+
var GITHUB_URL = '<%= error_github_issue_path(@error) %>';
|
|
112
138
|
|
|
113
139
|
document.querySelectorAll('.status-btn').forEach(function (btn) {
|
|
114
140
|
btn.addEventListener('click', function () { changeStatus(btn.dataset.status, null); });
|
|
@@ -154,6 +180,35 @@
|
|
|
154
180
|
});
|
|
155
181
|
};
|
|
156
182
|
|
|
183
|
+
window.createGithubIssue = function () {
|
|
184
|
+
var btn = document.getElementById('github-btn');
|
|
185
|
+
var status = document.getElementById('github-status');
|
|
186
|
+
if (!btn) return;
|
|
187
|
+
btn.disabled = true;
|
|
188
|
+
status.textContent = 'Creating issue…';
|
|
189
|
+
|
|
190
|
+
fetch(GITHUB_URL, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF }
|
|
193
|
+
})
|
|
194
|
+
.then(function (r) { return r.json(); })
|
|
195
|
+
.then(function (d) {
|
|
196
|
+
if (d.ok) {
|
|
197
|
+
status.innerHTML = '✓ Created: <a href="' + d.url + '" target="_blank">#' + d.number + '</a>';
|
|
198
|
+
btn.style.display = 'none';
|
|
199
|
+
showToast('GitHub issue #' + d.number + ' created', true);
|
|
200
|
+
} else {
|
|
201
|
+
status.textContent = 'Error: ' + (d.error || 'failed');
|
|
202
|
+
btn.disabled = false;
|
|
203
|
+
showToast(d.error || 'GitHub error', false);
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
.catch(function () {
|
|
207
|
+
status.textContent = 'Network error';
|
|
208
|
+
btn.disabled = false;
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
157
212
|
function showToast(msg, ok) {
|
|
158
213
|
var t = document.getElementById('toast');
|
|
159
214
|
t.textContent = msg;
|
data/config/routes.rb
CHANGED
|
@@ -3,9 +3,19 @@
|
|
|
3
3
|
ErrorRadar::Engine.routes.draw do
|
|
4
4
|
root to: 'dashboard#index'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
# REST API — JSON endpoints for CI/CD, external dashboards, scripts
|
|
7
|
+
namespace :api, defaults: { format: :json } do
|
|
8
|
+
resources :errors, only: %i[index show update]
|
|
9
|
+
resource :stats, only: [:show]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Web UI
|
|
13
|
+
get 'errors', to: 'errors#index', as: :errors
|
|
14
|
+
post 'errors/bulk', to: 'errors#bulk', as: :errors_bulk
|
|
15
|
+
post 'errors/:id/github_issue', to: 'errors#create_github_issue', as: :error_github_issue
|
|
16
|
+
get 'errors/:id', to: 'errors#show', as: :error
|
|
17
|
+
patch 'errors/:id/status', to: 'errors#update_status', as: :error_status
|
|
18
|
+
delete 'errors/:id', to: 'errors#destroy'
|
|
19
|
+
|
|
20
|
+
post 'maintenance/purge', to: 'dashboard#purge', as: :maintenance_purge
|
|
11
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,29 @@ 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
|
+
|
|
85
|
+
# REST API ────────────────────────────────────────────────────────────────
|
|
86
|
+
# Bearer token for /api/* endpoints. nil = unauthenticated (not for prod).
|
|
87
|
+
attr_accessor :api_token
|
|
88
|
+
|
|
89
|
+
# GitHub integration ──────────────────────────────────────────────────────
|
|
90
|
+
# Personal access token with repo scope.
|
|
91
|
+
attr_accessor :github_token
|
|
92
|
+
# "owner/repo" string, e.g. "myorg/myapp".
|
|
93
|
+
attr_accessor :github_repo
|
|
94
|
+
|
|
72
95
|
# Custom classification rules. Each is a callable `->(exception) { :category | nil }`.
|
|
73
96
|
# The first rule that returns a non-nil category wins; built-in rules run after.
|
|
74
97
|
attr_accessor :categorizers
|
|
@@ -124,6 +147,15 @@ module ErrorRadar
|
|
|
124
147
|
@app_name = nil
|
|
125
148
|
@app_host = nil
|
|
126
149
|
@error_callbacks = []
|
|
150
|
+
|
|
151
|
+
@async_capture = false
|
|
152
|
+
@capture_job_queue = :default
|
|
153
|
+
@retention_days = nil
|
|
154
|
+
@max_records = nil
|
|
155
|
+
|
|
156
|
+
@api_token = nil
|
|
157
|
+
@github_token = nil
|
|
158
|
+
@github_repo = nil
|
|
127
159
|
@categorizers = []
|
|
128
160
|
@detail_extractors = []
|
|
129
161
|
@expected_servers = []
|
data/lib/error_radar/engine.rb
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module ErrorRadar
|
|
8
|
+
module Integrations
|
|
9
|
+
# Creates GitHub issues from ErrorLog records via the GitHub REST API.
|
|
10
|
+
# Requires a personal access token with the `repo` scope and a repo in
|
|
11
|
+
# "owner/repo" format.
|
|
12
|
+
module Github
|
|
13
|
+
API_BASE = 'https://api.github.com'
|
|
14
|
+
|
|
15
|
+
# Returns the parsed JSON response from GitHub.
|
|
16
|
+
# Raises StandardError on network/API failure.
|
|
17
|
+
def self.create_issue(error_log, token:, repo:)
|
|
18
|
+
owner, repo_name = repo.split('/', 2)
|
|
19
|
+
uri = URI("#{API_BASE}/repos/#{owner}/#{repo_name}/issues")
|
|
20
|
+
|
|
21
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
22
|
+
http.use_ssl = true
|
|
23
|
+
http.open_timeout = 10
|
|
24
|
+
http.read_timeout = 10
|
|
25
|
+
|
|
26
|
+
req = Net::HTTP::Post.new(uri)
|
|
27
|
+
req['Authorization'] = "Bearer #{token}"
|
|
28
|
+
req['Content-Type'] = 'application/json'
|
|
29
|
+
req['Accept'] = 'application/vnd.github+json'
|
|
30
|
+
req['X-GitHub-Api-Version'] = '2022-11-28'
|
|
31
|
+
req.body = {
|
|
32
|
+
title: issue_title(error_log),
|
|
33
|
+
body: issue_body(error_log),
|
|
34
|
+
labels: ['bug', 'error-radar']
|
|
35
|
+
}.to_json
|
|
36
|
+
|
|
37
|
+
response = http.request(req)
|
|
38
|
+
JSON.parse(response.body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.issue_title(log)
|
|
42
|
+
short_msg = log.message.to_s.truncate(80).gsub(/\n/, ' ')
|
|
43
|
+
"[Error Radar] #{log.error_class}: #{short_msg}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.issue_body(log)
|
|
47
|
+
url = ErrorRadar::Notifier.error_url(log)
|
|
48
|
+
backtrace = log.backtrace.to_s.split("\n").first(20).join("\n")
|
|
49
|
+
|
|
50
|
+
parts = []
|
|
51
|
+
parts << "## Error Details\n"
|
|
52
|
+
parts << "| Field | Value |"
|
|
53
|
+
parts << "|-------|-------|"
|
|
54
|
+
parts << "| **Error Class** | `#{log.error_class}` |"
|
|
55
|
+
parts << "| **Source** | #{log.source || 'unknown'} |"
|
|
56
|
+
parts << "| **Category** | #{log.category} |"
|
|
57
|
+
parts << "| **Severity** | #{log.severity} |"
|
|
58
|
+
parts << "| **Occurrences** | #{log.occurrences} |"
|
|
59
|
+
parts << "| **First seen** | #{log.first_seen_at&.strftime('%Y-%m-%d %H:%M UTC')} |"
|
|
60
|
+
parts << "| **Last seen** | #{log.last_seen_at&.strftime('%Y-%m-%d %H:%M UTC')} |"
|
|
61
|
+
parts << "\n## Message\n\n```\n#{log.message.to_s.truncate(1000)}\n```"
|
|
62
|
+
|
|
63
|
+
unless backtrace.empty?
|
|
64
|
+
parts << "\n## Backtrace\n\n```\n#{backtrace}\n```"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
parts << "\n## Link\n\n[View in Error Radar](#{url})" if url
|
|
68
|
+
|
|
69
|
+
parts << "\n---\n*Created automatically by [Error Radar](https://github.com/chienbn9x/error_radar)*"
|
|
70
|
+
parts.join("\n")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/error_radar/tracking.rb
CHANGED
|
@@ -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}")
|
data/lib/error_radar/version.rb
CHANGED
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'
|
|
@@ -72,6 +72,36 @@ ErrorRadar.configure do |config|
|
|
|
72
72
|
# config.expected_servers = [
|
|
73
73
|
# { key: 'web', name: 'Web', tag: 'sidekiq_web', host: 'web', queue_hint: 'low' }
|
|
74
74
|
# ]
|
|
75
|
+
|
|
76
|
+
# --- REST API ---
|
|
77
|
+
# Protect /api/* endpoints with a Bearer token.
|
|
78
|
+
# config.api_token = ENV['ERROR_RADAR_API_TOKEN']
|
|
79
|
+
# curl -H "Authorization: Bearer $TOKEN" https://myapp.com/error_radar/api/stats
|
|
80
|
+
|
|
81
|
+
# --- GitHub Integration ---
|
|
82
|
+
# Creates GitHub issues directly from the error detail page.
|
|
83
|
+
# Requires running: bin/rails generate error_radar:upgrade_v050 && bin/rails db:migrate
|
|
84
|
+
# config.github_token = ENV['GITHUB_TOKEN'] # PAT with repo scope
|
|
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
|
|
75
105
|
end
|
|
76
106
|
|
|
77
107
|
# ActiveJob is now captured automatically via install_active_job = true above.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
|
|
6
|
+
module ErrorRadar
|
|
7
|
+
module Generators
|
|
8
|
+
class UpgradeV050Generator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
|
+
|
|
13
|
+
desc 'Generates the migration for Error Radar v0.5.0 (adds github_issue_url column).'
|
|
14
|
+
|
|
15
|
+
def self.next_migration_number(dirname)
|
|
16
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_migration_file
|
|
20
|
+
migration_template(
|
|
21
|
+
'add_github_issue_to_error_radar_error_logs.rb.tt',
|
|
22
|
+
'db/migrate/add_github_issue_to_error_radar_error_logs.rb'
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- chienbn9x
|
|
@@ -57,6 +57,9 @@ files:
|
|
|
57
57
|
- MIT-LICENSE
|
|
58
58
|
- README.md
|
|
59
59
|
- Rakefile
|
|
60
|
+
- app/controllers/error_radar/api/base_controller.rb
|
|
61
|
+
- app/controllers/error_radar/api/errors_controller.rb
|
|
62
|
+
- app/controllers/error_radar/api/stats_controller.rb
|
|
60
63
|
- app/controllers/error_radar/application_controller.rb
|
|
61
64
|
- app/controllers/error_radar/dashboard_controller.rb
|
|
62
65
|
- app/controllers/error_radar/errors_controller.rb
|
|
@@ -72,9 +75,12 @@ files:
|
|
|
72
75
|
- app/views/layouts/error_radar/application.html.erb
|
|
73
76
|
- config/routes.rb
|
|
74
77
|
- lib/error_radar.rb
|
|
78
|
+
- lib/error_radar/capture_job.rb
|
|
79
|
+
- lib/error_radar/cleanup.rb
|
|
75
80
|
- lib/error_radar/configuration.rb
|
|
76
81
|
- lib/error_radar/engine.rb
|
|
77
82
|
- lib/error_radar/integrations/active_job.rb
|
|
83
|
+
- lib/error_radar/integrations/github.rb
|
|
78
84
|
- lib/error_radar/integrations/rails_admin.rb
|
|
79
85
|
- lib/error_radar/integrations/rake.rb
|
|
80
86
|
- lib/error_radar/integrations/sidekiq.rb
|
|
@@ -90,6 +96,9 @@ files:
|
|
|
90
96
|
- lib/generators/error_radar/install/install_generator.rb
|
|
91
97
|
- lib/generators/error_radar/install/templates/create_error_radar_error_logs.rb.tt
|
|
92
98
|
- lib/generators/error_radar/install/templates/initializer.rb
|
|
99
|
+
- lib/generators/error_radar/upgrade_v050/templates/add_github_issue_to_error_radar_error_logs.rb.tt
|
|
100
|
+
- lib/generators/error_radar/upgrade_v050/upgrade_v050_generator.rb
|
|
101
|
+
- lib/tasks/error_radar.rake
|
|
93
102
|
homepage: https://github.com/chienbn9x/error_radar
|
|
94
103
|
licenses:
|
|
95
104
|
- MIT
|