rails_error_dashboard 0.5.9 → 0.5.10

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: 18faae83cf7bba23409732000c1cb230ba7ef067f23a454084445f754df4be0d
4
- data.tar.gz: ad82e112e173f8247cf31b214b040ba82e4035040bbdf14d8c53a5782db36bb1
3
+ metadata.gz: f3004ee5b49b622c064ceb8103c6abb2212f1a0cc269471a7281b49338213240
4
+ data.tar.gz: a8e27db79b78052909acc3ce04ab4875b31148b6e50b631b46bfe67eb0562fd3
5
5
  SHA512:
6
- metadata.gz: 0d0032aeb56ccc44a8c8966da338f75c37e71002534fd0bbf3e019cfe0582df37638e50863e8d4a77212731c1190de214d521521df2e986a23d36a575748d5d9
7
- data.tar.gz: 4862e358faa4a87f1091e33cf08c0e654a7e959b8af551a1223e3c9d098d383d503342aa8c2fb4d66f20c95fc4864276df631be6b93bcdd22ef50269516ef997
6
+ metadata.gz: c2dfe54a84c8a7049e6b51956d3512c6c18f8d37cabdd5f655bbdb046b6ed7ce2b287886f80a36a7bba85839090309eb81e823e29b5b766036f9b7ba726f7fef
7
+ data.tar.gz: fdcf6aea6b7f568da4a525330e63de6121aa087926ffd8982c631ec90efa5282081d428c7fd9069b4ed923f31a1c1445483c443f4b845d4677648b10c4d56fc4
data/README.md CHANGED
@@ -150,23 +150,43 @@ config.enable_activestorage_tracking = true # requires enable_breadcrumbs = tru
150
150
  <details>
151
151
  <summary><strong>Issue Tracking — GitHub, GitLab, Codeberg</strong></summary>
152
152
 
153
- Create, link, and manage issues directly from error detail pages. Supports GitHub, GitLab, and Codeberg/Gitea/Forgejo with provider auto-detection.
153
+ One switch connects errors to your issue tracker. Platform becomes the source of truth — status, assignees, labels, and comments are mirrored live in the dashboard.
154
154
 
155
- - **Manual:** "Create Issue" button + "Link Existing Issue" URL input
156
- - **Auto-create:** On first occurrence and/or severity threshold — configurable
157
- - **Lifecycle sync:** Resolve → close issue, recur → reopen + comment (throttled)
158
- - **Two-way webhooks:** Issue closed/reopened on platform syncs to dashboard
155
+ - **Create & link:** "Create Issue" button or paste an existing URL
156
+ - **Auto-create:** New errors auto-create issues. Critical/high severity always creates
157
+ - **Lifecycle sync:** Resolve → close, recur → reopen + comment, all via background jobs
158
+ - **Platform mirror:** Issue state, assignees (with avatars), labels (with colors), and comments displayed in the dashboard. Workflow controls (Resolve, Assign, Priority) replaced by platform state
159
+ - **Two-way webhooks:** Issue closed/reopened on platform syncs back to dashboard
159
160
  - **RED branding:** Issues show "Created by RED (Rails Error Dashboard)"
160
161
 
161
162
  ```ruby
162
163
  config.enable_issue_tracking = true
163
164
  config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
164
- # Provider and repo auto-detected from git_repository_url
165
+ # That's it — provider and repo auto-detected from git_repository_url
165
166
  ```
166
167
 
167
168
  [Complete documentation →](docs/guides/CONFIGURATION.md)
168
169
  </details>
169
170
 
171
+ <details>
172
+ <summary><strong>Release Tracking</strong></summary>
173
+
174
+ Dedicated Releases page at `/errors/releases` shows a timeline of all deploys/versions with health stats. Answers: "Did this deploy introduce new errors?" and "Is this release stable?"
175
+
176
+ - **Release timeline:** Every version seen, sorted newest-first, with error counts, unique types, and time range
177
+ - **"New in this release":** Errors whose fingerprint first appeared in each version — flagged with a red badge
178
+ - **Stability indicators:** Green (at or below average), yellow (1-2x), red (>2x average error rate)
179
+ - **Release comparison:** Delta and percentage change vs the previous release
180
+ - **Current release:** Highlighted card with live health stats
181
+ - **Zero config:** Works automatically when `app_version` or `git_sha` is set (via config, `APP_VERSION`, `GIT_SHA`, `HEROKU_SLUG_COMMIT`, or `RENDER_GIT_COMMIT` env vars)
182
+
183
+ ```ruby
184
+ config.app_version = "1.2.0" # or set APP_VERSION env var
185
+ config.git_sha = ENV["GIT_SHA"] # auto-detected on Heroku/Render
186
+ config.git_repository_url = "https://github.com/user/repo" # enables SHA links
187
+ ```
188
+ </details>
189
+
170
190
  <details>
171
191
  <summary><strong>Source Code Integration + Git Blame</strong></summary>
172
192
 
@@ -276,6 +276,16 @@ module RailsErrorDashboard
276
276
  @platform_specific_errors = correlation.platform_specific_errors
277
277
  end
278
278
 
279
+ def releases
280
+ days = (params[:days] || 30).to_i
281
+ @days = days
282
+ result = Queries::ReleaseTimeline.call(days, application_id: @current_application_id)
283
+ all_releases = result[:releases]
284
+ @summary = result[:summary]
285
+
286
+ @pagy, @releases = pagy(:offset, all_releases, limit: params[:per_page] || 25)
287
+ end
288
+
279
289
  def deprecations
280
290
  unless RailsErrorDashboard.configuration.enable_breadcrumbs
281
291
  flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
@@ -43,7 +43,8 @@ module RailsErrorDashboard
43
43
  private
44
44
 
45
45
  def verify_webhook_enabled
46
- unless RailsErrorDashboard.configuration.enable_issue_webhooks
46
+ config = RailsErrorDashboard.configuration
47
+ unless config.enable_issue_tracking && config.issue_webhook_secret.present?
47
48
  head :not_found
48
49
  end
49
50
  end
@@ -1651,6 +1651,11 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1651
1651
  <i class="bi bi-diagram-3"></i> Correlation
1652
1652
  <% end %>
1653
1653
  </li>
1654
+ <li class="nav-item">
1655
+ <%= link_to releases_errors_path(nav_params), class: "nav-link #{request.path == releases_errors_path ? 'active' : ''}" do %>
1656
+ <i class="bi bi-rocket-takeoff"></i> Releases
1657
+ <% end %>
1658
+ </li>
1654
1659
  <li class="nav-item">
1655
1660
  <%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
1656
1661
  <i class="bi bi-gear"></i> Settings
@@ -71,8 +71,21 @@
71
71
  <% end %>
72
72
  </div>
73
73
 
74
+ <% if RailsErrorDashboard.configuration.enable_issue_tracking %>
75
+ <div class="mb-3">
76
+ <small class="text-muted" style="font-size: 0.78em;">
77
+ <i class="bi bi-info-circle me-1"></i>
78
+ Status, assignment, and priority are managed via your
79
+ <% if error.respond_to?(:external_issue_url) && error.external_issue_url.present? %>
80
+ <a href="<%= error.external_issue_url %>" target="_blank" rel="noopener">linked issue</a>.
81
+ <% else %>
82
+ issue tracker. Link an issue to see details.
83
+ <% end %>
84
+ </small>
85
+ </div>
86
+ <% end %>
74
87
  <% unless RailsErrorDashboard.configuration.enable_issue_tracking %>
75
- <!-- Workflow Status (hidden when issue tracking enabled — platform is source of truth) -->
88
+ <!-- Workflow Status -->
76
89
  <div class="mb-3">
77
90
  <small class="metadata-label d-block mb-1">Workflow Status</small>
78
91
  <% if error.respond_to?(:status) %>
@@ -0,0 +1,284 @@
1
+ <% content_for :page_title, "Releases" %>
2
+
3
+ <div class="container-fluid py-4">
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1 class="h3 mb-0">
6
+ <i class="bi bi-rocket-takeoff me-2"></i>
7
+ Releases
8
+ </h1>
9
+
10
+ <div class="btn-group" role="group">
11
+ <%= link_to releases_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
+ 7 Days
13
+ <% end %>
14
+ <%= link_to releases_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
+ 30 Days
16
+ <% end %>
17
+ <%= link_to releases_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
+ 90 Days
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @releases.empty? %>
24
+ <div class="text-center py-5">
25
+ <i class="bi bi-rocket-takeoff display-1 text-muted mb-3"></i>
26
+ <h4 class="text-muted">No Release Data Found</h4>
27
+ <p class="text-muted">
28
+ No version data was detected in errors over the last <%= @days %> days.
29
+ </p>
30
+ <div class="card mx-auto" style="max-width: 500px;">
31
+ <div class="card-body text-start">
32
+ <h6>How to enable release tracking:</h6>
33
+ <ul class="mb-0">
34
+ <li>Set <code>config.app_version</code> in your initializer</li>
35
+ <li>Or set the <code>APP_VERSION</code> environment variable</li>
36
+ <li>Git SHA is auto-detected from <code>GIT_SHA</code>, <code>HEROKU_SLUG_COMMIT</code>, or <code>RENDER_GIT_COMMIT</code></li>
37
+ <li>Version and SHA are captured automatically when errors occur</li>
38
+ </ul>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <% else %>
43
+ <%
44
+ current_release = @releases.find { |r| r[:current] }
45
+ %>
46
+
47
+ <!-- Summary Cards -->
48
+ <div class="row mb-4">
49
+ <div class="col-md-3">
50
+ <div class="card text-center">
51
+ <div class="card-body">
52
+ <div class="display-6 text-primary"><%= @summary[:total_releases] %></div>
53
+ <small class="text-muted">Releases</small>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <div class="col-md-3">
58
+ <div class="card text-center">
59
+ <div class="card-body">
60
+ <div class="display-6"><code><%= @summary[:current_version] || "N/A" %></code></div>
61
+ <small class="text-muted">Current Release</small>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <div class="col-md-3">
66
+ <div class="card text-center">
67
+ <div class="card-body">
68
+ <div class="display-6 text-warning"><%= @summary[:avg_errors_per_release] %></div>
69
+ <small class="text-muted">Avg Errors / Release</small>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ <div class="col-md-3">
74
+ <div class="card text-center">
75
+ <div class="card-body">
76
+ <%
77
+ problematic_count = @releases.count { |r| r[:problematic] }
78
+ %>
79
+ <div class="display-6 <%= problematic_count > 0 ? 'text-danger' : 'text-success' %>"><%= problematic_count %></div>
80
+ <small class="text-muted">Problematic Releases</small>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <% if current_release %>
87
+ <!-- Current Release Card -->
88
+ <div class="card mb-4 border-primary">
89
+ <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
90
+ <h5 class="mb-0">
91
+ <i class="bi bi-broadcast"></i> Current Release: <strong><%= current_release[:version] %></strong>
92
+ </h5>
93
+ <span class="badge bg-light text-primary">Live</span>
94
+ </div>
95
+ <div class="card-body">
96
+ <div class="row text-center">
97
+ <div class="col-md-2">
98
+ <div class="h4 mb-0"><%= current_release[:total_errors] %></div>
99
+ <small class="text-muted">Errors</small>
100
+ </div>
101
+ <div class="col-md-2">
102
+ <div class="h4 mb-0"><%= current_release[:unique_error_types] %></div>
103
+ <small class="text-muted">Error Types</small>
104
+ </div>
105
+ <div class="col-md-2">
106
+ <div class="h4 mb-0">
107
+ <% if current_release[:new_error_count] > 0 %>
108
+ <span class="text-danger"><%= current_release[:new_error_count] %></span>
109
+ <% else %>
110
+ <span class="text-success">0</span>
111
+ <% end %>
112
+ </div>
113
+ <small class="text-muted">New Errors</small>
114
+ </div>
115
+ <div class="col-md-2">
116
+ <%
117
+ stability_color = case current_release[:stability]
118
+ when :green then "success"
119
+ when :yellow then "warning"
120
+ when :red then "danger"
121
+ else "secondary"
122
+ end
123
+ %>
124
+ <div class="h4 mb-0">
125
+ <span class="badge bg-<%= stability_color %>">
126
+ <%= current_release[:stability].to_s.capitalize %>
127
+ </span>
128
+ </div>
129
+ <small class="text-muted">Stability</small>
130
+ </div>
131
+ <div class="col-md-2">
132
+ <% if current_release[:git_shas].any? %>
133
+ <div class="h4 mb-0"><%= git_commit_link(current_release[:git_shas].first) %></div>
134
+ <% else %>
135
+ <div class="h4 mb-0 text-muted">N/A</div>
136
+ <% end %>
137
+ <small class="text-muted">Git SHA</small>
138
+ </div>
139
+ <div class="col-md-2">
140
+ <% if current_release[:delta_from_previous] %>
141
+ <%
142
+ delta = current_release[:delta_from_previous]
143
+ delta_class = delta > 0 ? "text-danger" : (delta < 0 ? "text-success" : "text-muted")
144
+ delta_icon = delta > 0 ? "bi-arrow-up" : (delta < 0 ? "bi-arrow-down" : "bi-dash")
145
+ %>
146
+ <div class="h4 mb-0 <%= delta_class %>">
147
+ <i class="bi <%= delta_icon %>"></i> <%= delta.abs %>
148
+ </div>
149
+ <% else %>
150
+ <div class="h4 mb-0 text-muted">&mdash;</div>
151
+ <% end %>
152
+ <small class="text-muted">vs Previous</small>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ <% end %>
158
+
159
+ <!-- Release Timeline Table -->
160
+ <div class="card mb-4">
161
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
162
+ <h5 class="mb-0">
163
+ <i class="bi bi-clock-history text-primary me-2"></i>
164
+ Release Timeline
165
+ <span class="badge bg-primary"><%= @summary[:total_releases] %></span>
166
+ </h5>
167
+ <small class="text-muted"><%== @pagy.info_tag %></small>
168
+ </div>
169
+ <div class="card-body p-0">
170
+ <div class="table-responsive">
171
+ <table class="table table-hover mb-0">
172
+ <thead class="table-light">
173
+ <tr>
174
+ <th>Version</th>
175
+ <th>Git SHA</th>
176
+ <th>Time Range</th>
177
+ <th width="100">Errors</th>
178
+ <th width="100">Types</th>
179
+ <th width="120">New Errors</th>
180
+ <th width="100">Stability</th>
181
+ <th width="120">vs Previous</th>
182
+ </tr>
183
+ </thead>
184
+ <tbody>
185
+ <% @releases.each do |release| %>
186
+ <tr class="<%= 'table-active' if release[:current] %>">
187
+ <td>
188
+ <strong><%= release[:version] %></strong>
189
+ <% if release[:current] %>
190
+ <span class="badge bg-primary ms-1">Current</span>
191
+ <% end %>
192
+ <% if release[:problematic] %>
193
+ <span class="badge bg-danger ms-1">Problematic</span>
194
+ <% end %>
195
+ </td>
196
+ <td>
197
+ <% if release[:git_shas].any? %>
198
+ <%= git_commit_link(release[:git_shas].first) %>
199
+ <% if release[:git_shas].size > 1 %>
200
+ <small class="text-muted">+<%= release[:git_shas].size - 1 %></small>
201
+ <% end %>
202
+ <% else %>
203
+ <span class="text-muted">N/A</span>
204
+ <% end %>
205
+ </td>
206
+ <td>
207
+ <small>
208
+ <%= local_time_ago(release[:first_seen]) %>
209
+ <% unless release[:first_seen] == release[:last_seen] %>
210
+ &rarr; <%= local_time_ago(release[:last_seen]) %>
211
+ <% end %>
212
+ </small>
213
+ </td>
214
+ <td>
215
+ <span class="badge bg-secondary"><%= release[:total_errors] %></span>
216
+ </td>
217
+ <td>
218
+ <span class="badge bg-info"><%= release[:unique_error_types] %></span>
219
+ </td>
220
+ <td>
221
+ <% if release[:new_error_count] > 0 %>
222
+ <span class="badge bg-danger">
223
+ <i class="bi bi-exclamation-circle"></i> <%= release[:new_error_count] %> new
224
+ </span>
225
+ <% else %>
226
+ <span class="badge bg-success">
227
+ <i class="bi bi-check-circle"></i> 0 new
228
+ </span>
229
+ <% end %>
230
+ </td>
231
+ <td>
232
+ <%
233
+ stability_color = case release[:stability]
234
+ when :green then "success"
235
+ when :yellow then "warning"
236
+ when :red then "danger"
237
+ else "secondary"
238
+ end
239
+ %>
240
+ <span class="badge bg-<%= stability_color %>">
241
+ <i class="bi bi-circle-fill" style="font-size: 0.5em; vertical-align: middle;"></i>
242
+ <%= release[:stability].to_s.capitalize %>
243
+ </span>
244
+ </td>
245
+ <td>
246
+ <% if release[:delta_from_previous] %>
247
+ <%
248
+ delta = release[:delta_from_previous]
249
+ pct = release[:delta_percentage]
250
+ delta_class = delta > 0 ? "text-danger" : (delta < 0 ? "text-success" : "text-muted")
251
+ delta_icon = delta > 0 ? "bi-arrow-up" : (delta < 0 ? "bi-arrow-down" : "bi-dash")
252
+ %>
253
+ <span class="<%= delta_class %>">
254
+ <i class="bi <%= delta_icon %>"></i>
255
+ <%= delta > 0 ? "+#{delta}" : delta %>
256
+ <% if pct %>
257
+ <small>(<%= pct > 0 ? "+#{pct}" : pct %>%)</small>
258
+ <% end %>
259
+ </span>
260
+ <% else %>
261
+ <span class="text-muted">&mdash;</span>
262
+ <% end %>
263
+ </td>
264
+ </tr>
265
+ <% end %>
266
+ </tbody>
267
+ </table>
268
+ </div>
269
+ </div>
270
+ <div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
271
+ <small class="text-muted">
272
+ <i class="bi bi-info-circle"></i>
273
+ Stability: <span class="badge bg-success">Green</span> &le; avg errors,
274
+ <span class="badge bg-warning">Yellow</span> 1-2x avg,
275
+ <span class="badge bg-danger">Red</span> &gt; 2x avg.
276
+ "New" errors are error types first seen in that release.
277
+ </small>
278
+ <div>
279
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ <% end %>
284
+ </div>
@@ -245,6 +245,21 @@
245
245
  <%
246
246
  end
247
247
 
248
+ when :secret
249
+ if value.present?
250
+ %>
251
+ <span class="badge bg-success fs-6">
252
+ <i class="bi bi-check-circle"></i> Set
253
+ </span>
254
+ <%
255
+ else
256
+ %>
257
+ <span class="badge bg-warning text-dark fs-6">
258
+ <i class="bi bi-exclamation-triangle"></i> Not set
259
+ </span>
260
+ <%
261
+ end
262
+
248
263
  when :retention_days
249
264
  # Show retention configuration with manual cleanup instructions
250
265
  if value.present?
@@ -113,6 +113,38 @@
113
113
  dashboard_base_url: { label: "Dashboard Base URL", description: "Base URL for dashboard links", type: :url }
114
114
  }
115
115
  },
116
+ "Issue Tracking" => {
117
+ icon: "bi-link-45deg",
118
+ color: "primary",
119
+ settings: {
120
+ enable_issue_tracking: { label: "Issue Tracking", description: "GitHub/GitLab/Codeberg integration — one switch enables all: issue creation, auto-create, lifecycle sync, platform state mirroring, comment display. Replaces workflow controls with platform state.", type: :boolean },
121
+ issue_tracker_token: { label: "API Token", description: "RED bot token (set RED_BOT_TOKEN env var)", type: :secret, show_if: :enable_issue_tracking },
122
+ issue_tracker_provider: { label: "Provider", description: "Auto-detected from git_repository_url", type: :auto_detect, show_if: :enable_issue_tracking, method: :effective_issue_tracker_provider },
123
+ issue_tracker_repo: { label: "Repository", description: "Auto-detected from git_repository_url", type: :auto_detect, show_if: :enable_issue_tracking, method: :effective_issue_tracker_repo },
124
+ issue_tracker_labels: { label: "Labels", description: "Labels added to new issues", type: :array, show_if: :enable_issue_tracking },
125
+ issue_webhook_secret: { label: "Webhook Secret", description: "Set to enable two-way sync (HMAC verification)", type: :secret, show_if: :enable_issue_tracking }
126
+ }
127
+ },
128
+ "Deep Debugging" => {
129
+ icon: "bi-search",
130
+ color: "danger",
131
+ settings: {
132
+ enable_local_variables: { label: "Local Variable Capture", description: "TracePoint(:raise) — capture locals at exception time", type: :boolean },
133
+ enable_instance_variables: { label: "Instance Variable Capture", description: "Capture self's ivars at exception time", type: :boolean },
134
+ detect_swallowed_exceptions: { label: "Swallowed Exception Detection", description: "TracePoint(:raise) + (:rescue) — detect silently rescued exceptions (Ruby 3.3+)", type: :boolean },
135
+ enable_diagnostic_dump: { label: "Diagnostic Dump", description: "On-demand system state snapshots", type: :boolean },
136
+ enable_crash_capture: { label: "Process Crash Capture", description: "at_exit hook for unhandled crashes", type: :boolean }
137
+ }
138
+ },
139
+ "Event Tracking" => {
140
+ icon: "bi-broadcast",
141
+ color: "info",
142
+ settings: {
143
+ enable_rack_attack_tracking: { label: "Rack Attack Tracking", description: "Track throttle/blocklist events as breadcrumbs", type: :boolean },
144
+ enable_actioncable_tracking: { label: "ActionCable Tracking", description: "Track WebSocket channel events as breadcrumbs", type: :boolean },
145
+ enable_activestorage_tracking: { label: "ActiveStorage Tracking", description: "Track uploads/downloads/deletes as breadcrumbs", type: :boolean }
146
+ }
147
+ },
116
148
  "Advanced Configuration" => {
117
149
  icon: "bi-sliders",
118
150
  color: "secondary",
data/config/routes.rb CHANGED
@@ -37,6 +37,7 @@ RailsErrorDashboard::Engine.routes.draw do
37
37
  get :rack_attack_summary
38
38
  get :actioncable_health_summary
39
39
  get :activestorage_health_summary
40
+ get :releases
40
41
  get :diagnostic_dumps
41
42
  post :create_diagnostic_dump
42
43
  post :batch_action
@@ -444,10 +444,40 @@ RailsErrorDashboard.configure do |config|
444
444
  config.git_sha = ENV["GIT_SHA"]
445
445
  # config.total_users_for_impact = 10000 # For user impact % calculation
446
446
 
447
- # Git repository URL for clickable commit links
447
+ # Git repository URL for clickable commit links and issue tracking
448
448
  # Examples:
449
449
  # GitHub: "https://github.com/username/repo"
450
450
  # GitLab: "https://gitlab.com/username/repo"
451
- # Bitbucket: "https://bitbucket.org/username/repo"
451
+ # Codeberg: "https://codeberg.org/username/repo"
452
452
  # config.git_repository_url = ENV["GIT_REPOSITORY_URL"]
453
+
454
+ # ============================================================================
455
+ # ISSUE TRACKING (GitHub / GitLab / Codeberg)
456
+ # ============================================================================
457
+ #
458
+ # One switch enables everything: issue creation, auto-create on first
459
+ # occurrence, lifecycle sync (resolve → close, reopen → reopen), platform
460
+ # state mirroring (status, assignees, labels), and comment display.
461
+ #
462
+ # IMPORTANT: When enabled, the dashboard shows platform state instead of
463
+ # internal workflow controls:
464
+ # - "Mark as Resolved" → replaced by issue open/closed from platform
465
+ # - Workflow Status → issue state from platform
466
+ # - Assigned To → assignees from platform (with avatars)
467
+ # - Priority → labels from platform (with colors)
468
+ # - Snooze and Mute remain (no platform equivalent)
469
+ #
470
+ # Setup:
471
+ # 1. Create a RED bot account on GitHub/GitLab/Codeberg
472
+ # 2. Generate a token and set RED_BOT_TOKEN env var
473
+ # 3. Set git_repository_url above (already used for source code linking)
474
+ # 4. Enable:
475
+ #
476
+ # config.enable_issue_tracking = true
477
+ # config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
478
+ #
479
+ # Optional overrides:
480
+ # config.issue_tracker_labels = ["bug"] # Labels added to new issues
481
+ # config.issue_tracker_auto_create_severities = [:critical, :high] # Auto-create threshold
482
+ # config.issue_webhook_secret = ENV["ISSUE_WEBHOOK_SECRET"] # Enables two-way webhook sync
453
483
  end
@@ -82,17 +82,17 @@ module RailsErrorDashboard
82
82
  attr_accessor :git_repository_url
83
83
 
84
84
  # Issue tracker integration (GitHub, GitLab, Codeberg/Gitea/Forgejo)
85
- attr_accessor :enable_issue_tracking # Master switch (default: false)
86
- attr_accessor :issue_tracker_provider # :github, :gitlab, :codeberg (auto-detected from git_repository_url)
85
+ # One switch enables everything: issue creation, auto-create, lifecycle sync,
86
+ # platform state mirroring, and comment display. Webhooks activate when
87
+ # issue_webhook_secret is set.
88
+ attr_accessor :enable_issue_tracking # Master switch (default: false) — enables all platform integration
87
89
  attr_accessor :issue_tracker_token # String or lambda/proc for Rails credentials
90
+ attr_accessor :issue_tracker_provider # :github, :gitlab, :codeberg (auto-detected from git_repository_url)
88
91
  attr_accessor :issue_tracker_repo # "owner/repo" (auto-extracted from git_repository_url)
89
92
  attr_accessor :issue_tracker_labels # Array of label strings (default: ["bug"])
90
93
  attr_accessor :issue_tracker_api_url # Custom API base URL for self-hosted instances
91
- attr_accessor :auto_create_issues # Boolean (default: false) — auto-create issues for new errors
92
- attr_accessor :auto_create_issues_on_first_occurrence # Boolean (default: true) create on first occurrence
93
- attr_accessor :auto_create_issues_for_severities # Array of symbols (default: [:critical, :high])
94
- attr_accessor :enable_issue_webhooks # Boolean (default: false) — receive webhooks for two-way sync
95
- attr_accessor :issue_webhook_secret # String — HMAC secret for webhook signature verification
94
+ attr_accessor :issue_tracker_auto_create_severities # Auto-create for these severities (default: [:critical, :high])
95
+ attr_accessor :issue_webhook_secret # HMAC secretwebhooks activate when this is set
96
96
 
97
97
  # Advanced error analysis features
98
98
  attr_accessor :enable_similar_errors # Fuzzy error matching
@@ -244,17 +244,14 @@ module RailsErrorDashboard
244
244
  @total_users_for_impact = nil # Auto-detect if not set
245
245
  @git_repository_url = ENV["GIT_REPOSITORY_URL"]
246
246
 
247
- # Issue tracker integration defaults — OFF by default
247
+ # Issue tracker integration defaults — OFF by default, one switch enables all
248
248
  @enable_issue_tracking = false
249
+ @issue_tracker_token = ENV["RED_BOT_TOKEN"] || ENV["ISSUE_TRACKER_TOKEN"]
249
250
  @issue_tracker_provider = nil # Auto-detect from git_repository_url
250
- @issue_tracker_token = ENV["ISSUE_TRACKER_TOKEN"]
251
251
  @issue_tracker_repo = nil # Auto-extract from git_repository_url
252
252
  @issue_tracker_labels = [ "bug" ]
253
253
  @issue_tracker_api_url = nil # For self-hosted instances
254
- @auto_create_issues = false
255
- @auto_create_issues_on_first_occurrence = true
256
- @auto_create_issues_for_severities = [ :critical, :high ]
257
- @enable_issue_webhooks = false
254
+ @issue_tracker_auto_create_severities = [ :critical, :high ]
258
255
  @issue_webhook_secret = ENV["ISSUE_WEBHOOK_SECRET"]
259
256
 
260
257
  # Advanced error analysis features (all OFF by default - opt-in)
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Queries
5
+ # Query: Build a release timeline from error data, showing per-version health stats,
6
+ # "new in this release" error detection, stability indicators, and release-over-release deltas.
7
+ # Uses existing app_version and git_sha columns on error_logs — no new migration needed.
8
+ class ReleaseTimeline
9
+ def self.call(days = 30, application_id: nil)
10
+ new(days, application_id: application_id).call
11
+ end
12
+
13
+ def initialize(days = 30, application_id: nil)
14
+ @days = days
15
+ @application_id = application_id
16
+ @start_date = days.days.ago
17
+ end
18
+
19
+ def call
20
+ return empty_result unless has_version_column?
21
+
22
+ releases = build_releases
23
+ {
24
+ releases: releases,
25
+ summary: build_summary(releases)
26
+ }
27
+ rescue => e
28
+ Rails.logger.error("[RailsErrorDashboard] ReleaseTimeline failed: #{e.class}: #{e.message}")
29
+ empty_result
30
+ end
31
+
32
+ private
33
+
34
+ def base_scope
35
+ scope = ErrorLog.where("occurred_at >= ?", @start_date)
36
+ .where.not(app_version: [ nil, "" ])
37
+ scope = scope.where(application_id: @application_id) if @application_id.present?
38
+ scope
39
+ end
40
+
41
+ def build_releases
42
+ version_stats = aggregate_version_stats
43
+ return [] if version_stats.empty?
44
+
45
+ new_errors = new_errors_per_version
46
+ avg_errors = version_stats.sum { |_v, s| s[:total_errors] }.to_f / version_stats.size
47
+
48
+ # Sort chronologically by first_seen (oldest first) for delta calculation
49
+ sorted_chrono = version_stats.sort_by { |_v, s| s[:first_seen] }
50
+
51
+ releases = []
52
+ sorted_chrono.each_with_index do |(version, stats), idx|
53
+ previous_count = idx > 0 ? sorted_chrono[idx - 1][1][:total_errors] : nil
54
+
55
+ releases << {
56
+ version: version,
57
+ git_shas: stats[:git_shas],
58
+ first_seen: stats[:first_seen],
59
+ last_seen: stats[:last_seen],
60
+ current: false, # set below
61
+ total_errors: stats[:total_errors],
62
+ unique_error_types: stats[:unique_error_types],
63
+ new_error_count: new_errors[version] || 0,
64
+ stability: stability_indicator(stats[:total_errors], avg_errors),
65
+ problematic: stats[:total_errors] > (avg_errors * 2),
66
+ delta_from_previous: previous_count ? (stats[:total_errors] - previous_count) : nil,
67
+ delta_percentage: previous_count && previous_count > 0 ? ((stats[:total_errors] - previous_count).to_f / previous_count * 100).round(1) : nil
68
+ }
69
+ end
70
+
71
+ # Mark the most recent release as current
72
+ releases.last[:current] = true if releases.any?
73
+
74
+ # Return in reverse chronological order (newest first)
75
+ releases.reverse
76
+ end
77
+
78
+ # Single GROUP BY query for per-version aggregates
79
+ def aggregate_version_stats
80
+ rows = base_scope
81
+ .group(:app_version)
82
+ .select(
83
+ :app_version,
84
+ "COUNT(*) AS total_count",
85
+ "COUNT(DISTINCT error_type) AS unique_types",
86
+ "MIN(occurred_at) AS first_seen_at",
87
+ "MAX(occurred_at) AS last_seen_at"
88
+ )
89
+
90
+ # Collect git_shas per version in a second lightweight query
91
+ sha_map = {}
92
+ if has_git_sha_column?
93
+ base_scope.where.not(git_sha: [ nil, "" ])
94
+ .group(:app_version, :git_sha)
95
+ .pluck(:app_version, :git_sha)
96
+ .each do |version, sha|
97
+ (sha_map[version] ||= []) << sha
98
+ end
99
+ sha_map.each_value(&:uniq!)
100
+ end
101
+
102
+ rows.each_with_object({}) do |row, result|
103
+ version = row.app_version
104
+ first_seen = row.first_seen_at
105
+ last_seen = row.last_seen_at
106
+ first_seen = Time.zone.parse(first_seen) if first_seen.is_a?(String)
107
+ last_seen = Time.zone.parse(last_seen) if last_seen.is_a?(String)
108
+
109
+ result[version] = {
110
+ total_errors: row.total_count.to_i,
111
+ unique_error_types: row.unique_types.to_i,
112
+ first_seen: first_seen,
113
+ last_seen: last_seen,
114
+ git_shas: sha_map[version] || []
115
+ }
116
+ end
117
+ rescue => e
118
+ Rails.logger.error("[RailsErrorDashboard] ReleaseTimeline aggregate failed: #{e.class}: #{e.message}")
119
+ {}
120
+ end
121
+
122
+ # For each error_hash in the window, find which app_version it first appeared in.
123
+ # Count per version to get "new errors introduced in this release".
124
+ def new_errors_per_version
125
+ return {} unless has_error_hash_column?
126
+
127
+ # Get the earliest occurrence per error_hash, with its app_version
128
+ # We need (error_hash, app_version) at MIN(occurred_at)
129
+ earliest = {}
130
+ base_scope.select(:error_hash, :app_version, :occurred_at)
131
+ .where.not(error_hash: [ nil, "" ])
132
+ .order(:occurred_at)
133
+ .each do |row|
134
+ earliest[row.error_hash] ||= row.app_version
135
+ end
136
+
137
+ # Count how many error_hashes have each version as their earliest
138
+ earliest.values.tally
139
+ rescue => e
140
+ Rails.logger.error("[RailsErrorDashboard] ReleaseTimeline new_errors failed: #{e.class}: #{e.message}")
141
+ {}
142
+ end
143
+
144
+ def stability_indicator(count, avg)
145
+ return :green if avg <= 0
146
+ ratio = count.to_f / avg
147
+ if ratio <= 1.0
148
+ :green
149
+ elsif ratio <= 2.0
150
+ :yellow
151
+ else
152
+ :red
153
+ end
154
+ end
155
+
156
+ def build_summary(releases)
157
+ {
158
+ total_releases: releases.size,
159
+ current_version: releases.first&.dig(:version),
160
+ avg_errors_per_release: releases.any? ? (releases.sum { |r| r[:total_errors] }.to_f / releases.size).round(1) : 0
161
+ }
162
+ end
163
+
164
+ def empty_result
165
+ { releases: [], summary: { total_releases: 0, current_version: nil, avg_errors_per_release: 0 } }
166
+ end
167
+
168
+ def has_version_column?
169
+ ErrorLog.column_names.include?("app_version")
170
+ end
171
+
172
+ def has_git_sha_column?
173
+ ErrorLog.column_names.include?("git_sha")
174
+ end
175
+
176
+ def has_error_hash_column?
177
+ ErrorLog.column_names.include?("error_hash")
178
+ end
179
+ end
180
+ end
181
+ end
@@ -49,17 +49,15 @@ module RailsErrorDashboard
49
49
 
50
50
  def should_auto_create?(error_log)
51
51
  config = RailsErrorDashboard.configuration
52
- return false unless config.enable_issue_tracking && config.auto_create_issues
52
+ return false unless config.enable_issue_tracking
53
53
  return false if error_log.external_issue_url.present?
54
54
 
55
- # First occurrence check
56
- if config.auto_create_issues_on_first_occurrence && error_log.occurrence_count == 1
57
- return true
58
- end
55
+ # First occurrence — always auto-create
56
+ return true if error_log.occurrence_count == 1
59
57
 
60
58
  # Severity threshold check
61
59
  severity = error_log.severity&.to_sym
62
- if config.auto_create_issues_for_severities&.include?(severity)
60
+ if config.issue_tracker_auto_create_severities&.include?(severity)
63
61
  return true
64
62
  end
65
63
 
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.5.9"
2
+ VERSION = "0.5.10"
3
3
  end
@@ -119,6 +119,7 @@ require "rails_error_dashboard/queries/job_health_summary"
119
119
  require "rails_error_dashboard/queries/database_health_summary"
120
120
  require "rails_error_dashboard/queries/swallowed_exception_summary"
121
121
  require "rails_error_dashboard/queries/rack_attack_summary"
122
+ require "rails_error_dashboard/queries/release_timeline"
122
123
  require "rails_error_dashboard/error_reporter"
123
124
  require "rails_error_dashboard/middleware/error_catcher"
124
125
  require "rails_error_dashboard/middleware/rate_limiter"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.9
4
+ version: 0.5.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -310,6 +310,7 @@ files:
310
310
  - app/views/rails_error_dashboard/errors/overview.html.erb
311
311
  - app/views/rails_error_dashboard/errors/platform_comparison.html.erb
312
312
  - app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb
313
+ - app/views/rails_error_dashboard/errors/releases.html.erb
313
314
  - app/views/rails_error_dashboard/errors/settings.html.erb
314
315
  - app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb
315
316
  - app/views/rails_error_dashboard/errors/show.html.erb
@@ -414,6 +415,7 @@ files:
414
415
  - lib/rails_error_dashboard/queries/platform_comparison.rb
415
416
  - lib/rails_error_dashboard/queries/rack_attack_summary.rb
416
417
  - lib/rails_error_dashboard/queries/recurring_issues.rb
418
+ - lib/rails_error_dashboard/queries/release_timeline.rb
417
419
  - lib/rails_error_dashboard/queries/similar_errors.rb
418
420
  - lib/rails_error_dashboard/queries/swallowed_exception_summary.rb
419
421
  - lib/rails_error_dashboard/services/analytics_cache_manager.rb
@@ -485,7 +487,7 @@ metadata:
485
487
  bug_tracker_uri: https://github.com/AnjanJ/rails_error_dashboard/issues
486
488
  funding_uri: https://github.com/sponsors/AnjanJ
487
489
  post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
488
- \ Rails Error Dashboard v0.5.9\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
490
+ \ Rails Error Dashboard v0.5.10\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
489
491
  First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
490
492
  db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
491
493
  => '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n