error_radar 0.4.0 → 0.5.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 +29 -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/errors_controller.rb +34 -1
- data/app/views/error_radar/errors/show.html.erb +55 -0
- data/config/routes.rb +13 -5
- data/lib/error_radar/configuration.rb +14 -0
- data/lib/error_radar/integrations/github.rb +74 -0
- data/lib/error_radar/version.rb +1 -1
- data/lib/generators/error_radar/install/templates/initializer.rb +11 -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
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 106881790c16306716c7588f317da722c0e589db0a0a2bbb6e7ad838c9a8d118
|
|
4
|
+
data.tar.gz: d4c9ccf1363b88637dc625418daf8553e15aad0796f42f79aadc6eafb3a4f554
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c880963d54ef38274dbc226b970f848f7a9ced038c6d88ababffe1b3eb71ef2d8490695a0a256775a500e707a72b9af2a017a066e8af1f66b17ab842a18ac726
|
|
7
|
+
data.tar.gz: ba612e7cc8a547cd1bf7ffb8704347636bccc7f763fba3a73fbfe1919b86573421873b61c88f6c8b44da0ed661cf8cf73d4da62a54998828dfe1a767642580ad
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.0] - 2026-07-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **REST API** at `/api/*` for external integrations (CI/CD, dashboards, scripts):
|
|
9
|
+
- `GET /api/errors` — paginated list with the same filters as the web UI
|
|
10
|
+
(`status`, `severity`, `category`, `q`, `from`, `to`, `sort`, `order`, `page`)
|
|
11
|
+
- `GET /api/errors/:id` — full detail including context, backtrace, and
|
|
12
|
+
`github_issue_url` (if column is present)
|
|
13
|
+
- `PATCH /api/errors/:id` — update status; resolve accepts optional `note`
|
|
14
|
+
and `resolved_by` params
|
|
15
|
+
- `GET /api/stats` — summary counts by status, severity, and category
|
|
16
|
+
- **Bearer-token API auth**: set `config.api_token` to protect all `/api/*`
|
|
17
|
+
endpoints with `Authorization: Bearer <token>`.
|
|
18
|
+
- **GitHub Issue integration**: "Create GitHub Issue" button on the error detail
|
|
19
|
+
page opens a pre-filled issue with error class, source, message, backtrace,
|
|
20
|
+
and a deep-link back to Error Radar. Requires `config.github_token` and
|
|
21
|
+
`config.github_repo`.
|
|
22
|
+
- **`github_issue_url` column**: stored on the error row so the button becomes
|
|
23
|
+
"View GitHub Issue" once an issue exists. Requires running the upgrade
|
|
24
|
+
migration: `bin/rails generate error_radar:upgrade_v050 && bin/rails db:migrate`.
|
|
25
|
+
- **`error_radar:upgrade_v050` generator**: generates the migration that adds
|
|
26
|
+
`github_issue_url` to `error_radar_error_logs`.
|
|
27
|
+
|
|
28
|
+
### Notes
|
|
29
|
+
- The GitHub column is optional — the integration is gracefully degraded when the
|
|
30
|
+
migration has not been run (button appears but URL is not persisted).
|
|
31
|
+
- The API controllers live in `ErrorRadar::Api::*` to avoid polluting the host
|
|
32
|
+
app's controller namespace.
|
|
33
|
+
|
|
5
34
|
## [0.4.0] - 2026-07-03
|
|
6
35
|
|
|
7
36
|
### 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
|
|
@@ -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
|
|
@@ -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,17 @@
|
|
|
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'
|
|
11
19
|
end
|
|
@@ -69,6 +69,16 @@ module ErrorRadar
|
|
|
69
69
|
@error_callbacks << block
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# REST API ────────────────────────────────────────────────────────────────
|
|
73
|
+
# Bearer token for /api/* endpoints. nil = unauthenticated (not for prod).
|
|
74
|
+
attr_accessor :api_token
|
|
75
|
+
|
|
76
|
+
# GitHub integration ──────────────────────────────────────────────────────
|
|
77
|
+
# Personal access token with repo scope.
|
|
78
|
+
attr_accessor :github_token
|
|
79
|
+
# "owner/repo" string, e.g. "myorg/myapp".
|
|
80
|
+
attr_accessor :github_repo
|
|
81
|
+
|
|
72
82
|
# Custom classification rules. Each is a callable `->(exception) { :category | nil }`.
|
|
73
83
|
# The first rule that returns a non-nil category wins; built-in rules run after.
|
|
74
84
|
attr_accessor :categorizers
|
|
@@ -124,6 +134,10 @@ module ErrorRadar
|
|
|
124
134
|
@app_name = nil
|
|
125
135
|
@app_host = nil
|
|
126
136
|
@error_callbacks = []
|
|
137
|
+
|
|
138
|
+
@api_token = nil
|
|
139
|
+
@github_token = nil
|
|
140
|
+
@github_repo = nil
|
|
127
141
|
@categorizers = []
|
|
128
142
|
@detail_extractors = []
|
|
129
143
|
@expected_servers = []
|
|
@@ -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/version.rb
CHANGED
|
@@ -72,6 +72,17 @@ 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
|
|
75
86
|
end
|
|
76
87
|
|
|
77
88
|
# 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
|
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.5.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
|
|
@@ -75,6 +78,7 @@ files:
|
|
|
75
78
|
- lib/error_radar/configuration.rb
|
|
76
79
|
- lib/error_radar/engine.rb
|
|
77
80
|
- lib/error_radar/integrations/active_job.rb
|
|
81
|
+
- lib/error_radar/integrations/github.rb
|
|
78
82
|
- lib/error_radar/integrations/rails_admin.rb
|
|
79
83
|
- lib/error_radar/integrations/rake.rb
|
|
80
84
|
- lib/error_radar/integrations/sidekiq.rb
|
|
@@ -90,6 +94,8 @@ files:
|
|
|
90
94
|
- lib/generators/error_radar/install/install_generator.rb
|
|
91
95
|
- lib/generators/error_radar/install/templates/create_error_radar_error_logs.rb.tt
|
|
92
96
|
- lib/generators/error_radar/install/templates/initializer.rb
|
|
97
|
+
- lib/generators/error_radar/upgrade_v050/templates/add_github_issue_to_error_radar_error_logs.rb.tt
|
|
98
|
+
- lib/generators/error_radar/upgrade_v050/upgrade_v050_generator.rb
|
|
93
99
|
homepage: https://github.com/chienbn9x/error_radar
|
|
94
100
|
licenses:
|
|
95
101
|
- MIT
|