rails_error_dashboard 0.5.8 → 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 +4 -4
- data/README.md +29 -8
- data/app/controllers/rails_error_dashboard/errors_controller.rb +51 -17
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +2 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +5 -0
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +76 -43
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +78 -24
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +1 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +88 -71
- data/app/views/rails_error_dashboard/errors/releases.html.erb +284 -0
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +15 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +32 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +10 -8
- data/config/routes.rb +1 -1
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +32 -2
- data/lib/rails_error_dashboard/configuration.rb +10 -13
- data/lib/rails_error_dashboard/queries/release_timeline.rb +181 -0
- data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +23 -0
- data/lib/rails_error_dashboard/services/github_issue_client.rb +23 -0
- data/lib/rails_error_dashboard/services/gitlab_issue_client.rb +23 -0
- data/lib/rails_error_dashboard/services/issue_tracker_client.rb +6 -0
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +4 -6
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +1 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3004ee5b49b622c064ceb8103c6abb2212f1a0cc269471a7281b49338213240
|
|
4
|
+
data.tar.gz: a8e27db79b78052909acc3ce04ab4875b31148b6e50b631b46bfe67eb0562fd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2dfe54a84c8a7049e6b51956d3512c6c18f8d37cabdd5f655bbdb046b6ed7ce2b287886f80a36a7bba85839090309eb81e823e29b5b766036f9b7ba726f7fef
|
|
7
|
+
data.tar.gz: fdcf6aea6b7f568da4a525330e63de6121aa087926ffd8982c631ec90efa5282081d428c7fd9069b4ed923f31a1c1445483c443f4b845d4677648b10c4d56fc4
|
data/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/rails_error_dashboard)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://github.com/AnjanJ/rails_error_dashboard/actions)
|
|
7
|
+
[](https://github.com/sponsors/AnjanJ)
|
|
7
8
|
[](https://buymeacoffee.com/anjanj)
|
|
8
9
|
|
|
9
10
|
**Self-hosted Rails error monitoring — free, forever.**
|
|
@@ -149,23 +150,43 @@ config.enable_activestorage_tracking = true # requires enable_breadcrumbs = tru
|
|
|
149
150
|
<details>
|
|
150
151
|
<summary><strong>Issue Tracking — GitHub, GitLab, Codeberg</strong></summary>
|
|
151
152
|
|
|
152
|
-
|
|
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.
|
|
153
154
|
|
|
154
|
-
- **
|
|
155
|
-
- **Auto-create:**
|
|
156
|
-
- **Lifecycle sync:** Resolve → close
|
|
157
|
-
- **
|
|
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
|
|
158
160
|
- **RED branding:** Issues show "Created by RED (Rails Error Dashboard)"
|
|
159
161
|
|
|
160
162
|
```ruby
|
|
161
163
|
config.enable_issue_tracking = true
|
|
162
164
|
config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
|
|
163
|
-
#
|
|
165
|
+
# That's it — provider and repo auto-detected from git_repository_url
|
|
164
166
|
```
|
|
165
167
|
|
|
166
168
|
[Complete documentation →](docs/guides/CONFIGURATION.md)
|
|
167
169
|
</details>
|
|
168
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
|
+
|
|
169
190
|
<details>
|
|
170
191
|
<summary><strong>Source Code Integration + Git Blame</strong></summary>
|
|
171
192
|
|
|
@@ -479,9 +500,9 @@ Special thanks to [@bonniesimon](https://github.com/bonniesimon), [@gundestrup](
|
|
|
479
500
|
|
|
480
501
|
## Support
|
|
481
502
|
|
|
482
|
-
If this gem saves you some headaches (or some money on error tracking SaaS), consider
|
|
503
|
+
If this gem saves you some headaches (or some money on error tracking SaaS), consider sponsoring the project. It keeps RED going and lets me know people are finding it useful.
|
|
483
504
|
|
|
484
|
-
<a href="https://www.buymeacoffee.com/anjanj" target="_blank"><img src="https://
|
|
505
|
+
<a href="https://github.com/sponsors/AnjanJ" target="_blank"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-ea4aaa?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Sponsor on GitHub"></a> <a href="https://www.buymeacoffee.com/anjanj" target="_blank"><img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black" alt="Buy Me A Coffee"></a>
|
|
485
506
|
|
|
486
507
|
---
|
|
487
508
|
|
|
@@ -81,6 +81,10 @@ module RailsErrorDashboard
|
|
|
81
81
|
@related_errors = @error.related_errors(limit: 5, application_id: @current_application_id)
|
|
82
82
|
@error_markdown = Services::MarkdownErrorFormatter.call(@error, related_errors: @related_errors)
|
|
83
83
|
|
|
84
|
+
# Fetch platform issue state and comments if linked
|
|
85
|
+
@platform_issue = fetch_platform_issue(@error)
|
|
86
|
+
@platform_comments = fetch_platform_comments(@error)
|
|
87
|
+
|
|
84
88
|
# Dispatch plugin event for error viewed
|
|
85
89
|
RailsErrorDashboard::PluginRegistry.dispatch(:on_error_viewed, @error)
|
|
86
90
|
end
|
|
@@ -144,11 +148,12 @@ module RailsErrorDashboard
|
|
|
144
148
|
result = Commands::CreateIssue.call(params[:id], dashboard_url: dashboard_url)
|
|
145
149
|
|
|
146
150
|
if result[:success]
|
|
147
|
-
flash[:notice] = "Issue created
|
|
151
|
+
flash[:notice] = "Issue created successfully"
|
|
152
|
+
flash[:new_issue_url] = result[:issue_url]
|
|
148
153
|
else
|
|
149
154
|
flash[:alert] = "Failed to create issue: #{result[:error]}"
|
|
150
155
|
end
|
|
151
|
-
redirect_to error_path(params[:id])
|
|
156
|
+
redirect_to error_path(params[:id], anchor: "issue-tracking")
|
|
152
157
|
end
|
|
153
158
|
|
|
154
159
|
def link_issue
|
|
@@ -159,21 +164,7 @@ module RailsErrorDashboard
|
|
|
159
164
|
else
|
|
160
165
|
flash[:alert] = "Failed to link issue: #{result[:error]}"
|
|
161
166
|
end
|
|
162
|
-
redirect_to error_path(params[:id])
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def unlink_issue
|
|
166
|
-
error = ErrorLog.find(params[:id])
|
|
167
|
-
error.update!(
|
|
168
|
-
external_issue_url: nil,
|
|
169
|
-
external_issue_number: nil,
|
|
170
|
-
external_issue_provider: nil
|
|
171
|
-
)
|
|
172
|
-
flash[:notice] = "Issue unlinked"
|
|
173
|
-
redirect_to error_path(error)
|
|
174
|
-
rescue => e
|
|
175
|
-
flash[:alert] = "Failed to unlink issue: #{e.message}"
|
|
176
|
-
redirect_to error_path(params[:id])
|
|
167
|
+
redirect_to error_path(params[:id], anchor: "issue-tracking")
|
|
177
168
|
end
|
|
178
169
|
|
|
179
170
|
def analytics
|
|
@@ -285,6 +276,16 @@ module RailsErrorDashboard
|
|
|
285
276
|
@platform_specific_errors = correlation.platform_specific_errors
|
|
286
277
|
end
|
|
287
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
|
+
|
|
288
289
|
def deprecations
|
|
289
290
|
unless RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
290
291
|
flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
@@ -563,6 +564,39 @@ module RailsErrorDashboard
|
|
|
563
564
|
@applications = Application.ordered_by_name.pluck(:name, :id)
|
|
564
565
|
end
|
|
565
566
|
|
|
567
|
+
def fetch_platform_issue(error)
|
|
568
|
+
return nil unless error.external_issue_url.present? && error.external_issue_number.present?
|
|
569
|
+
return nil unless RailsErrorDashboard.configuration.enable_issue_tracking
|
|
570
|
+
|
|
571
|
+
cache_key = "red/issue_state/#{error.external_issue_provider}/#{error.external_issue_number}"
|
|
572
|
+
Rails.cache.fetch(cache_key, expires_in: 60.seconds) do
|
|
573
|
+
client = Services::IssueTrackerClient.from_config
|
|
574
|
+
return nil unless client
|
|
575
|
+
|
|
576
|
+
result = client.fetch_issue(number: error.external_issue_number)
|
|
577
|
+
result[:success] ? result : nil
|
|
578
|
+
end
|
|
579
|
+
rescue => e
|
|
580
|
+
nil
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def fetch_platform_comments(error)
|
|
584
|
+
return [] unless error.external_issue_url.present? && error.external_issue_number.present?
|
|
585
|
+
return [] unless RailsErrorDashboard.configuration.enable_issue_tracking
|
|
586
|
+
|
|
587
|
+
# Cache for 60 seconds to avoid API hammering on page refreshes
|
|
588
|
+
cache_key = "red/issue_comments/#{error.external_issue_provider}/#{error.external_issue_number}"
|
|
589
|
+
Rails.cache.fetch(cache_key, expires_in: 60.seconds) do
|
|
590
|
+
client = Services::IssueTrackerClient.from_config
|
|
591
|
+
return [] unless client
|
|
592
|
+
|
|
593
|
+
result = client.fetch_comments(number: error.external_issue_number, per_page: 20)
|
|
594
|
+
result[:success] ? result[:comments] : []
|
|
595
|
+
end
|
|
596
|
+
rescue => e
|
|
597
|
+
[]
|
|
598
|
+
end
|
|
599
|
+
|
|
566
600
|
def check_default_credentials
|
|
567
601
|
@default_credentials_warning = RailsErrorDashboard.configuration.default_credentials?
|
|
568
602
|
end
|
|
@@ -43,7 +43,8 @@ module RailsErrorDashboard
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
45
|
def verify_webhook_enabled
|
|
46
|
-
|
|
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
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
</h5>
|
|
197
197
|
<small class="text-muted">Activity trail leading up to this error</small>
|
|
198
198
|
</div>
|
|
199
|
-
<div class="card-body p-0">
|
|
199
|
+
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
|
200
200
|
<div class="table-responsive">
|
|
201
201
|
<table class="table table-sm table-hover mb-0">
|
|
202
202
|
<thead class="table-light">
|
|
@@ -1,66 +1,99 @@
|
|
|
1
1
|
<% has_linked_issue = error.respond_to?(:external_issue_url) && error.external_issue_url.present? %>
|
|
2
|
-
<%
|
|
2
|
+
<% has_audit_comments = error.respond_to?(:comments) && error.comments.any? %>
|
|
3
|
+
<% platform_comments = @platform_comments || [] %>
|
|
3
4
|
<% issue_tracking_enabled = RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
4
5
|
|
|
5
|
-
<% if has_linked_issue
|
|
6
|
+
<% if has_linked_issue && (platform_comments.any? || has_audit_comments) %>
|
|
6
7
|
<div class="card mb-4" id="section-discussion">
|
|
7
|
-
<div class="card-header bg-white">
|
|
8
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
8
9
|
<h5 class="mb-0">
|
|
9
10
|
<i class="bi bi-chat-dots"></i> Discussion
|
|
10
|
-
<% if
|
|
11
|
-
<span class="badge bg-secondary ms-2"><%=
|
|
11
|
+
<% if platform_comments.any? %>
|
|
12
|
+
<span class="badge bg-secondary ms-2"><%= platform_comments.size %></span>
|
|
12
13
|
<% end %>
|
|
13
14
|
</h5>
|
|
15
|
+
<a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
|
|
16
|
+
<i class="bi bi-box-arrow-up-right me-1"></i> Reply on <%= error.external_issue_provider&.capitalize || "Platform" %>
|
|
17
|
+
</a>
|
|
14
18
|
</div>
|
|
15
|
-
<div class="card-body">
|
|
16
|
-
<% if
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Link or create an issue above to start a discussion on your issue tracker.
|
|
37
|
-
</p>
|
|
19
|
+
<div class="card-body p-0">
|
|
20
|
+
<% if platform_comments.any? %>
|
|
21
|
+
<div style="max-height: 400px; overflow-y: auto;">
|
|
22
|
+
<% platform_comments.each_with_index do |comment, index| %>
|
|
23
|
+
<div class="px-3 py-3 <%= index < platform_comments.size - 1 ? 'border-bottom' : '' %>">
|
|
24
|
+
<div class="d-flex align-items-start gap-2 mb-2">
|
|
25
|
+
<% if comment[:avatar_url].present? %>
|
|
26
|
+
<img src="<%= comment[:avatar_url] %>" alt="<%= comment[:author] %>" width="24" height="24" class="rounded-circle" style="flex-shrink: 0;">
|
|
27
|
+
<% else %>
|
|
28
|
+
<i class="bi bi-person-circle" style="font-size: 1.3em; flex-shrink: 0;"></i>
|
|
29
|
+
<% end %>
|
|
30
|
+
<div>
|
|
31
|
+
<strong class="text-primary"><%= comment[:author] || "Unknown" %></strong>
|
|
32
|
+
<small class="text-muted ms-2"><%= comment[:created_at] ? Time.parse(comment[:created_at]).strftime("%b %d, %Y %H:%M") : "" %></small>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="ms-4 text-break" style="font-size: 0.9em;">
|
|
36
|
+
<%= simple_format(h(comment[:body].to_s.truncate(500)), {}, wrapper_tag: "span") %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
38
40
|
</div>
|
|
39
41
|
<% end %>
|
|
40
42
|
|
|
41
|
-
<% if
|
|
42
|
-
<!-- Audit trail —
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
<% end %>
|
|
43
|
+
<% if has_audit_comments %>
|
|
44
|
+
<!-- Audit trail — workflow actions -->
|
|
45
|
+
<div class="px-3 py-2 bg-light border-top">
|
|
46
|
+
<small class="text-muted fw-bold"><i class="bi bi-clock-history"></i> Activity Log</small>
|
|
47
|
+
</div>
|
|
47
48
|
<% error.comments.recent_first.each_with_index do |comment, index| %>
|
|
48
|
-
<div class="<%= index < error.comments.count - 1 ? 'border-bottom' : '' %>
|
|
49
|
-
<div class="d-flex justify-content-between align-items-start
|
|
49
|
+
<div class="px-3 py-2 <%= index < error.comments.count - 1 ? 'border-bottom' : '' %>">
|
|
50
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
50
51
|
<div>
|
|
51
|
-
<
|
|
52
|
-
<i class="bi bi-person-circle"></i> <%= comment.author_name %>
|
|
53
|
-
</strong>
|
|
52
|
+
<small class="text-primary fw-bold"><i class="bi bi-person-circle"></i> <%= comment.author_name %></small>
|
|
54
53
|
<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
54
|
</div>
|
|
59
55
|
</div>
|
|
60
|
-
<
|
|
56
|
+
<small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
|
|
61
57
|
</div>
|
|
62
58
|
<% end %>
|
|
63
59
|
<% end %>
|
|
64
60
|
</div>
|
|
65
61
|
</div>
|
|
62
|
+
<% elsif has_linked_issue %>
|
|
63
|
+
<!-- Issue linked but no comments yet -->
|
|
64
|
+
<div class="card mb-4" id="section-discussion">
|
|
65
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
66
|
+
<h5 class="mb-0">
|
|
67
|
+
<i class="bi bi-chat-dots"></i> Discussion
|
|
68
|
+
</h5>
|
|
69
|
+
<a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
|
|
70
|
+
<i class="bi bi-box-arrow-up-right me-1"></i> Start discussion on <%= error.external_issue_provider&.capitalize || "Platform" %>
|
|
71
|
+
</a>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="card-body text-center py-3">
|
|
74
|
+
<p class="text-muted small mb-0">No comments yet. Be the first to discuss this error on <%= error.external_issue_provider&.capitalize || "the platform" %>.</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<% elsif has_audit_comments %>
|
|
78
|
+
<!-- No issue linked, but has audit trail -->
|
|
79
|
+
<div class="card mb-4" id="section-discussion">
|
|
80
|
+
<div class="card-header bg-white">
|
|
81
|
+
<h5 class="mb-0">
|
|
82
|
+
<i class="bi bi-clock-history"></i> Activity Log
|
|
83
|
+
</h5>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="card-body p-0">
|
|
86
|
+
<% error.comments.recent_first.each_with_index do |comment, index| %>
|
|
87
|
+
<div class="px-3 py-2 <%= index < error.comments.count - 1 ? 'border-bottom' : '' %>">
|
|
88
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
89
|
+
<div>
|
|
90
|
+
<small class="text-primary fw-bold"><i class="bi bi-person-circle"></i> <%= comment.author_name %></small>
|
|
91
|
+
<small class="text-muted ms-2"><%= comment.formatted_time %></small>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<small class="text-muted text-break"><%= simple_format(h(comment.body), {}, wrapper_tag: "span") %></small>
|
|
95
|
+
</div>
|
|
96
|
+
<% end %>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
66
99
|
<% end %>
|
|
@@ -1,38 +1,84 @@
|
|
|
1
1
|
<% if RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
2
2
|
<div class="card mb-4" id="issue-tracking">
|
|
3
|
-
<div class="card-header bg-white">
|
|
3
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
4
4
|
<h5 class="mb-0">
|
|
5
5
|
<i class="bi bi-link-45deg me-2"></i>
|
|
6
6
|
Issue Tracker
|
|
7
7
|
</h5>
|
|
8
|
+
<% if @error.external_issue_url.present? %>
|
|
9
|
+
<a href="<%= @error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
|
|
10
|
+
<i class="bi bi-box-arrow-up-right me-1"></i> View Issue
|
|
11
|
+
</a>
|
|
12
|
+
<% end %>
|
|
8
13
|
</div>
|
|
9
14
|
<div class="card-body">
|
|
10
15
|
<% if @error.external_issue_url.present? %>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
<% provider = @error.external_issue_provider %>
|
|
17
|
+
<% icon = case provider
|
|
18
|
+
when "github" then "bi-github"
|
|
19
|
+
when "gitlab" then "bi-gitlab"
|
|
20
|
+
when "codeberg" then "bi-git"
|
|
21
|
+
else "bi-link-45deg"
|
|
22
|
+
end %>
|
|
23
|
+
<% issue = @platform_issue %>
|
|
24
|
+
|
|
25
|
+
<!-- Issue badge + status -->
|
|
26
|
+
<div class="d-flex align-items-center gap-2 mb-3">
|
|
27
|
+
<span class="badge bg-success">
|
|
28
|
+
<i class="bi <%= icon %> me-1"></i>
|
|
29
|
+
<%= provider&.capitalize || "Linked" %> #<%= @error.external_issue_number || "?" %>
|
|
30
|
+
</span>
|
|
31
|
+
<% if issue %>
|
|
32
|
+
<% state_color = issue[:state] == "open" ? "success" : "secondary" %>
|
|
33
|
+
<% state_icon = issue[:state] == "open" ? "bi-circle-fill" : "bi-check-circle-fill" %>
|
|
34
|
+
<span class="badge bg-<%= state_color %>">
|
|
35
|
+
<i class="bi <%= state_icon %> me-1" style="font-size: 0.6em;"></i>
|
|
36
|
+
<%= issue[:state]&.capitalize %>
|
|
24
37
|
</span>
|
|
25
|
-
|
|
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>
|
|
38
|
+
<% end %>
|
|
35
39
|
</div>
|
|
40
|
+
|
|
41
|
+
<% if issue %>
|
|
42
|
+
<div class="row">
|
|
43
|
+
<!-- Assignees -->
|
|
44
|
+
<div class="col-md-6 mb-2">
|
|
45
|
+
<small class="text-muted d-block mb-1">Assignees</small>
|
|
46
|
+
<% if issue[:assignees].present? && issue[:assignees].any? %>
|
|
47
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
48
|
+
<% issue[:assignees].each do |assignee| %>
|
|
49
|
+
<span class="d-inline-flex align-items-center gap-1 border rounded-pill px-2 py-1" style="font-size: 0.85em;">
|
|
50
|
+
<% if assignee[:avatar_url].present? %>
|
|
51
|
+
<img src="<%= assignee[:avatar_url] %>" width="18" height="18" class="rounded-circle">
|
|
52
|
+
<% end %>
|
|
53
|
+
<%= assignee[:login] %>
|
|
54
|
+
</span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<% else %>
|
|
58
|
+
<span class="text-muted" style="font-size: 0.85em;">Unassigned</span>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Labels -->
|
|
63
|
+
<div class="col-md-6 mb-2">
|
|
64
|
+
<small class="text-muted d-block mb-1">Labels</small>
|
|
65
|
+
<% if issue[:labels].present? && issue[:labels].any? %>
|
|
66
|
+
<div class="d-flex align-items-center gap-1 flex-wrap">
|
|
67
|
+
<% issue[:labels].each do |label| %>
|
|
68
|
+
<% bg_color = label[:color].present? ? "##{label[:color]}" : "#6c757d" %>
|
|
69
|
+
<% text_color = label[:color].present? && label[:color].scan(/../).map { |c| c.to_i(16) }.sum > 382 ? "#000" : "#fff" %>
|
|
70
|
+
<span class="badge rounded-pill" style="background-color: <%= bg_color %>; color: <%= text_color %>; font-size: 0.78em;">
|
|
71
|
+
<%= label[:name] %>
|
|
72
|
+
</span>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
<% else %>
|
|
76
|
+
<span class="text-muted" style="font-size: 0.85em;">No labels</span>
|
|
77
|
+
<% end %>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
81
|
+
|
|
36
82
|
<% else %>
|
|
37
83
|
<!-- No linked issue — show create/link options -->
|
|
38
84
|
<div class="row">
|
|
@@ -64,4 +110,12 @@
|
|
|
64
110
|
<% end %>
|
|
65
111
|
</div>
|
|
66
112
|
</div>
|
|
113
|
+
|
|
114
|
+
<% if flash[:new_issue_url].present? %>
|
|
115
|
+
<script>
|
|
116
|
+
(function() {
|
|
117
|
+
window.open('<%= j flash[:new_issue_url] %>', '_blank');
|
|
118
|
+
})();
|
|
119
|
+
</script>
|
|
120
|
+
<% end %>
|
|
67
121
|
<% end %>
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
{ id: 'section-similar-errors', icon: 'bi-diagram-3', label: 'Similar' },
|
|
67
67
|
{ id: 'section-co-occurring', icon: 'bi-intersect', label: 'Co-occurring' },
|
|
68
68
|
{ id: 'section-timeline', icon: 'bi-clock-history', label: 'Timeline' },
|
|
69
|
+
{ id: 'issue-tracking', icon: 'bi-link-45deg', label: 'Issue' },
|
|
69
70
|
{ id: 'section-discussion', icon: 'bi-chat-dots', label: 'Discussion' },
|
|
70
71
|
{ id: 'section-error-cascades', icon: 'bi-share', label: 'Cascades' },
|
|
71
72
|
{ id: 'section-occurrence-patterns', icon: 'bi-graph-up', label: 'Patterns' }
|