error_radar 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 131362a3ccb1bd3de4242f28690bb60059555bee0357ff488fbcce86f9b04f37
4
- data.tar.gz: 8f0218c89a83e54e514657dd1c7f9e25a28c39d0adfe284424efdb134bc58467
3
+ metadata.gz: 106881790c16306716c7588f317da722c0e589db0a0a2bbb6e7ad838c9a8d118
4
+ data.tar.gz: d4c9ccf1363b88637dc625418daf8553e15aad0796f42f79aadc6eafb3a4f554
5
5
  SHA512:
6
- metadata.gz: 43ade7e6e0d65e66af9cc34bfa3ee6768fdd9cbad0d79772df01ea6f355b7647f1e5c7a8d8be4667a80abfa5e1c383becc4c97651709c21df2e58fb65a2fede0
7
- data.tar.gz: ec4cf4f0863aa19ffa5639659b6a61642921d72462130caaff18a6c2bb2f9424594d9ebbab96dc65c974fab1e4b00b91db96573c3434949ec769ab7b46ef2918
6
+ metadata.gz: c880963d54ef38274dbc226b970f848f7a9ced038c6d88ababffe1b3eb71ef2d8490695a0a256775a500e707a72b9af2a017a066e8af1f66b17ab842a18ac726
7
+ data.tar.gz: ba612e7cc8a547cd1bf7ffb8704347636bccc7f763fba3a73fbfe1919b86573421873b61c88f6c8b44da0ed661cf8cf73d4da62a54998828dfe1a767642580ad
data/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
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
+
34
+ ## [0.4.0] - 2026-07-03
35
+
36
+ ### Added
37
+ - **Slack notifications**: sends a Block Kit message with error details and a
38
+ deep-link button. Configure with `config.slack_webhook_url` and optional
39
+ `config.slack_channel`.
40
+ - **Discord notifications**: sends a rich embed to any Discord webhook.
41
+ Configure with `config.discord_webhook_url`.
42
+ - **Email notifications**: delivers an HTML + plain-text email via ActionMailer.
43
+ Configure `config.email_recipients` and `config.email_from`.
44
+ - **Generic webhook**: POSTs a JSON payload to any URL (PagerDuty, OpsGenie,
45
+ custom scripts). Configure with `config.webhook_urls = [url1, url2]`.
46
+ - **Custom callbacks**: `config.on_error { |log| ... }` for arbitrary alerting
47
+ logic (e.g. PagerDuty SDK, Telegram, SMS).
48
+ - **`notify_on` rule set**: controls when alerts fire — `:new_error` (default,
49
+ first occurrence per fingerprint), `:critical` (any critical severity, 1/hour
50
+ throttle), `:all` (every occurrence, 1/hour throttle per fingerprint).
51
+ - **In-memory throttle**: prevents notification storms for `:critical` and `:all`
52
+ rules — at most one alert per fingerprint per hour.
53
+ - **Deep-links in notifications**: set `config.app_host` to include a link to
54
+ the error detail page in every notification.
55
+ - **`ErrorLog#new_fingerprint?`**: transient predicate (not persisted) that is
56
+ `true` when the record was just created for the first time.
57
+
5
58
  ## [0.3.0] - 2026-07-03
6
59
 
7
60
  ### 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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ class ErrorMailer < ActionMailer::Base
5
+ layout false
6
+
7
+ def new_error(error_log)
8
+ @error = error_log
9
+ @is_new = error_log.new_fingerprint?
10
+ @url = build_url
11
+ @app_name = ErrorRadar::Notifier.app_name
12
+
13
+ prefix = @is_new ? 'New' : 'Critical'
14
+ subject = "[#{@app_name}] #{prefix} #{@error.severity}: #{@error.error_class}"
15
+
16
+ mail(
17
+ to: ErrorRadar.config.email_recipients,
18
+ from: ErrorRadar.config.email_from || 'noreply@localhost',
19
+ subject: subject
20
+ )
21
+ end
22
+
23
+ private
24
+
25
+ def build_url
26
+ host = ErrorRadar.config.app_host.to_s.chomp('/')
27
+ return nil if host.empty?
28
+
29
+ "#{host}/error_radar/errors/#{@error.id}"
30
+ end
31
+ end
32
+ end
@@ -29,13 +29,18 @@ module ErrorRadar
29
29
  # Record (or roll-up) an error. Idempotent per fingerprint: identical errors
30
30
  # increment `occurrences` and bump `last_seen_at` instead of creating a new
31
31
  # row. NEVER raises — logging must not break the calling code path.
32
+ def new_fingerprint?
33
+ @new_fingerprint || false
34
+ end
35
+
32
36
  def self.record(category:, message:, severity: :error, error_class: nil, source: nil,
33
37
  backtrace: nil, context: {}, http_status: nil, request_url: nil,
34
38
  api_code: nil, api_subcode: nil, fingerprint: nil)
35
39
  now = Time.current
36
40
  fp = presence(fingerprint) || build_fingerprint(category: category, error_class: error_class, source: source, message: message)
37
41
 
38
- log = find_or_initialize_by(fingerprint: fp)
42
+ log = find_or_initialize_by(fingerprint: fp)
43
+ new_fingerprint = !log.persisted?
39
44
 
40
45
  if log.persisted?
41
46
  log.occurrences += 1
@@ -54,6 +59,7 @@ module ErrorRadar
54
59
  log.severity = severity if log.new_record? || severity_rank(severity) > severity_rank(log.severity)
55
60
  log.last_seen_at = now
56
61
  log.save!
62
+ log.instance_variable_set(:@new_fingerprint, new_fingerprint)
57
63
  log
58
64
  rescue StandardError => e
59
65
  ErrorRadar::Tracking.warn_internal("ErrorLog.record failed: #{e.class}: #{e.message}")
@@ -0,0 +1,82 @@
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
+ <style>
7
+ body{margin:0;padding:20px;background:#f4f6f8;font-family:-apple-system,"Segoe UI",Roboto,Arial,sans-serif;color:#1f2933}
8
+ .container{max-width:600px;margin:0 auto}
9
+ .header{background:#1f2933;color:#fff;padding:24px;border-radius:10px 10px 0 0}
10
+ .header h1{margin:0 0 6px;font-size:18px}
11
+ .header .meta{color:#9ca3af;font-size:12px}
12
+ .body{background:#fff;padding:24px;border-radius:0 0 10px 10px}
13
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
14
+ .field .label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#9ca3af;margin-bottom:3px}
15
+ .field .value{font-size:14px;color:#1f2933}
16
+ .badge{display:inline-block;padding:3px 10px;border-radius:10px;color:#fff;font-size:12px;font-weight:600}
17
+ .msg-box{background:#f8fafc;border-left:4px solid #dc3545;padding:12px 14px;border-radius:0 8px 8px 0;font-family:ui-monospace,monospace;font-size:12px;white-space:pre-wrap;word-break:break-word;color:#374151;margin:16px 0}
18
+ .btn{display:inline-block;background:#2563eb;color:#fff!important;padding:11px 22px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600;margin-top:8px}
19
+ .footer{margin-top:20px;font-size:11px;color:#9ca3af;text-align:center}
20
+ <%
21
+ sev_colors = { 'critical' => '#7b001c', 'error' => '#dc3545', 'warning' => '#fd7e14', 'info' => '#17a2b8' }
22
+ %>
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div class="container">
27
+ <div class="header">
28
+ <h1><%= @is_new ? '🆕 New error detected' : '🔁 Critical error recurring' %></h1>
29
+ <div class="meta"><%= @app_name %> · <%= @error.last_seen_at&.strftime('%Y-%m-%d %H:%M UTC') %></div>
30
+ </div>
31
+
32
+ <div class="body">
33
+ <div class="field" style="margin-bottom:20px">
34
+ <div class="label">Error class</div>
35
+ <div class="value" style="font-family:ui-monospace,monospace;font-size:17px;font-weight:700"><%= @error.error_class %></div>
36
+ </div>
37
+
38
+ <div class="grid">
39
+ <div class="field">
40
+ <div class="label">Source</div>
41
+ <div class="value"><%= @error.source || 'unknown' %></div>
42
+ </div>
43
+ <div class="field">
44
+ <div class="label">Category</div>
45
+ <div class="value"><%= @error.category.to_s.humanize %></div>
46
+ </div>
47
+ <div class="field">
48
+ <div class="label">Severity</div>
49
+ <div class="value">
50
+ <span class="badge" style="background:<%= sev_colors[@error.severity] || '#6c757d' %>"><%= @error.severity %></span>
51
+ </div>
52
+ </div>
53
+ <div class="field">
54
+ <div class="label">Occurrences</div>
55
+ <div class="value" style="font-weight:700;font-size:16px"><%= @error.occurrences %></div>
56
+ </div>
57
+ <div class="field">
58
+ <div class="label">First seen</div>
59
+ <div class="value"><%= @error.first_seen_at&.strftime('%Y-%m-%d %H:%M') %></div>
60
+ </div>
61
+ <div class="field">
62
+ <div class="label">Last seen</div>
63
+ <div class="value"><%= @error.last_seen_at&.strftime('%Y-%m-%d %H:%M') %></div>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="field">
68
+ <div class="label">Message</div>
69
+ <div class="msg-box"><%= @error.message.to_s.truncate(600) %></div>
70
+ </div>
71
+
72
+ <% if @url %>
73
+ <a href="<%= @url %>" class="btn">View in Error Radar →</a>
74
+ <% end %>
75
+ </div>
76
+
77
+ <div class="footer">
78
+ Sent by <strong>Error Radar</strong> · <%= @app_name %>
79
+ </div>
80
+ </div>
81
+ </body>
82
+ </html>
@@ -0,0 +1,21 @@
1
+ [<%= @app_name %>] <%= @is_new ? 'New error' : 'Critical error recurring' %>: <%= @error.error_class %>
2
+ <%= '=' * 60 %>
3
+
4
+ Error class: <%= @error.error_class %>
5
+ Source: <%= @error.source || 'unknown' %>
6
+ Category: <%= @error.category %>
7
+ Severity: <%= @error.severity %>
8
+ Occurrences: <%= @error.occurrences %>
9
+ First seen: <%= @error.first_seen_at&.strftime('%Y-%m-%d %H:%M UTC') %>
10
+ Last seen: <%= @error.last_seen_at&.strftime('%Y-%m-%d %H:%M UTC') %>
11
+
12
+ Message:
13
+ <%= @error.message.to_s.truncate(1000) %>
14
+ <% if @url %>
15
+
16
+ View in Error Radar:
17
+ <%= @url %>
18
+ <% end %>
19
+
20
+ --
21
+ Sent by Error Radar · <%= @app_name %>
@@ -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
+ &#9651; View GitHub Issue
116
+ </a>
117
+ <% else %>
118
+ <button id="github-btn" class="er-btn" style="background:#24292e" onclick="createGithubIssue()">
119
+ &#9651; 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 &amp;&amp; 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
- get 'errors', to: 'errors#index', as: :errors
7
- post 'errors/bulk', to: 'errors#bulk', as: :errors_bulk
8
- get 'errors/:id', to: 'errors#show', as: :error
9
- patch 'errors/:id/status', to: 'errors#update_status', as: :error_status
10
- delete 'errors/:id', to: 'errors#destroy'
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
@@ -35,6 +35,50 @@ module ErrorRadar
35
35
  attr_accessor :install_middleware, :install_sidekiq, :install_rails_admin,
36
36
  :install_active_job, :install_rake
37
37
 
38
+ # Notifications ─────────────────────────────────────────────────────────
39
+ # When to fire: :new_error (first time a fingerprint is seen),
40
+ # :critical (any critical-severity occurrence, throttled),
41
+ # :all (every occurrence, throttled to 1/hour/fingerprint)
42
+ attr_accessor :notify_on
43
+
44
+ # Slack incoming-webhook URL (https://hooks.slack.com/services/...)
45
+ attr_accessor :slack_webhook_url
46
+ # Override target channel. Leave nil to use webhook's default channel.
47
+ attr_accessor :slack_channel
48
+
49
+ # Discord incoming-webhook URL
50
+ attr_accessor :discord_webhook_url
51
+
52
+ # ActionMailer recipients. Requires ActionMailer to be configured in host app.
53
+ attr_accessor :email_recipients
54
+ # From address for notification emails.
55
+ attr_accessor :email_from
56
+
57
+ # One or more plain HTTPS URLs that receive a POST with JSON body on each alert.
58
+ attr_accessor :webhook_urls
59
+
60
+ # App name shown in notification subject/title. Defaults to Rails app name.
61
+ attr_accessor :app_name
62
+ # Base URL used to build deep-links (e.g. "https://myapp.com").
63
+ attr_accessor :app_host
64
+
65
+ # Custom callbacks: ->(error_log) { ... }. Called after built-in channels.
66
+ attr_reader :error_callbacks
67
+
68
+ def on_error(&block)
69
+ @error_callbacks << block
70
+ end
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
+
38
82
  # Custom classification rules. Each is a callable `->(exception) { :category | nil }`.
39
83
  # The first rule that returns a non-nil category wins; built-in rules run after.
40
84
  attr_accessor :categorizers
@@ -79,6 +123,21 @@ module ErrorRadar
79
123
  @install_rails_admin = true
80
124
  @install_active_job = true
81
125
  @install_rake = true
126
+
127
+ @notify_on = [:new_error]
128
+ @slack_webhook_url = nil
129
+ @slack_channel = nil
130
+ @discord_webhook_url = nil
131
+ @email_recipients = []
132
+ @email_from = nil
133
+ @webhook_urls = []
134
+ @app_name = nil
135
+ @app_host = nil
136
+ @error_callbacks = []
137
+
138
+ @api_token = nil
139
+ @github_token = nil
140
+ @github_repo = nil
82
141
  @categorizers = []
83
142
  @detail_extractors = []
84
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
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module ErrorRadar
8
+ module Notifications
9
+ module Discord
10
+ SEV_COLORS = {
11
+ 'critical' => 0x7b001c,
12
+ 'error' => 0xdc3545,
13
+ 'warning' => 0xfd7e14,
14
+ 'info' => 0x17a2b8
15
+ }.freeze
16
+
17
+ def self.deliver(log)
18
+ url = URI(ErrorRadar.config.discord_webhook_url)
19
+ payload = build_payload(log)
20
+
21
+ http = Net::HTTP.new(url.host, url.port)
22
+ http.use_ssl = url.scheme == 'https'
23
+ http.open_timeout = 5
24
+ http.read_timeout = 5
25
+
26
+ req = Net::HTTP::Post.new(url)
27
+ req['Content-Type'] = 'application/json'
28
+ req.body = payload.to_json
29
+
30
+ http.request(req)
31
+ rescue StandardError => e
32
+ ErrorRadar::Tracking.warn_internal("Discord notification failed: #{e.message}")
33
+ end
34
+
35
+ def self.build_payload(log)
36
+ app = ErrorRadar::Notifier.app_name
37
+ url = ErrorRadar::Notifier.error_url(log)
38
+ prefix = log.new_fingerprint? ? '🆕 New error' : '🔁 Critical error'
39
+
40
+ fields = [
41
+ { name: 'Source', value: (log.source || 'unknown').truncate(100), inline: true },
42
+ { name: 'Category', value: log.category.to_s, inline: true },
43
+ { name: 'Severity', value: log.severity.to_s, inline: true },
44
+ { name: 'Occurrences', value: log.occurrences.to_s, inline: true }
45
+ ]
46
+ fields << { name: 'View', value: "[Error Radar](#{url})", inline: false } if url
47
+
48
+ embed = {
49
+ title: "#{prefix}: #{log.error_class}",
50
+ description: "```#{log.message.to_s.truncate(300)}```",
51
+ color: SEV_COLORS[log.severity] || 0x6c757d,
52
+ fields: fields,
53
+ footer: { text: "[#{app}] Error Radar" },
54
+ timestamp: log.last_seen_at&.iso8601
55
+ }.compact
56
+
57
+ { embeds: [embed] }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ module Notifications
5
+ # Delivers notification emails via ActionMailer (must be configured in
6
+ # the host app). Uses deliver_later so it doesn't block the request cycle.
7
+ module Email
8
+ def self.deliver(log)
9
+ return unless defined?(::ActionMailer::Base)
10
+
11
+ require 'error_radar/mailers/error_mailer'
12
+ ErrorRadar::ErrorMailer.new_error(log).deliver_later
13
+ rescue StandardError => e
14
+ ErrorRadar::Tracking.warn_internal("Email notification failed: #{e.message}")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module ErrorRadar
8
+ module Notifications
9
+ module Slack
10
+ SEV_EMOJI = {
11
+ 'critical' => ':red_circle:',
12
+ 'error' => ':large_orange_circle:',
13
+ 'warning' => ':large_yellow_circle:',
14
+ 'info' => ':large_blue_circle:'
15
+ }.freeze
16
+
17
+ def self.deliver(log)
18
+ url = URI(ErrorRadar.config.slack_webhook_url)
19
+ payload = build_payload(log)
20
+
21
+ http = Net::HTTP.new(url.host, url.port)
22
+ http.use_ssl = url.scheme == 'https'
23
+ http.open_timeout = 5
24
+ http.read_timeout = 5
25
+
26
+ req = Net::HTTP::Post.new(url)
27
+ req['Content-Type'] = 'application/json'
28
+ req.body = payload.to_json
29
+
30
+ http.request(req)
31
+ rescue StandardError => e
32
+ ErrorRadar::Tracking.warn_internal("Slack notification failed: #{e.message}")
33
+ end
34
+
35
+ def self.build_payload(log)
36
+ emoji = SEV_EMOJI[log.severity] || ':white_circle:'
37
+ title = "#{emoji} #{log.new_fingerprint? ? 'New error' : 'Critical error'}: *#{log.error_class}*"
38
+ url = ErrorRadar::Notifier.error_url(log)
39
+ app = ErrorRadar::Notifier.app_name
40
+ channel = ErrorRadar.config.slack_channel
41
+
42
+ blocks = [
43
+ { type: 'section', text: { type: 'mrkdwn', text: "*[#{app}]* #{title}" } },
44
+ {
45
+ type: 'section',
46
+ fields: [
47
+ { type: 'mrkdwn', text: "*Source*\n#{(log.source || 'unknown').truncate(80)}" },
48
+ { type: 'mrkdwn', text: "*Category*\n#{log.category}" },
49
+ { type: 'mrkdwn', text: "*Severity*\n#{log.severity}" },
50
+ { type: 'mrkdwn', text: "*Occurrences*\n#{log.occurrences}" }
51
+ ]
52
+ },
53
+ {
54
+ type: 'section',
55
+ text: { type: 'mrkdwn', text: "*Message*\n```#{log.message.to_s.truncate(400)}```" }
56
+ }
57
+ ]
58
+
59
+ if url
60
+ blocks << {
61
+ type: 'actions',
62
+ elements: [{
63
+ type: 'button',
64
+ text: { type: 'plain_text', text: 'View in Error Radar', emoji: true },
65
+ url: url
66
+ }]
67
+ }
68
+ end
69
+
70
+ payload = { blocks: blocks, text: "[#{app}] #{log.error_class}: #{log.message.to_s.truncate(200)}" }
71
+ payload[:channel] = channel if channel.to_s != ''
72
+ payload
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module ErrorRadar
8
+ module Notifications
9
+ # Generic outbound webhook. POSTs a JSON payload to any URL.
10
+ # Useful for PagerDuty, OpsGenie, custom scripts, etc.
11
+ module Webhook
12
+ def self.deliver(log, url:)
13
+ uri = URI(url)
14
+
15
+ http = Net::HTTP.new(uri.host, uri.port)
16
+ http.use_ssl = uri.scheme == 'https'
17
+ http.open_timeout = 5
18
+ http.read_timeout = 5
19
+
20
+ req = Net::HTTP::Post.new(uri)
21
+ req['Content-Type'] = 'application/json'
22
+ req['User-Agent'] = "ErrorRadar/#{ErrorRadar::VERSION}"
23
+ req.body = build_payload(log).to_json
24
+
25
+ http.request(req)
26
+ rescue StandardError => e
27
+ ErrorRadar::Tracking.warn_internal("Webhook (#{url}) notification failed: #{e.message}")
28
+ end
29
+
30
+ def self.build_payload(log)
31
+ {
32
+ event: log.new_fingerprint? ? 'new_error' : 'recurring_error',
33
+ app: ErrorRadar::Notifier.app_name,
34
+ error_class: log.error_class,
35
+ source: log.source,
36
+ category: log.category,
37
+ severity: log.severity,
38
+ message: log.message.to_s.truncate(500),
39
+ occurrences: log.occurrences,
40
+ fingerprint: log.fingerprint,
41
+ first_seen_at: log.first_seen_at&.iso8601,
42
+ last_seen_at: log.last_seen_at&.iso8601,
43
+ url: ErrorRadar::Notifier.error_url(log)
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ # Dispatches alert notifications (Slack, Discord, email, webhooks, custom
5
+ # callbacks) after an ErrorLog is persisted. Called from Tracking.capture and
6
+ # Tracking.notify. Never raises — a broken notification must not affect the
7
+ # host application.
8
+ module Notifier
9
+ THROTTLE_MUTEX = Mutex.new
10
+ # fingerprint => Time of last notification (in-memory, resets on restart)
11
+ THROTTLE = {}
12
+ THROTTLE_INTERVAL = 3600 # seconds between repeat alerts per fingerprint
13
+
14
+ def self.dispatch(log)
15
+ return unless ErrorRadar.config.enabled
16
+ return unless should_fire?(log)
17
+
18
+ cfg = ErrorRadar.config
19
+ send_slack(log) if cfg.slack_webhook_url.to_s.start_with?('http')
20
+ send_discord(log) if cfg.discord_webhook_url.to_s.start_with?('http')
21
+ send_email(log) if cfg.email_recipients.any?
22
+ cfg.webhook_urls.each { |url| send_webhook(log, url) }
23
+ cfg.error_callbacks.each { |cb| safe_call(cb, log) }
24
+ rescue StandardError => e
25
+ ErrorRadar::Tracking.warn_internal("Notifier.dispatch failed: #{e.message}")
26
+ end
27
+
28
+ # ── Fire decision ──────────────────────────────────────────────────────
29
+ def self.should_fire?(log)
30
+ rules = Array(ErrorRadar.config.notify_on).map(&:to_sym)
31
+ return false if rules.empty?
32
+
33
+ # :new_error — fire exactly once per fingerprint (no throttle needed)
34
+ return true if rules.include?(:new_error) && log.new_fingerprint?
35
+
36
+ # :critical / :all — fire for recurring events, subject to throttle
37
+ matches_severity = rules.include?(:all) ||
38
+ (rules.include?(:critical) && log.severity_critical?)
39
+ matches_severity && throttle_ok?(log.fingerprint)
40
+ end
41
+
42
+ def self.throttle_ok?(fingerprint)
43
+ THROTTLE_MUTEX.synchronize do
44
+ last = THROTTLE[fingerprint]
45
+ ok = last.nil? || (Time.current - last) >= THROTTLE_INTERVAL
46
+ THROTTLE[fingerprint] = Time.current if ok
47
+ ok
48
+ end
49
+ end
50
+
51
+ # ── Channel dispatchers ────────────────────────────────────────────────
52
+ def self.send_slack(log)
53
+ require 'error_radar/notifications/slack'
54
+ Notifications::Slack.deliver(log)
55
+ end
56
+
57
+ def self.send_discord(log)
58
+ require 'error_radar/notifications/discord'
59
+ Notifications::Discord.deliver(log)
60
+ end
61
+
62
+ def self.send_email(log)
63
+ require 'error_radar/notifications/email'
64
+ Notifications::Email.deliver(log)
65
+ end
66
+
67
+ def self.send_webhook(log, url)
68
+ require 'error_radar/notifications/webhook'
69
+ Notifications::Webhook.deliver(log, url: url)
70
+ end
71
+
72
+ def self.safe_call(cb, log)
73
+ cb.call(log)
74
+ rescue StandardError => e
75
+ ErrorRadar::Tracking.warn_internal("on_error callback failed: #{e.message}")
76
+ end
77
+
78
+ # Build a deep-link URL to the error detail page.
79
+ def self.error_url(log)
80
+ host = ErrorRadar.config.app_host.to_s.chomp('/')
81
+ return nil if host.empty?
82
+
83
+ "#{host}/error_radar/errors/#{log.id}"
84
+ end
85
+
86
+ def self.app_name
87
+ ErrorRadar.config.app_name ||
88
+ (defined?(Rails) && Rails.application ? Rails.application.class.module_parent_name : 'App')
89
+ end
90
+ end
91
+ end
@@ -27,7 +27,9 @@ module ErrorRadar
27
27
  attrs.merge!(extra.compact) if extra.is_a?(Hash)
28
28
  end
29
29
 
30
- ErrorRadar::ErrorLog.record(**attrs)
30
+ log = ErrorRadar::ErrorLog.record(**attrs)
31
+ ErrorRadar::Notifier.dispatch(log) if log
32
+ log
31
33
  rescue StandardError => e
32
34
  warn_internal("capture failed: #{e.class}: #{e.message}")
33
35
  nil
@@ -47,7 +49,9 @@ module ErrorRadar
47
49
  def notify(message, category: :application, severity: :error, source: nil, context: {})
48
50
  return nil unless ErrorRadar.config.enabled
49
51
 
50
- ErrorRadar::ErrorLog.record(category: category, severity: severity, message: message, source: source, context: context)
52
+ log = ErrorRadar::ErrorLog.record(category: category, severity: severity, message: message, source: source, context: context)
53
+ ErrorRadar::Notifier.dispatch(log) if log
54
+ log
51
55
  rescue StandardError => e
52
56
  warn_internal("notify failed: #{e.class}: #{e.message}")
53
57
  nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErrorRadar
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/error_radar.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'error_radar/version'
4
4
  require 'error_radar/configuration'
5
5
  require 'error_radar/tracking'
6
+ require 'error_radar/notifier'
6
7
  require 'error_radar/middleware'
7
8
  require 'error_radar/server_monitor'
8
9
  require 'error_radar/engine'
@@ -14,6 +14,33 @@ ErrorRadar.configure do |config|
14
14
  config.install_rake = true # capture Rake task failures (auto)
15
15
  config.install_rails_admin = true # register the ErrorLog board (auto, if RailsAdmin present)
16
16
 
17
+ # --- Notifications ---
18
+ # When to fire: :new_error (first occurrence of a fingerprint, default),
19
+ # :critical (any critical severity, throttled to 1/hour),
20
+ # :all (every occurrence, throttled to 1/hour per fingerprint)
21
+ config.notify_on = [:new_error]
22
+
23
+ # Slack incoming webhook (https://api.slack.com/messaging/webhooks)
24
+ # config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
25
+ # config.slack_channel = '#errors' # optional override
26
+
27
+ # Discord incoming webhook
28
+ # config.discord_webhook_url = ENV['DISCORD_WEBHOOK_URL']
29
+
30
+ # Email (requires ActionMailer to be configured in the host app)
31
+ # config.email_recipients = ['dev@myapp.com', 'oncall@myapp.com']
32
+ # config.email_from = 'errors@myapp.com'
33
+
34
+ # Generic webhook — POST JSON to any URL (PagerDuty, OpsGenie, custom scripts)
35
+ # config.webhook_urls = [ENV['PAGERDUTY_WEBHOOK_URL']]
36
+
37
+ # Base URL used to generate deep-links in notifications
38
+ # config.app_host = 'https://myapp.com'
39
+ # config.app_name = 'MyApp' # shown in notification title/subject
40
+
41
+ # Custom callback — runs after all built-in channels
42
+ # config.on_error { |error_log| MyPager.create_incident(error_log) }
43
+
17
44
  # --- Dashboard access control ---
18
45
  # Run as a before_action; raise/redirect inside to deny. Example with Devise:
19
46
  # config.authenticate = ->(controller) { controller.send(:authenticate_admin!) }
@@ -45,6 +72,17 @@ ErrorRadar.configure do |config|
45
72
  # config.expected_servers = [
46
73
  # { key: 'web', name: 'Web', tag: 'sidekiq_web', host: 'web', queue_hint: 'low' }
47
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
48
86
  end
49
87
 
50
88
  # ActiveJob is now captured automatically via install_active_job = true above.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddGithubIssueToErrorRadarErrorLogs < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ add_column :error_radar_error_logs, :github_issue_url, :string, limit: 500
6
+ end
7
+ end
@@ -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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chienbn9x
@@ -57,13 +57,19 @@ 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
66
+ - app/mailers/error_radar/error_mailer.rb
63
67
  - app/models/error_radar/application_record.rb
64
68
  - app/models/error_radar/error_log.rb
65
69
  - app/views/error_radar/dashboard/index.html.erb
66
70
  - app/views/error_radar/dashboard/show.html.erb
71
+ - app/views/error_radar/error_mailer/new_error.html.erb
72
+ - app/views/error_radar/error_mailer/new_error.text.erb
67
73
  - app/views/error_radar/errors/index.html.erb
68
74
  - app/views/error_radar/errors/show.html.erb
69
75
  - app/views/layouts/error_radar/application.html.erb
@@ -72,16 +78,24 @@ files:
72
78
  - lib/error_radar/configuration.rb
73
79
  - lib/error_radar/engine.rb
74
80
  - lib/error_radar/integrations/active_job.rb
81
+ - lib/error_radar/integrations/github.rb
75
82
  - lib/error_radar/integrations/rails_admin.rb
76
83
  - lib/error_radar/integrations/rake.rb
77
84
  - lib/error_radar/integrations/sidekiq.rb
78
85
  - lib/error_radar/middleware.rb
86
+ - lib/error_radar/notifications/discord.rb
87
+ - lib/error_radar/notifications/email.rb
88
+ - lib/error_radar/notifications/slack.rb
89
+ - lib/error_radar/notifications/webhook.rb
90
+ - lib/error_radar/notifier.rb
79
91
  - lib/error_radar/server_monitor.rb
80
92
  - lib/error_radar/tracking.rb
81
93
  - lib/error_radar/version.rb
82
94
  - lib/generators/error_radar/install/install_generator.rb
83
95
  - lib/generators/error_radar/install/templates/create_error_radar_error_logs.rb.tt
84
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
85
99
  homepage: https://github.com/chienbn9x/error_radar
86
100
  licenses:
87
101
  - MIT