error_radar 0.6.0 → 0.7.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: bdfb13eb6566d150855376a07e28a5a76a758b23526d7cf31de5a6adc17e5d27
4
- data.tar.gz: 713dcf3fa8c7867b971b6e7da6dd7dbf56a6e4057ec284dbadb6a8818b83f6d6
3
+ metadata.gz: 57b3fa6a7576ae08baeb699b180048363846c84ecbb489fc93e16ea537939898
4
+ data.tar.gz: 67008613bd52755dad89b7ed1abbc9a86eb92c138f95cae8005f5272e8336594
5
5
  SHA512:
6
- metadata.gz: b71979e7ef0ca2737d789f3f047a024c9933d8b2149e83e5d88fc734144213226dfb1a160b4080ee008104dd0c19dd35a1da06ce0fbeb2ae61a0a65455a65d0f
7
- data.tar.gz: 1838775fea5865d76bc746c49bc51efb0a557d6f6caf0924bdcd489b081570ff8667715690e3ef1b8a32ca7af219d7a26a1fe312e17304c27e4a9025d0632bf4
6
+ metadata.gz: 2882f7be2a07a38bdafd4b44c8251774a470f37d44fcd3dc4403046cd2998eb1077ac576c97cd8aed6a43ab4a86153440598bafa5a6a47576f06bbc137a2528d
7
+ data.tar.gz: 8717757580e628dc988422df68c3a806bd7d9727f297439b5e7303cc049a48371f37035442dbfd12f7789cb8b480221b093720dda965f6ebeffc2c75ef3bcb57
data/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.7.0] - 2026-07-03
6
+
7
+ ### Added
8
+ - **Occurrence history**: every individual error hit can now be stored in a
9
+ separate `error_radar_occurrences` table. Enable with
10
+ `config.track_occurrences = true` after running the upgrade migration:
11
+ `bin/rails generate error_radar:upgrade_v060 && bin/rails db:migrate`.
12
+ - **`ErrorRadar::ErrorOccurrence` model**: columns `occurred_at`, `context`,
13
+ `backtrace`, `http_status`, `request_url`. Belongs to `ErrorLog` via
14
+ `error_log_id`. Indexed on `(error_log_id, occurred_at)` for fast retrieval.
15
+ - **`config.max_occurrences_per_error`** (default: 200): automatically prunes
16
+ the oldest occurrences for a given error on each new hit so the table stays
17
+ bounded without manual cleanup.
18
+ - **Occurrences panel on error detail page**: shows the 20 most recent
19
+ occurrences with timestamp, HTTP status badge, request URL, and expandable
20
+ "Context" / "Stack" toggles. Paginated at 20 per page.
21
+ - **`GET /api/errors/:id`** now includes `recent_occurrences` (last 10 hits
22
+ with `occurred_at`, `http_status`, `request_url`, `context`) when
23
+ `track_occurrences` is enabled.
24
+ - **`error_radar:upgrade_v060` generator**: generates the migration that creates
25
+ `error_radar_occurrences`.
26
+ - **`ErrorLog#occurrences`** association: `has_many :occurrences, dependent: :delete_all`
27
+ so destroying an error log also removes its occurrence history.
28
+
29
+ ### Notes
30
+ - `track_occurrences` defaults to `false` to avoid unexpected writes before the
31
+ migration is run. Set it to `true` after the migration has been applied.
32
+ - Occurrences are recorded via `ErrorLog.record`, which is called by both the
33
+ synchronous capture path and `CaptureJob` (async path) — no extra
34
+ configuration needed once enabled.
35
+
5
36
  ## [0.6.0] - 2026-07-03
6
37
 
7
38
  ### Added
@@ -99,6 +99,22 @@ module ErrorRadar
99
99
  context: log.context,
100
100
  backtrace: log.backtrace
101
101
  )
102
+
103
+ if ErrorRadar.config.track_occurrences
104
+ begin
105
+ data[:recent_occurrences] = log.occurrences.recent.limit(10).map do |occ|
106
+ {
107
+ id: occ.id,
108
+ occurred_at: occ.occurred_at&.iso8601,
109
+ http_status: occ.http_status,
110
+ request_url: occ.request_url,
111
+ context: occ.context
112
+ }
113
+ end
114
+ rescue ActiveRecord::StatementInvalid
115
+ data[:recent_occurrences] = []
116
+ end
117
+ end
102
118
  end
103
119
 
104
120
  if log.class.column_names.include?('github_issue_url')
@@ -22,7 +22,23 @@ module ErrorRadar
22
22
  @pages = pagination_pages(@page, @total_pages)
23
23
  end
24
24
 
25
- def show; end
25
+ def show
26
+ @occurrences = []
27
+ return unless ErrorRadar.config.track_occurrences
28
+
29
+ begin
30
+ @occ_page = [params[:occ_page].to_i, 1].max
31
+ occ_per_page = 20
32
+ @occ_total = @error.occurrences.count
33
+ @occ_total_pages = [(@occ_total.to_f / occ_per_page).ceil, 1].max
34
+ @occ_page = [@occ_page, @occ_total_pages].min
35
+ @occurrences = @error.occurrences.recent
36
+ .limit(occ_per_page)
37
+ .offset((@occ_page - 1) * occ_per_page)
38
+ rescue ActiveRecord::StatementInvalid
39
+ @occurrences = []
40
+ end
41
+ end
26
42
 
27
43
  def update_status
28
44
  new_status = params[:status].to_s
@@ -16,6 +16,10 @@ module ErrorRadar
16
16
 
17
17
  enum status: { open: 0, in_progress: 1, resolved: 2, ignored: 3 }, _prefix: :status
18
18
 
19
+ has_many :occurrences, class_name: 'ErrorRadar::ErrorOccurrence',
20
+ foreign_key: :error_log_id,
21
+ dependent: :delete_all
22
+
19
23
  validates :fingerprint, presence: true, uniqueness: true
20
24
  validates :first_seen_at, :last_seen_at, presence: true
21
25
 
@@ -60,6 +64,17 @@ module ErrorRadar
60
64
  log.last_seen_at = now
61
65
  log.save!
62
66
  log.instance_variable_set(:@new_fingerprint, new_fingerprint)
67
+
68
+ if ErrorRadar.config.track_occurrences
69
+ ErrorRadar::ErrorOccurrence.record_for(
70
+ log,
71
+ context: context,
72
+ backtrace: backtrace,
73
+ http_status: http_status,
74
+ request_url: request_url
75
+ )
76
+ end
77
+
63
78
  log
64
79
  rescue StandardError => e
65
80
  ErrorRadar::Tracking.warn_internal("ErrorLog.record failed: #{e.class}: #{e.message}")
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ # One row per individual error hit. Linked to an ErrorLog (the deduplicated
5
+ # aggregate). Only recorded when config.track_occurrences is true and the
6
+ # upgrade_v060 migration has been run.
7
+ class ErrorOccurrence < ApplicationRecord
8
+ belongs_to :error_log
9
+
10
+ scope :recent, -> { order(occurred_at: :desc) }
11
+
12
+ def self.record_for(log, context: {}, backtrace: nil, http_status: nil, request_url: nil)
13
+ max = ErrorRadar.config.max_occurrences_per_error
14
+
15
+ create!(
16
+ error_log: log,
17
+ occurred_at: Time.current,
18
+ context: context.presence,
19
+ backtrace: Array(backtrace).join("\n").presence,
20
+ http_status: http_status,
21
+ request_url: request_url
22
+ )
23
+
24
+ if max&.positive?
25
+ keep_ids = where(error_log_id: log.id).order(occurred_at: :desc).limit(max).pluck(:id)
26
+ where(error_log_id: log.id).where.not(id: keep_ids).delete_all if keep_ids.any?
27
+ end
28
+ rescue ActiveRecord::StatementInvalid
29
+ # Table not yet created — run: bin/rails generate error_radar:upgrade_v060 && bin/rails db:migrate
30
+ rescue StandardError => e
31
+ ErrorRadar::Tracking.warn_internal("ErrorOccurrence.record_for failed: #{e.class}: #{e.message}")
32
+ end
33
+
34
+ def context_pretty
35
+ return '{}' if context.blank?
36
+ JSON.pretty_generate(context.is_a?(Hash) ? context : JSON.parse(context.to_s))
37
+ rescue JSON::ParserError
38
+ context.to_s
39
+ end
40
+ end
41
+ end
@@ -104,6 +104,97 @@
104
104
  </div>
105
105
  </div>
106
106
 
107
+ <%# ── Occurrences History ── %>
108
+ <% if ErrorRadar.config.track_occurrences %>
109
+ <div class="panel" style="margin-top:16px" id="occurrences-panel">
110
+ <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px">
111
+ <h2 style="margin:0">Occurrence History
112
+ <% if @occ_total.to_i > 0 %>
113
+ <span style="font-size:13px;font-weight:400;color:#6b7280">(<%= @occ_total %> total)</span>
114
+ <% end %>
115
+ </h2>
116
+ </div>
117
+
118
+ <% if @occurrences.empty? %>
119
+ <p style="font-size:13px;color:#9ca3af;margin:0">
120
+ <% if @occ_total.to_i == 0 %>
121
+ No occurrences recorded yet. New hits will appear here.
122
+ <% else %>
123
+ No occurrences on this page.
124
+ <% end %>
125
+ </p>
126
+ <% else %>
127
+ <table style="width:100%;border-collapse:collapse;font-size:13px">
128
+ <thead>
129
+ <tr style="border-bottom:2px solid #e5e7eb">
130
+ <th style="text-align:left;padding:6px 8px;color:#6b7280;width:170px">Time</th>
131
+ <th style="text-align:left;padding:6px 8px;color:#6b7280;width:70px">HTTP</th>
132
+ <th style="text-align:left;padding:6px 8px;color:#6b7280">Context / Request</th>
133
+ <th style="text-align:left;padding:6px 8px;color:#6b7280;width:80px">Backtrace</th>
134
+ </tr>
135
+ </thead>
136
+ <tbody>
137
+ <% @occurrences.each_with_index do |occ, i| %>
138
+ <tr style="border-bottom:1px solid #f3f4f6" class="occ-row">
139
+ <td style="padding:7px 8px;font-family:monospace;font-size:11px;white-space:nowrap">
140
+ <%= occ.occurred_at&.strftime('%Y-%m-%d %H:%M:%S') %>
141
+ </td>
142
+ <td style="padding:7px 8px">
143
+ <% if occ.http_status.present? %>
144
+ <span class="badge" style="background:<%= occ.http_status.to_i >= 500 ? '#dc3545' : '#fd7e14' %>">
145
+ <%= occ.http_status %>
146
+ </span>
147
+ <% end %>
148
+ </td>
149
+ <td style="padding:7px 8px;max-width:400px">
150
+ <% if occ.request_url.present? %>
151
+ <div style="font-size:11px;color:#6b7280;word-break:break-all;margin-bottom:4px"><%= occ.request_url.truncate(120) %></div>
152
+ <% end %>
153
+ <% unless (occ.context || {}).empty? %>
154
+ <button onclick="toggleOcc('ctx-<%= i %>')"
155
+ style="padding:2px 8px;font-size:11px;border:1px solid #d1d5db;border-radius:4px;background:#f9fafb;cursor:pointer">
156
+ Context ▾
157
+ </button>
158
+ <div id="ctx-<%= i %>" style="display:none;margin-top:6px">
159
+ <pre class="er-pre" style="max-height:200px;font-size:10px"><%= occ.context_pretty %></pre>
160
+ </div>
161
+ <% end %>
162
+ </td>
163
+ <td style="padding:7px 8px">
164
+ <% if occ.backtrace.present? %>
165
+ <button onclick="toggleOcc('bt-<%= i %>')"
166
+ style="padding:2px 8px;font-size:11px;border:1px solid #d1d5db;border-radius:4px;background:#f9fafb;cursor:pointer">
167
+ Stack ▾
168
+ </button>
169
+ <div id="bt-<%= i %>" style="display:none;position:absolute;z-index:10;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:12px;width:600px;max-width:90vw;box-shadow:0 4px 16px rgba(0,0,0,.12)">
170
+ <pre class="er-pre" style="max-height:300px"><%= occ.backtrace %></pre>
171
+ <button onclick="toggleOcc('bt-<%= i %>')" style="margin-top:6px;font-size:11px;border:0;background:none;cursor:pointer;color:#6b7280">Close</button>
172
+ </div>
173
+ <% end %>
174
+ </td>
175
+ </tr>
176
+ <% end %>
177
+ </tbody>
178
+ </table>
179
+
180
+ <%# Pagination %>
181
+ <% if @occ_total_pages.to_i > 1 %>
182
+ <div style="margin-top:12px;display:flex;gap:6px;font-size:12px;align-items:center">
183
+ <% (1..@occ_total_pages).each do |p| %>
184
+ <% if p == @occ_page %>
185
+ <span style="padding:4px 10px;background:#2563eb;color:#fff;border-radius:6px"><%= p %></span>
186
+ <% else %>
187
+ <%= link_to p, error_path(@error, occ_page: p, anchor: 'occurrences-panel'),
188
+ style: 'padding:4px 10px;border:1px solid #d1d5db;border-radius:6px;text-decoration:none;color:#374151' %>
189
+ <% end %>
190
+ <% end %>
191
+ <span style="color:#9ca3af">of <%= @occ_total_pages %> pages</span>
192
+ </div>
193
+ <% end %>
194
+ <% end %>
195
+ </div>
196
+ <% end %>
197
+
107
198
  <%# ── GitHub Issue ── %>
108
199
  <% if ErrorRadar.config.github_token.present? && ErrorRadar.config.github_repo.present? %>
109
200
  <div class="panel" style="margin-top:16px">
@@ -130,6 +221,11 @@
130
221
  <% end %>
131
222
 
132
223
  <script>
224
+ window.toggleOcc = function (id) {
225
+ var el = document.getElementById(id);
226
+ if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none';
227
+ };
228
+
133
229
  (function () {
134
230
  var CSRF = document.querySelector('meta[name="csrf-token"]').content;
135
231
  var STATUS_URL = '<%= error_status_path(@error) %>';
@@ -69,6 +69,14 @@ module ErrorRadar
69
69
  @error_callbacks << block
70
70
  end
71
71
 
72
+ # Occurrences ─────────────────────────────────────────────────────────────
73
+ # When true, each individual error hit is stored in error_radar_occurrences.
74
+ # Requires running: bin/rails generate error_radar:upgrade_v060 && bin/rails db:migrate
75
+ attr_accessor :track_occurrences
76
+ # Maximum occurrences to keep per ErrorLog. Oldest are pruned on each new hit.
77
+ # nil = keep all (not recommended for high-volume apps).
78
+ attr_accessor :max_occurrences_per_error
79
+
72
80
  # Performance ─────────────────────────────────────────────────────────────
73
81
  # When true, ErrorLog.record is enqueued via ActiveJob (non-blocking).
74
82
  # Requires ActiveJob to be configured. Falls back to sync if ActiveJob is absent.
@@ -148,6 +156,9 @@ module ErrorRadar
148
156
  @app_host = nil
149
157
  @error_callbacks = []
150
158
 
159
+ @track_occurrences = false
160
+ @max_occurrences_per_error = 200
161
+
151
162
  @async_capture = false
152
163
  @capture_job_queue = :default
153
164
  @retention_days = nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErrorRadar
4
- VERSION = '0.6.0'
4
+ VERSION = '0.7.0'
5
5
  end
@@ -84,6 +84,12 @@ ErrorRadar.configure do |config|
84
84
  # config.github_token = ENV['GITHUB_TOKEN'] # PAT with repo scope
85
85
  # config.github_repo = 'myorg/myapp' # "owner/repo" format
86
86
 
87
+ # --- Occurrence History ---
88
+ # Store each individual error hit so you can see context/backtrace per occurrence.
89
+ # Requires running: bin/rails generate error_radar:upgrade_v060 && bin/rails db:migrate
90
+ # config.track_occurrences = true
91
+ # config.max_occurrences_per_error = 200 # keep last N per error (oldest pruned on each new hit)
92
+
87
93
  # --- Performance & Async Capture ---
88
94
  # Write ErrorLog records via ActiveJob so exceptions don't block the request.
89
95
  # Requires ActiveJob + a queue adapter (Sidekiq, Solid Queue, etc.).
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateErrorRadarOccurrences < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ create_table :error_radar_occurrences do |t|
6
+ t.references :error_log, null: false,
7
+ foreign_key: { to_table: :error_radar_error_logs },
8
+ index: false # covered by composite index below
9
+
10
+ t.datetime :occurred_at, null: false
11
+
12
+ t.json :context # structured payload snapshot for this hit
13
+ t.text :backtrace # backtrace at the moment of this occurrence
14
+ t.integer :http_status
15
+ t.string :request_url, limit: 2000
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ # Fast per-error chronological listing
21
+ add_index :error_radar_occurrences, %i[error_log_id occurred_at],
22
+ name: 'idx_er_occurrences_log_time'
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record'
4
+
5
+ module ErrorRadar
6
+ module Generators
7
+ class UpgradeV060Generator < ActiveRecord::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ argument :name, type: :string, default: 'upgrade_v060'
11
+
12
+ desc 'Creates the error_radar_occurrences table for per-hit occurrence history (v0.7.0).'
13
+
14
+ def create_migration
15
+ migration_template 'create_error_radar_occurrences.rb.tt',
16
+ 'db/migrate/create_error_radar_occurrences.rb'
17
+ end
18
+ end
19
+ end
20
+ 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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - chienbn9x
@@ -66,6 +66,7 @@ files:
66
66
  - app/mailers/error_radar/error_mailer.rb
67
67
  - app/models/error_radar/application_record.rb
68
68
  - app/models/error_radar/error_log.rb
69
+ - app/models/error_radar/error_occurrence.rb
69
70
  - app/views/error_radar/dashboard/index.html.erb
70
71
  - app/views/error_radar/dashboard/show.html.erb
71
72
  - app/views/error_radar/error_mailer/new_error.html.erb
@@ -98,6 +99,8 @@ files:
98
99
  - lib/generators/error_radar/install/templates/initializer.rb
99
100
  - lib/generators/error_radar/upgrade_v050/templates/add_github_issue_to_error_radar_error_logs.rb.tt
100
101
  - lib/generators/error_radar/upgrade_v050/upgrade_v050_generator.rb
102
+ - lib/generators/error_radar/upgrade_v060/templates/create_error_radar_occurrences.rb.tt
103
+ - lib/generators/error_radar/upgrade_v060/upgrade_v060_generator.rb
101
104
  - lib/tasks/error_radar.rake
102
105
  homepage: https://github.com/chienbn9x/error_radar
103
106
  licenses: