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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -1
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +57 -4
  4. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +192 -0
  5. data/app/jobs/rails_error_dashboard/add_issue_recurrence_comment_job.rb +71 -0
  6. data/app/jobs/rails_error_dashboard/close_linked_issue_job.rb +43 -0
  7. data/app/jobs/rails_error_dashboard/create_issue_job.rb +68 -0
  8. data/app/jobs/rails_error_dashboard/reopen_linked_issue_job.rb +44 -0
  9. data/app/models/rails_error_dashboard/error_log.rb +2 -1
  10. data/app/views/layouts/rails_error_dashboard.html.erb +19 -6
  11. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +61 -102
  12. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +67 -0
  13. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +148 -0
  14. data/app/views/rails_error_dashboard/errors/show.html.erb +3 -1
  15. data/config/routes.rb +7 -1
  16. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +5 -0
  17. data/db/migrate/20260326000001_add_issue_tracking_to_error_logs.rb +15 -0
  18. data/lib/generators/rails_error_dashboard/install/install_generator.rb +12 -4
  19. data/lib/rails_error_dashboard/commands/create_issue.rb +59 -0
  20. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +65 -0
  21. data/lib/rails_error_dashboard/configuration.rb +99 -0
  22. data/lib/rails_error_dashboard/engine.rb +39 -0
  23. data/lib/rails_error_dashboard/queries/active_storage_summary.rb +101 -0
  24. data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +99 -0
  25. data/lib/rails_error_dashboard/services/github_issue_client.rb +94 -0
  26. data/lib/rails_error_dashboard/services/github_link_generator.rb +19 -1
  27. data/lib/rails_error_dashboard/services/gitlab_issue_client.rb +98 -0
  28. data/lib/rails_error_dashboard/services/issue_body_formatter.rb +132 -0
  29. data/lib/rails_error_dashboard/services/issue_tracker_client.rb +162 -0
  30. data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +129 -10
  31. data/lib/rails_error_dashboard/subscribers/active_storage_subscriber.rb +112 -0
  32. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +71 -0
  33. data/lib/rails_error_dashboard/version.rb +1 -1
  34. data/lib/rails_error_dashboard.rb +11 -1
  35. metadata +20 -2
@@ -1,107 +1,66 @@
1
- <!-- Phase 3: Comment Threads -->
2
- <% if error.respond_to?(:comments) %>
3
- <div class="card mb-4" id="section-discussion">
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
- <!-- Add Comment Form -->
43
- <div class="<%= error.comments.any? ? '' : 'border-top' %> pt-3">
44
- <h6 class="mb-3">
45
- <i class="bi bi-plus-circle"></i> Add Comment
46
- </h6>
47
- <%= form_with url: add_comment_error_path(error), method: :post do |f| %>
48
- <div class="mb-3">
49
- <label for="author_name" class="form-label">Your Name <span class="text-danger">*</span></label>
50
- <%= text_field_tag :author_name, error.assigned_to, class: "form-control", placeholder: "e.g., John Doe", required: true %>
51
- </div>
52
- <div class="mb-3">
53
- <label for="body" class="form-label">Comment <span class="text-danger">*</span></label>
54
-
55
- <!-- Quick Templates -->
56
- <div class="mb-2">
57
- <small class="metadata-label d-block mb-1">
58
- <i class="bi bi-lightning-fill"></i> Quick templates:
59
- </small>
60
- <div class="d-flex flex-wrap gap-1">
61
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('investigating')">
62
- <i class="bi bi-search"></i> Investigating
63
- </button>
64
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('found_fix')">
65
- <i class="bi bi-wrench"></i> Found Fix
66
- </button>
67
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('need_info')">
68
- <i class="bi bi-question-circle"></i> Need Info
69
- </button>
70
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('duplicate')">
71
- <i class="bi bi-files"></i> Duplicate
72
- </button>
73
- <button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('cannot_reproduce')">
74
- <i class="bi bi-x-circle"></i> Cannot Reproduce
75
- </button>
76
- </div>
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
- <%= text_area_tag :body, nil, class: "form-control", rows: 4, placeholder: "Share your thoughts, findings, or updates...", required: true, id: "comment_body" %>
80
- </div>
81
- <%= submit_tag "Post Comment", class: "btn btn-primary" %>
82
- <% end %>
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
- </div>
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(/\\n/g, '\n'), this)" data-markdown="<%= j @error_markdown %>" title="Copy error details as Markdown for LLM debugging">
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 :add_comment
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 /error_dashboard"
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/error_dashboard"
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/error_dashboard"
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