rails_error_dashboard 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +27 -1
- data/app/controllers/rails_error_dashboard/errors_controller.rb +57 -4
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +192 -0
- data/app/jobs/rails_error_dashboard/add_issue_recurrence_comment_job.rb +71 -0
- data/app/jobs/rails_error_dashboard/close_linked_issue_job.rb +43 -0
- data/app/jobs/rails_error_dashboard/create_issue_job.rb +68 -0
- data/app/jobs/rails_error_dashboard/reopen_linked_issue_job.rb +44 -0
- data/app/models/rails_error_dashboard/error_log.rb +2 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +19 -6
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +61 -102
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +67 -0
- data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +148 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +3 -1
- data/config/routes.rb +7 -1
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +5 -0
- data/db/migrate/20260326000001_add_issue_tracking_to_error_logs.rb +15 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +12 -4
- data/lib/rails_error_dashboard/commands/create_issue.rb +59 -0
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +65 -0
- data/lib/rails_error_dashboard/configuration.rb +99 -0
- data/lib/rails_error_dashboard/engine.rb +39 -0
- data/lib/rails_error_dashboard/queries/active_storage_summary.rb +101 -0
- data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +99 -0
- data/lib/rails_error_dashboard/services/github_issue_client.rb +94 -0
- data/lib/rails_error_dashboard/services/github_link_generator.rb +19 -1
- data/lib/rails_error_dashboard/services/gitlab_issue_client.rb +98 -0
- data/lib/rails_error_dashboard/services/issue_body_formatter.rb +132 -0
- data/lib/rails_error_dashboard/services/issue_tracker_client.rb +162 -0
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +129 -10
- data/lib/rails_error_dashboard/subscribers/active_storage_subscriber.rb +112 -0
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +71 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +11 -1
- metadata +20 -2
|
@@ -1,107 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<div class="card-header bg-white">
|
|
5
|
-
<h5 class="mb-0">
|
|
6
|
-
<i class="bi bi-chat-dots"></i> Discussion
|
|
7
|
-
<span class="badge bg-secondary ms-2"><%= error.comments.count %></span>
|
|
8
|
-
</h5>
|
|
9
|
-
</div>
|
|
10
|
-
<div class="card-body">
|
|
11
|
-
<!-- Existing Comments -->
|
|
12
|
-
<% if error.comments.recent_first.any? %>
|
|
13
|
-
<div class="mb-4">
|
|
14
|
-
<% error.comments.recent_first.each_with_index do |comment, index| %>
|
|
15
|
-
<div class="<%= index < error.comments.count - 1 ? 'border-bottom' : '' %> pb-3 mb-3">
|
|
16
|
-
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
17
|
-
<div>
|
|
18
|
-
<strong class="text-primary">
|
|
19
|
-
<i class="bi bi-person-circle"></i> <%= comment.author_name %>
|
|
20
|
-
</strong>
|
|
21
|
-
<% if comment.recent? %>
|
|
22
|
-
<span class="badge bg-success ms-2">New</span>
|
|
23
|
-
<% end %>
|
|
24
|
-
</div>
|
|
25
|
-
<small class="text-muted">
|
|
26
|
-
<%= local_time(comment.created_at, format: :datetime) %>
|
|
27
|
-
<span class="ms-1 text-muted">(<%= local_time_ago(comment.created_at) %>)</span>
|
|
28
|
-
</small>
|
|
29
|
-
</div>
|
|
30
|
-
<div class="text-break">
|
|
31
|
-
<%= auto_link_urls(comment.body, error: error).html_safe %>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
<% end %>
|
|
35
|
-
</div>
|
|
36
|
-
<% else %>
|
|
37
|
-
<p class="text-muted mb-4">
|
|
38
|
-
<i class="bi bi-info-circle"></i> No comments yet. Start the discussion below.
|
|
39
|
-
</p>
|
|
40
|
-
<% end %>
|
|
1
|
+
<% has_linked_issue = error.respond_to?(:external_issue_url) && error.external_issue_url.present? %>
|
|
2
|
+
<% has_comments = error.respond_to?(:comments) && error.comments.any? %>
|
|
3
|
+
<% issue_tracking_enabled = RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
41
4
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</div>
|
|
5
|
+
<% if has_linked_issue || has_comments || issue_tracking_enabled %>
|
|
6
|
+
<div class="card mb-4" id="section-discussion">
|
|
7
|
+
<div class="card-header bg-white">
|
|
8
|
+
<h5 class="mb-0">
|
|
9
|
+
<i class="bi bi-chat-dots"></i> Discussion
|
|
10
|
+
<% if has_comments %>
|
|
11
|
+
<span class="badge bg-secondary ms-2"><%= error.comments.count %></span>
|
|
12
|
+
<% end %>
|
|
13
|
+
</h5>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card-body">
|
|
16
|
+
<% if has_linked_issue %>
|
|
17
|
+
<!-- Platform discussion link -->
|
|
18
|
+
<% provider = error.external_issue_provider&.capitalize || "Platform" %>
|
|
19
|
+
<% icon = case error.external_issue_provider
|
|
20
|
+
when "github" then "bi-github"
|
|
21
|
+
when "gitlab" then "bi-gitlab"
|
|
22
|
+
when "codeberg" then "bi-git"
|
|
23
|
+
else "bi-chat-dots"
|
|
24
|
+
end %>
|
|
25
|
+
<div class="text-center py-3 mb-3">
|
|
26
|
+
<a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-primary">
|
|
27
|
+
<i class="bi <%= icon %> me-1"></i>
|
|
28
|
+
Discuss on <%= provider %> #<%= error.external_issue_number || "?" %>
|
|
29
|
+
<i class="bi bi-box-arrow-up-right ms-1" style="font-size: 0.8em;"></i>
|
|
30
|
+
</a>
|
|
31
|
+
</div>
|
|
32
|
+
<% elsif issue_tracking_enabled %>
|
|
33
|
+
<div class="text-center py-3 mb-3">
|
|
34
|
+
<p class="text-muted small mb-0">
|
|
35
|
+
<i class="bi bi-link-45deg"></i>
|
|
36
|
+
Link or create an issue above to start a discussion on your issue tracker.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
78
40
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
41
|
+
<% if has_comments %>
|
|
42
|
+
<!-- Audit trail — read-only workflow comments (snooze, mute, status changes) -->
|
|
43
|
+
<% if has_linked_issue || issue_tracking_enabled %>
|
|
44
|
+
<hr class="my-3">
|
|
45
|
+
<h6 class="text-muted mb-3"><i class="bi bi-clock-history"></i> Activity Log</h6>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% error.comments.recent_first.each_with_index do |comment, index| %>
|
|
48
|
+
<div class="<%= index < error.comments.count - 1 ? 'border-bottom' : '' %> pb-3 mb-3">
|
|
49
|
+
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
50
|
+
<div>
|
|
51
|
+
<strong class="text-primary">
|
|
52
|
+
<i class="bi bi-person-circle"></i> <%= comment.author_name %>
|
|
53
|
+
</strong>
|
|
54
|
+
<small class="text-muted ms-2"><%= comment.formatted_time %></small>
|
|
55
|
+
<% if comment.recent? %>
|
|
56
|
+
<span class="badge bg-info ms-1">New</span>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
83
59
|
</div>
|
|
84
|
-
|
|
85
|
-
<script>
|
|
86
|
-
function insertTemplate(templateType) {
|
|
87
|
-
const textarea = document.getElementById('comment_body');
|
|
88
|
-
const templates = {
|
|
89
|
-
investigating: "🔍 Investigating this issue now. Will update with findings.",
|
|
90
|
-
found_fix: "✅ Found the fix!\n\nRoot cause: \nSolution: \nPR: ",
|
|
91
|
-
need_info: "ℹ️ Need more information:\n\n- \n- \n\nPlease provide details to help debug this issue.",
|
|
92
|
-
duplicate: "📋 This appears to be a duplicate of error #\n\nClosing as duplicate.",
|
|
93
|
-
cannot_reproduce: "❌ Cannot reproduce this issue.\n\nAttempted:\n- \n- \n\nNeed more details or steps to reproduce."
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const template = templates[templateType];
|
|
97
|
-
if (template) {
|
|
98
|
-
textarea.value = template;
|
|
99
|
-
textarea.focus();
|
|
100
|
-
// Move cursor to end
|
|
101
|
-
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
</script>
|
|
60
|
+
<p class="mb-0 text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></p>
|
|
105
61
|
</div>
|
|
106
|
-
|
|
62
|
+
<% end %>
|
|
107
63
|
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<% if RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
2
|
+
<div class="card mb-4" id="issue-tracking">
|
|
3
|
+
<div class="card-header bg-white">
|
|
4
|
+
<h5 class="mb-0">
|
|
5
|
+
<i class="bi bi-link-45deg me-2"></i>
|
|
6
|
+
Issue Tracker
|
|
7
|
+
</h5>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="card-body">
|
|
10
|
+
<% if @error.external_issue_url.present? %>
|
|
11
|
+
<!-- Linked issue display -->
|
|
12
|
+
<div class="d-flex align-items-center justify-content-between">
|
|
13
|
+
<div>
|
|
14
|
+
<% provider = @error.external_issue_provider %>
|
|
15
|
+
<% icon = case provider
|
|
16
|
+
when "github" then "bi-github"
|
|
17
|
+
when "gitlab" then "bi-gitlab"
|
|
18
|
+
when "codeberg" then "bi-git"
|
|
19
|
+
else "bi-link-45deg"
|
|
20
|
+
end %>
|
|
21
|
+
<span class="badge bg-success me-2">
|
|
22
|
+
<i class="bi <%= icon %> me-1"></i>
|
|
23
|
+
<%= provider&.capitalize || "Linked" %> #<%= @error.external_issue_number || "?" %>
|
|
24
|
+
</span>
|
|
25
|
+
<a href="<%= @error.external_issue_url %>" target="_blank" rel="noopener" class="text-decoration-none">
|
|
26
|
+
View Issue <i class="bi bi-box-arrow-up-right" style="font-size: 0.8em;"></i>
|
|
27
|
+
</a>
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
<a href="<%= @error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary me-1">
|
|
31
|
+
<i class="bi bi-chat-dots"></i> Discuss on <%= provider&.capitalize || "Platform" %>
|
|
32
|
+
</a>
|
|
33
|
+
<%= button_to "Unlink", unlink_issue_error_path(@error), method: :post, class: "btn btn-sm btn-outline-danger", data: { confirm: "Remove issue link?" } %>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<% else %>
|
|
37
|
+
<!-- No linked issue — show create/link options -->
|
|
38
|
+
<div class="row">
|
|
39
|
+
<div class="col-md-6">
|
|
40
|
+
<h6 class="text-muted mb-2">Create New Issue</h6>
|
|
41
|
+
<% provider = RailsErrorDashboard.configuration.effective_issue_tracker_provider %>
|
|
42
|
+
<% if provider && RailsErrorDashboard.configuration.effective_issue_tracker_token %>
|
|
43
|
+
<%= button_to create_issue_error_path(@error), method: :post, class: "btn btn-sm btn-primary" do %>
|
|
44
|
+
<i class="bi bi-plus-circle me-1"></i>
|
|
45
|
+
Create <%= provider.to_s.capitalize %> Issue
|
|
46
|
+
<% end %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<p class="text-muted small mb-0">
|
|
49
|
+
<i class="bi bi-info-circle"></i>
|
|
50
|
+
Configure <code>issue_tracker_token</code> and <code>git_repository_url</code> in your initializer to enable issue creation.
|
|
51
|
+
</p>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="col-md-6">
|
|
55
|
+
<h6 class="text-muted mb-2">Link Existing Issue</h6>
|
|
56
|
+
<%= form_tag link_issue_error_path(@error), method: :post, class: "d-flex gap-2" do %>
|
|
57
|
+
<%= text_field_tag :issue_url, nil, placeholder: "https://github.com/user/repo/issues/42", class: "form-control form-control-sm", required: true %>
|
|
58
|
+
<button type="submit" class="btn btn-sm btn-outline-secondary text-nowrap">
|
|
59
|
+
<i class="bi bi-link"></i> Link
|
|
60
|
+
</button>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<% content_for :page_title, "ActiveStorage Health" %>
|
|
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-cloud-arrow-up me-2"></i>
|
|
7
|
+
ActiveStorage Health
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<div class="btn-group" role="group">
|
|
11
|
+
<%= link_to activestorage_health_summary_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
12
|
+
7 Days
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to activestorage_health_summary_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
15
|
+
30 Days
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to activestorage_health_summary_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 @unique_services == 0 %>
|
|
24
|
+
<div class="text-center py-5">
|
|
25
|
+
<i class="bi bi-cloud-arrow-up display-1 text-success mb-3"></i>
|
|
26
|
+
<h4 class="text-muted">No ActiveStorage Events Found</h4>
|
|
27
|
+
<p class="text-muted">
|
|
28
|
+
No ActiveStorage service operations were detected in error breadcrumbs 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 ActiveStorage tracking works:</h6>
|
|
33
|
+
<ul class="mb-0">
|
|
34
|
+
<li>Breadcrumbs must be enabled (<code>enable_breadcrumbs = true</code>)</li>
|
|
35
|
+
<li>ActiveStorage tracking must be enabled (<code>enable_activestorage_tracking = true</code>)</li>
|
|
36
|
+
<li>ActiveStorage must be configured in your app</li>
|
|
37
|
+
<li>Uploads, downloads, and deletes are captured as breadcrumbs during requests that produce errors</li>
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<% else %>
|
|
43
|
+
<div class="row mb-4">
|
|
44
|
+
<div class="col-md-4">
|
|
45
|
+
<div class="card text-center">
|
|
46
|
+
<div class="card-body">
|
|
47
|
+
<div class="display-6 text-primary"><%= @unique_services %></div>
|
|
48
|
+
<small class="text-muted">Storage Services</small>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="col-md-4">
|
|
53
|
+
<div class="card text-center">
|
|
54
|
+
<div class="card-body">
|
|
55
|
+
<div class="display-6 text-info"><%= @total_operations %></div>
|
|
56
|
+
<small class="text-muted">Total Operations</small>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="col-md-4">
|
|
61
|
+
<div class="card text-center">
|
|
62
|
+
<div class="card-body">
|
|
63
|
+
<div class="display-6 text-warning"><%= @errors_with_storage %></div>
|
|
64
|
+
<small class="text-muted">Errors with Storage Ops</small>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="card mb-4">
|
|
71
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
72
|
+
<h5 class="mb-0">
|
|
73
|
+
<i class="bi bi-cloud-arrow-up text-primary me-2"></i>
|
|
74
|
+
Storage Operations by Service
|
|
75
|
+
<span class="badge bg-primary"><%= @unique_services %></span>
|
|
76
|
+
</h5>
|
|
77
|
+
<small class="text-muted"><%== @pagy.info_tag %></small>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="card-body p-0">
|
|
80
|
+
<div class="table-responsive">
|
|
81
|
+
<table class="table table-hover mb-0">
|
|
82
|
+
<thead class="table-light">
|
|
83
|
+
<tr>
|
|
84
|
+
<th>Service</th>
|
|
85
|
+
<th width="90">Uploads</th>
|
|
86
|
+
<th width="100">Downloads</th>
|
|
87
|
+
<th width="90">Deletes</th>
|
|
88
|
+
<th width="90">Exists</th>
|
|
89
|
+
<th width="80">Total</th>
|
|
90
|
+
<th width="100">Avg Duration</th>
|
|
91
|
+
<th width="100">Slowest</th>
|
|
92
|
+
<th width="80">Errors</th>
|
|
93
|
+
<th width="140">Last Seen</th>
|
|
94
|
+
</tr>
|
|
95
|
+
</thead>
|
|
96
|
+
<tbody>
|
|
97
|
+
<% @services.each do |svc| %>
|
|
98
|
+
<tr>
|
|
99
|
+
<td><code><%= svc[:service] %></code></td>
|
|
100
|
+
<td><%= svc[:upload_count] %></td>
|
|
101
|
+
<td><%= svc[:download_count] %></td>
|
|
102
|
+
<td><%= svc[:delete_count] %></td>
|
|
103
|
+
<td><%= svc[:exist_count] %></td>
|
|
104
|
+
<td><strong><%= svc[:total_operations] %></strong></td>
|
|
105
|
+
<td>
|
|
106
|
+
<% if svc[:avg_duration_ms] %>
|
|
107
|
+
<span class="<%= svc[:avg_duration_ms] > 100 ? 'text-danger' : svc[:avg_duration_ms] > 50 ? 'text-warning' : 'text-success' %>">
|
|
108
|
+
<%= svc[:avg_duration_ms] %>ms
|
|
109
|
+
</span>
|
|
110
|
+
<% else %>
|
|
111
|
+
<span class="text-muted">—</span>
|
|
112
|
+
<% end %>
|
|
113
|
+
</td>
|
|
114
|
+
<td>
|
|
115
|
+
<% if svc[:slowest_ms] %>
|
|
116
|
+
<span class="<%= svc[:slowest_ms] > 500 ? 'text-danger' : svc[:slowest_ms] > 100 ? 'text-warning' : 'text-muted' %>">
|
|
117
|
+
<%= svc[:slowest_ms].round(1) %>ms
|
|
118
|
+
</span>
|
|
119
|
+
<% else %>
|
|
120
|
+
<span class="text-muted">—</span>
|
|
121
|
+
<% end %>
|
|
122
|
+
</td>
|
|
123
|
+
<td><%= svc[:error_count] %></td>
|
|
124
|
+
<td><%= local_time_ago(svc[:last_seen]) %></td>
|
|
125
|
+
</tr>
|
|
126
|
+
<% end %>
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
|
|
132
|
+
<div>
|
|
133
|
+
<small class="text-muted">
|
|
134
|
+
<i class="bi bi-lightbulb text-warning"></i> Slow storage operations may indicate network issues, large file sizes, or service provider throttling.
|
|
135
|
+
</small>
|
|
136
|
+
<small class="ms-3">
|
|
137
|
+
<a href="https://guides.rubyonrails.org/active_storage_overview.html" target="_blank" rel="noopener" class="text-decoration-none">
|
|
138
|
+
<i class="bi bi-book"></i> ActiveStorage Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
|
|
139
|
+
</a>
|
|
140
|
+
</small>
|
|
141
|
+
</div>
|
|
142
|
+
<div>
|
|
143
|
+
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<% end %>
|
|
148
|
+
</div>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<button type="button" class="btn btn-outline-secondary" onclick="downloadErrorJSON(event)" title="Download error details as JSON">
|
|
33
33
|
<i class="bi bi-download"></i> Export JSON
|
|
34
34
|
</button>
|
|
35
|
-
<button type="button" class="btn btn-outline-secondary" onclick="copyToClipboard(this.dataset.markdown.replace(/\\
|
|
35
|
+
<button type="button" class="btn btn-outline-secondary" onclick="copyToClipboard(this.dataset.markdown.replace(/\\(.)/g, function(m,c){return c==='n'?'\n':c}), this)" data-markdown="<%= j @error_markdown %>" title="Copy error details as Markdown for LLM debugging">
|
|
36
36
|
<i class="bi bi-clipboard"></i> Copy for LLM
|
|
37
37
|
</button>
|
|
38
38
|
<% if @error.respond_to?(:muted?) && @error.muted? %>
|
|
@@ -82,6 +82,8 @@
|
|
|
82
82
|
<!-- Timeline: Related Errors -->
|
|
83
83
|
<%= render "timeline" %>
|
|
84
84
|
|
|
85
|
+
<%= render "issue_section", error: @error %>
|
|
86
|
+
|
|
85
87
|
<%= render "discussion", error: @error %>
|
|
86
88
|
|
|
87
89
|
<%= render "error_cascades", error: @error %>
|
data/config/routes.rb
CHANGED
|
@@ -7,6 +7,9 @@ RailsErrorDashboard::Engine.routes.draw do
|
|
|
7
7
|
# Settings page
|
|
8
8
|
get "settings", to: "errors#settings", as: :settings
|
|
9
9
|
|
|
10
|
+
# Webhook endpoint for two-way issue sync (GitHub/GitLab/Codeberg)
|
|
11
|
+
post "webhooks/:provider", to: "webhooks#receive", as: :webhook
|
|
12
|
+
|
|
10
13
|
resources :errors, only: [ :index, :show ] do
|
|
11
14
|
member do
|
|
12
15
|
post :resolve
|
|
@@ -18,7 +21,9 @@ RailsErrorDashboard::Engine.routes.draw do
|
|
|
18
21
|
post :mute
|
|
19
22
|
post :unmute
|
|
20
23
|
post :update_status
|
|
21
|
-
post :
|
|
24
|
+
post :create_issue
|
|
25
|
+
post :link_issue
|
|
26
|
+
post :unlink_issue
|
|
22
27
|
end
|
|
23
28
|
collection do
|
|
24
29
|
get :analytics
|
|
@@ -32,6 +37,7 @@ RailsErrorDashboard::Engine.routes.draw do
|
|
|
32
37
|
get :swallowed_exceptions
|
|
33
38
|
get :rack_attack_summary
|
|
34
39
|
get :actioncable_health_summary
|
|
40
|
+
get :activestorage_health_summary
|
|
35
41
|
get :diagnostic_dumps
|
|
36
42
|
post :create_diagnostic_dump
|
|
37
43
|
post :batch_action
|
|
@@ -101,6 +101,11 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
|
|
|
101
101
|
t.string :muted_by
|
|
102
102
|
t.string :muted_reason
|
|
103
103
|
|
|
104
|
+
# Issue tracking (GitHub, GitLab, Codeberg)
|
|
105
|
+
t.string :external_issue_url
|
|
106
|
+
t.integer :external_issue_number
|
|
107
|
+
t.string :external_issue_provider, limit: 20
|
|
108
|
+
|
|
104
109
|
t.timestamps
|
|
105
110
|
end
|
|
106
111
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Add columns for linking errors to external issue trackers (GitHub, GitLab, Codeberg/Gitea/Forgejo).
|
|
4
|
+
# These columns store the relationship between an error and its tracked issue.
|
|
5
|
+
#
|
|
6
|
+
# See: https://github.com/AnjanJ/rails_error_dashboard
|
|
7
|
+
class AddIssueTrackingToErrorLogs < ActiveRecord::Migration[7.0]
|
|
8
|
+
def change
|
|
9
|
+
return unless table_exists?(:rails_error_dashboard_error_logs)
|
|
10
|
+
|
|
11
|
+
add_column :rails_error_dashboard_error_logs, :external_issue_url, :string unless column_exists?(:rails_error_dashboard_error_logs, :external_issue_url)
|
|
12
|
+
add_column :rails_error_dashboard_error_logs, :external_issue_number, :integer unless column_exists?(:rails_error_dashboard_error_logs, :external_issue_number)
|
|
13
|
+
add_column :rails_error_dashboard_error_logs, :external_issue_provider, :string, limit: 20 unless column_exists?(:rails_error_dashboard_error_logs, :external_issue_provider)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -44,7 +44,7 @@ module RailsErrorDashboard
|
|
|
44
44
|
say "\n"
|
|
45
45
|
say "Core features will be enabled automatically:", :green
|
|
46
46
|
say " ✓ Error capture (controllers, jobs, middleware)"
|
|
47
|
-
say " ✓ Dashboard UI at /
|
|
47
|
+
say " ✓ Dashboard UI at /red"
|
|
48
48
|
say " ✓ Real-time updates"
|
|
49
49
|
say " ✓ Analytics & spike detection"
|
|
50
50
|
say " ✓ 90-day error retention"
|
|
@@ -398,7 +398,7 @@ module RailsErrorDashboard
|
|
|
398
398
|
end
|
|
399
399
|
|
|
400
400
|
def add_route
|
|
401
|
-
route "mount RailsErrorDashboard::Engine => '/error_dashboard
|
|
401
|
+
route "mount RailsErrorDashboard::Engine => '/red' # RED (Rails Error Dashboard) — also works at /error_dashboard"
|
|
402
402
|
end
|
|
403
403
|
|
|
404
404
|
def show_feature_summary
|
|
@@ -510,13 +510,13 @@ module RailsErrorDashboard
|
|
|
510
510
|
end
|
|
511
511
|
say " 4. Update credentials in config/initializers/rails_error_dashboard.rb"
|
|
512
512
|
say " 5. Restart your Rails server"
|
|
513
|
-
say " 6. Visit http://localhost:3000/
|
|
513
|
+
say " 6. Visit http://localhost:3000/red"
|
|
514
514
|
say " 7. Verify: rails error_dashboard:verify"
|
|
515
515
|
else
|
|
516
516
|
say " 1. Run: rails db:migrate"
|
|
517
517
|
say " 2. Update credentials in config/initializers/rails_error_dashboard.rb"
|
|
518
518
|
say " 3. Restart your Rails server"
|
|
519
|
-
say " 4. Visit http://localhost:3000/
|
|
519
|
+
say " 4. Visit http://localhost:3000/red"
|
|
520
520
|
end
|
|
521
521
|
say "Authentication:", :cyan
|
|
522
522
|
say " Default: HTTP Basic Auth (gandalf/youshallnotpass)", :white
|
|
@@ -524,6 +524,14 @@ module RailsErrorDashboard
|
|
|
524
524
|
say " Session-based: config.authenticate_with = -> { session[:admin] == true }", :white
|
|
525
525
|
say " See: https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md#custom-authentication", :white
|
|
526
526
|
say "\n"
|
|
527
|
+
say "Issue Tracking (optional):", :cyan
|
|
528
|
+
say " Create a dedicated RED (Rails Error Dashboard) bot account on your platform:", :white
|
|
529
|
+
say " GitHub: github.com/join → username: 'red-bot' or 'yourapp-red'", :white
|
|
530
|
+
say " GitLab: Use a Project Access Token (Settings > Access Tokens)", :white
|
|
531
|
+
say " Codeberg: codeberg.org → username: 'red-bot'", :white
|
|
532
|
+
say " Then: config.issue_tracker_token = ENV['RED_BOT_TOKEN']", :white
|
|
533
|
+
say " Issues will appear as created by your RED bot account.", :white
|
|
534
|
+
say "\n"
|
|
527
535
|
say "Documentation:", :white
|
|
528
536
|
say " Quick Start: https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/QUICKSTART.md", :white
|
|
529
537
|
say " Database Setup: https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/DATABASE_OPTIONS.md", :white
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Command: Create an issue on the configured issue tracker (GitHub/GitLab/Codeberg)
|
|
6
|
+
#
|
|
7
|
+
# Creates the issue via the provider API, then stores the issue URL, number,
|
|
8
|
+
# and provider on the error record for linking.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# result = CreateIssue.call(error_id, dashboard_url: "https://app.com/error_dashboard/errors/42")
|
|
12
|
+
# result[:success] # => true
|
|
13
|
+
# result[:issue_url] # => "https://github.com/user/repo/issues/42"
|
|
14
|
+
class CreateIssue
|
|
15
|
+
def self.call(error_id, dashboard_url: nil)
|
|
16
|
+
new(error_id, dashboard_url: dashboard_url).call
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(error_id, dashboard_url: nil)
|
|
20
|
+
@error_id = error_id
|
|
21
|
+
@dashboard_url = dashboard_url
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
error = ErrorLog.find(@error_id)
|
|
26
|
+
|
|
27
|
+
# Don't create duplicate issues
|
|
28
|
+
if error.external_issue_url.present?
|
|
29
|
+
return { success: false, error: "Error already has a linked issue: #{error.external_issue_url}" }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
client = Services::IssueTrackerClient.from_config
|
|
33
|
+
return { success: false, error: "Issue tracking is not configured" } unless client
|
|
34
|
+
|
|
35
|
+
config = RailsErrorDashboard.configuration
|
|
36
|
+
title = "[#{error.error_type}] #{error.message.to_s.truncate(100)}"
|
|
37
|
+
body = Services::IssueBodyFormatter.call(error, dashboard_url: @dashboard_url)
|
|
38
|
+
labels = config.issue_tracker_labels || []
|
|
39
|
+
|
|
40
|
+
result = client.create_issue(title: title, body: body, labels: labels)
|
|
41
|
+
|
|
42
|
+
if result[:success]
|
|
43
|
+
error.update!(
|
|
44
|
+
external_issue_url: result[:url],
|
|
45
|
+
external_issue_number: result[:number],
|
|
46
|
+
external_issue_provider: config.effective_issue_tracker_provider.to_s
|
|
47
|
+
)
|
|
48
|
+
{ success: true, issue_url: result[:url], issue_number: result[:number] }
|
|
49
|
+
else
|
|
50
|
+
{ success: false, error: result[:error] }
|
|
51
|
+
end
|
|
52
|
+
rescue ActiveRecord::RecordNotFound
|
|
53
|
+
{ success: false, error: "Error not found: #{@error_id}" }
|
|
54
|
+
rescue => e
|
|
55
|
+
{ success: false, error: "#{e.class}: #{e.message}" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Command: Link an existing issue URL to an error record
|
|
6
|
+
#
|
|
7
|
+
# Parses the URL to extract provider, owner/repo, and issue number.
|
|
8
|
+
# No API call needed — just stores the relationship.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# result = LinkExistingIssue.call(error_id, issue_url: "https://github.com/user/repo/issues/42")
|
|
12
|
+
# result[:success] # => true
|
|
13
|
+
class LinkExistingIssue
|
|
14
|
+
PROVIDER_PATTERNS = {
|
|
15
|
+
github: %r{github\.com/([^/]+/[^/]+)/issues/(\d+)}i,
|
|
16
|
+
gitlab: %r{gitlab\.com/([^/]+/[^/]+)/-/issues/(\d+)}i,
|
|
17
|
+
codeberg: %r{codeberg\.org/([^/]+/[^/]+)/issues/(\d+)}i
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def self.call(error_id, issue_url:)
|
|
21
|
+
new(error_id, issue_url: issue_url).call
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(error_id, issue_url:)
|
|
25
|
+
@error_id = error_id
|
|
26
|
+
@issue_url = issue_url.to_s.strip
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
return { success: false, error: "Issue URL is required" } if @issue_url.blank?
|
|
31
|
+
|
|
32
|
+
error = ErrorLog.find(@error_id)
|
|
33
|
+
parsed = parse_issue_url(@issue_url)
|
|
34
|
+
|
|
35
|
+
error.update!(
|
|
36
|
+
external_issue_url: @issue_url,
|
|
37
|
+
external_issue_number: parsed[:number],
|
|
38
|
+
external_issue_provider: parsed[:provider]&.to_s
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
{ success: true, issue_url: @issue_url, provider: parsed[:provider] }
|
|
42
|
+
rescue ActiveRecord::RecordNotFound
|
|
43
|
+
{ success: false, error: "Error not found: #{@error_id}" }
|
|
44
|
+
rescue => e
|
|
45
|
+
{ success: false, error: "#{e.class}: #{e.message}" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def parse_issue_url(url)
|
|
51
|
+
PROVIDER_PATTERNS.each do |provider, pattern|
|
|
52
|
+
match = url.match(pattern)
|
|
53
|
+
if match
|
|
54
|
+
return { provider: provider, repo: match[1], number: match[2].to_i }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Unknown provider — store URL without parsed details
|
|
59
|
+
# Try to extract issue number from common /issues/N pattern
|
|
60
|
+
number_match = url.match(%r{/issues/(\d+)}i)
|
|
61
|
+
{ provider: nil, repo: nil, number: number_match&.[](1)&.to_i }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|