rails_error_dashboard 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -3
- data/app/controllers/rails_error_dashboard/errors_controller.rb +29 -2
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +10 -9
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +7 -14
- data/app/helpers/rails_error_dashboard/overview_helper.rb +4 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +5 -0
- data/app/models/rails_error_dashboard/error_log.rb +11 -8
- data/app/models/rails_error_dashboard/error_occurrence.rb +1 -0
- data/app/models/rails_error_dashboard/error_occurrence_context.rb +14 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +3 -1
- data/app/views/rails_error_dashboard/errors/_captured_occurrences.html.erb +45 -0
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +10 -3
- data/app/views/rails_error_dashboard/errors/_occurrence_banner.html.erb +12 -0
- data/app/views/rails_error_dashboard/errors/overview.html.erb +10 -7
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +16 -11
- data/app/views/rails_error_dashboard/errors/settings.html.erb +6 -5
- data/app/views/rails_error_dashboard/errors/show.html.erb +7 -4
- data/config/locales/de.yml +19 -0
- data/config/locales/en.yml +21 -0
- data/config/locales/es.yml +20 -0
- data/config/locales/fr.yml +20 -0
- data/config/locales/it.yml +20 -0
- data/config/locales/ja.yml +18 -0
- data/config/locales/pl.yml +21 -0
- data/config/locales/pt-BR.yml +20 -0
- data/config/locales/ru.yml +21 -0
- data/config/locales/uk.yml +21 -0
- data/config/locales/zh-CN.yml +18 -0
- data/db/migrate/20260106094256_backfill_application_for_existing_errors.rb +3 -3
- data/db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb +35 -0
- data/db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb +46 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +14 -5
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +11 -2
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +7 -2
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +7 -2
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +6 -0
- data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +2 -4
- data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +2 -4
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +7 -6
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +2 -4
- data/lib/rails_error_dashboard/commands/log_error.rb +19 -7
- data/lib/rails_error_dashboard/commands/record_occurrence_context.rb +60 -0
- data/lib/rails_error_dashboard/configuration.rb +19 -3
- data/lib/rails_error_dashboard/error_reporter.rb +4 -2
- data/lib/rails_error_dashboard/manual_error_reporter.rb +11 -1
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +18 -4
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +0 -12
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +59 -33
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +14 -6
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +16 -7
- data/lib/rails_error_dashboard/services/crash_capture.rb +5 -0
- data/lib/rails_error_dashboard/services/github_link_generator.rb +1 -6
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +15 -16
- data/lib/rails_error_dashboard/services/repository_url.rb +18 -0
- data/lib/rails_error_dashboard/services/similarity_calculator.rb +3 -9
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +6 -3
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +6 -1
- data/lib/rails_error_dashboard/value_objects/occurrence_snapshot.rb +23 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +16 -0
- data/lib/tasks/error_dashboard.rake +2 -3
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84ad49626b8dddc0665ed515f3fc8e3357adadddcb5991679324cffa40d2aa2e
|
|
4
|
+
data.tar.gz: af06a63fb80ded2960e23f6e6c25ace1a741b85e13f014ae4bfbfd4b4b16980a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3154ebfa3b1d46d497a86cfca33dca5a09ed98127a699e7a9eb17ce3d05380f807172b377a2a824fbffff806e6f1e9a6c6ba96e8cb61802368e81ffc56ea93a
|
|
7
|
+
data.tar.gz: 571ca72a5e588371357a427c02a2ebed3bf565cf70f03b8c8e5877245e060bd7d8ddff873ae50235ac1c4103c6944d4f5e852c221766f7292225b4af8406ceca
|
data/README.md
CHANGED
|
@@ -394,7 +394,7 @@ config.git_repository_url = "https://github.com/user/repo" # enables SHA links
|
|
|
394
394
|
<details>
|
|
395
395
|
<summary><strong>Source Code Integration + Git Blame</strong></summary>
|
|
396
396
|
|
|
397
|
-
View actual source code directly in error backtraces
|
|
397
|
+
View actual source code directly in error backtraces, 5 lines before and after the error line by default. Git blame shows who last modified the code, when, and the commit message. Repository links jump to the exact line on GitHub, GitLab, Bitbucket or Codeberg.
|
|
398
398
|
|
|
399
399
|
```ruby
|
|
400
400
|
config.enable_source_code_integration = true
|
|
@@ -577,14 +577,18 @@ config.enable_crash_capture = true
|
|
|
577
577
|
<details>
|
|
578
578
|
<summary><strong>Plugin System</strong></summary>
|
|
579
579
|
|
|
580
|
-
Event-driven extensibility with hooks
|
|
580
|
+
Event-driven extensibility with hooks such as `on_error_logged`, `on_error_reopened` and `on_error_resolved` (eleven in all). Built-in examples for Jira integration, metrics tracking, and audit logging.
|
|
581
581
|
|
|
582
582
|
```ruby
|
|
583
583
|
class MyPlugin < RailsErrorDashboard::Plugin
|
|
584
|
+
def name = "My Plugin"
|
|
585
|
+
|
|
584
586
|
def on_error_logged(error_log)
|
|
585
587
|
# Your custom logic
|
|
586
588
|
end
|
|
587
589
|
end
|
|
590
|
+
|
|
591
|
+
RailsErrorDashboard.register_plugin(MyPlugin.new)
|
|
588
592
|
```
|
|
589
593
|
|
|
590
594
|
[Plugin System guide →](docs/PLUGIN_SYSTEM.md)
|
|
@@ -786,7 +790,7 @@ Rails 7.0–8.1 and Ruby 3.2–4.0.
|
|
|
786
790
|
### Advanced
|
|
787
791
|
- **[Multi-App Support](docs/MULTI_APP_PERFORMANCE.md)** — Track multiple applications
|
|
788
792
|
- **[Plugin System](docs/PLUGIN_SYSTEM.md)** — Build custom integrations
|
|
789
|
-
- **[API Reference](docs/API_REFERENCE.md)** —
|
|
793
|
+
- **[API Reference](docs/API_REFERENCE.md)** — The dashboard's HTTP routes and the Ruby API
|
|
790
794
|
- **[Customization](docs/CUSTOMIZATION.md)** — Customize everything
|
|
791
795
|
- **[Database Options](docs/guides/DATABASE_OPTIONS.md)** — Separate database setup
|
|
792
796
|
- **[Database Optimization](docs/guides/DATABASE_OPTIMIZATION.md)** — Performance tuning
|
|
@@ -119,6 +119,11 @@ module RailsErrorDashboard
|
|
|
119
119
|
@related_errors = @error.related_errors(limit: 5, application_id: @current_application_id)
|
|
120
120
|
@error_markdown = Services::MarkdownErrorFormatter.call(@error, related_errors: @related_errors)
|
|
121
121
|
|
|
122
|
+
# ?occurrence=ID: the context cards show that occurrence's stored context
|
|
123
|
+
@occurrence_context = find_occurrence_context(@error)
|
|
124
|
+
@snapshot = @occurrence_context ? ValueObjects::OccurrenceSnapshot.build(@error, @occurrence_context) : @error
|
|
125
|
+
@captured_occurrences = captured_occurrences(@error)
|
|
126
|
+
|
|
122
127
|
# Fetch platform issue state and comments if linked
|
|
123
128
|
@platform_issue = fetch_platform_issue(@error)
|
|
124
129
|
@platform_comments = fetch_platform_comments(@error)
|
|
@@ -279,8 +284,6 @@ module RailsErrorDashboard
|
|
|
279
284
|
@errors_by_hour = analytics[:errors_by_hour]
|
|
280
285
|
@top_users = analytics[:top_users]
|
|
281
286
|
@resolution_rate = analytics[:resolution_rate]
|
|
282
|
-
@mobile_errors = analytics[:mobile_errors]
|
|
283
|
-
@api_errors = analytics[:api_errors]
|
|
284
287
|
|
|
285
288
|
# Get recurring issues data (pass application filter)
|
|
286
289
|
recurring = Queries::RecurringIssues.call(days, application_id: @current_application_id)
|
|
@@ -901,6 +904,30 @@ module RailsErrorDashboard
|
|
|
901
904
|
red_t("red.errors.row.status.#{status}", default: status.to_s.tr("_", " "))
|
|
902
905
|
end
|
|
903
906
|
|
|
907
|
+
# Looked up through the error, so another error's occurrence id finds
|
|
908
|
+
# nothing. nil (the page shows the latest snapshot) when there is no id, it
|
|
909
|
+
# is foreign or unknown, or its context is no longer kept.
|
|
910
|
+
def find_occurrence_context(error)
|
|
911
|
+
return if params[:occurrence].blank?
|
|
912
|
+
# An app that updated the gem but has not migrated yet
|
|
913
|
+
return unless ErrorOccurrenceContext.table_exists?
|
|
914
|
+
|
|
915
|
+
error.occurrence_contexts.find_by(error_occurrence_id: params[:occurrence])
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# This error's occurrences that still have a stored context, newest first.
|
|
919
|
+
# The context table is read for its occurrence ids only, so listing them
|
|
920
|
+
# never loads the stored payloads.
|
|
921
|
+
def captured_occurrences(error)
|
|
922
|
+
return [] unless ErrorOccurrenceContext.table_exists?
|
|
923
|
+
|
|
924
|
+
error.error_occurrences
|
|
925
|
+
.where(id: ErrorOccurrenceContext.where(error_log_id: error.id).select(:error_occurrence_id))
|
|
926
|
+
.order(occurred_at: :desc, id: :desc)
|
|
927
|
+
.select(:id, :occurred_at, :app_version, :git_sha)
|
|
928
|
+
.to_a
|
|
929
|
+
end
|
|
930
|
+
|
|
904
931
|
def fetch_platform_issue(error)
|
|
905
932
|
return nil unless error.external_issue_url.present? && error.external_issue_number.present?
|
|
906
933
|
return nil unless RailsErrorDashboard.configuration.enable_issue_tracking
|
|
@@ -14,7 +14,12 @@ module RailsErrorDashboard
|
|
|
14
14
|
class WebhooksController < ActionController::Base
|
|
15
15
|
skip_before_action :verify_authenticity_token
|
|
16
16
|
|
|
17
|
+
PROVIDERS = %w[github gitlab codeberg linear].freeze
|
|
18
|
+
|
|
17
19
|
before_action :verify_webhook_enabled
|
|
20
|
+
# Before the signature check: an unknown provider is a wrong URL, and a 401
|
|
21
|
+
# would send the sender looking at a secret that is fine
|
|
22
|
+
before_action :verify_provider
|
|
18
23
|
before_action :verify_signature
|
|
19
24
|
|
|
20
25
|
def receive
|
|
@@ -32,9 +37,6 @@ module RailsErrorDashboard
|
|
|
32
37
|
handle_codeberg(payload)
|
|
33
38
|
when "linear"
|
|
34
39
|
handle_linear(payload)
|
|
35
|
-
else
|
|
36
|
-
head :not_found
|
|
37
|
-
return
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
head :ok
|
|
@@ -52,16 +54,15 @@ module RailsErrorDashboard
|
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
|
|
57
|
+
def verify_provider
|
|
58
|
+
head :not_found unless PROVIDERS.include?(params[:provider])
|
|
59
|
+
end
|
|
60
|
+
|
|
55
61
|
def verify_signature
|
|
56
62
|
provider = params[:provider]
|
|
63
|
+
# Present: verify_webhook_enabled answers 404 without it
|
|
57
64
|
secret = RailsErrorDashboard.configuration.issue_webhook_secret
|
|
58
65
|
|
|
59
|
-
unless secret.present?
|
|
60
|
-
Rails.logger.warn("[RailsErrorDashboard] Webhook received but no issue_webhook_secret configured")
|
|
61
|
-
head :unauthorized
|
|
62
|
-
return
|
|
63
|
-
end
|
|
64
|
-
|
|
65
66
|
body = request.body.read
|
|
66
67
|
request.body.rewind
|
|
67
68
|
|
|
@@ -292,15 +292,19 @@ module RailsErrorDashboard
|
|
|
292
292
|
|
|
293
293
|
# Determine which commit SHA to use based on strategy
|
|
294
294
|
def determine_commit_sha(error_log)
|
|
295
|
-
|
|
295
|
+
# A String (from ENV, say) means the same as the Symbol
|
|
296
|
+
strategy = (RailsErrorDashboard.configuration.git_branch_strategy || :commit_sha).to_s.to_sym
|
|
296
297
|
|
|
297
298
|
case strategy
|
|
298
299
|
when :commit_sha
|
|
299
300
|
# Use the SHA from when the error occurred (most accurate)
|
|
300
301
|
error_log.respond_to?(:git_sha) ? error_log.git_sha : nil
|
|
301
302
|
when :current_branch
|
|
302
|
-
#
|
|
303
|
-
|
|
303
|
+
# The commit this process is running: config.git_sha, the deploy ENV
|
|
304
|
+
# variables, then the .git HEAD read once at boot. Never `git rev-parse`
|
|
305
|
+
# here: this renders on a host request thread, and a subprocess with
|
|
306
|
+
# no timeout is the pattern the capture path already removed.
|
|
307
|
+
Commands::LogError.capture_git_sha
|
|
304
308
|
when :main
|
|
305
309
|
# Use main/master branch
|
|
306
310
|
nil # Will default to "main" in GithubLinkGenerator
|
|
@@ -308,16 +312,5 @@ module RailsErrorDashboard
|
|
|
308
312
|
nil
|
|
309
313
|
end
|
|
310
314
|
end
|
|
311
|
-
|
|
312
|
-
# Get current git SHA from repository
|
|
313
|
-
def get_current_git_sha
|
|
314
|
-
return @current_git_sha if defined?(@current_git_sha)
|
|
315
|
-
|
|
316
|
-
@current_git_sha = begin
|
|
317
|
-
`git rev-parse HEAD 2>/dev/null`.strip.presence
|
|
318
|
-
rescue StandardError
|
|
319
|
-
nil
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
315
|
end
|
|
323
316
|
end
|
|
@@ -67,6 +67,8 @@ module RailsErrorDashboard
|
|
|
67
67
|
"success"
|
|
68
68
|
when :warning
|
|
69
69
|
"warning"
|
|
70
|
+
when :not_rated
|
|
71
|
+
"secondary"
|
|
70
72
|
else
|
|
71
73
|
"danger"
|
|
72
74
|
end
|
|
@@ -82,6 +84,8 @@ module RailsErrorDashboard
|
|
|
82
84
|
red_t("red.common.health_status.healthy")
|
|
83
85
|
when :warning
|
|
84
86
|
red_t("red.common.health_status.warning")
|
|
87
|
+
when :not_rated
|
|
88
|
+
red_t("red.common.health_status.not_rated")
|
|
85
89
|
else
|
|
86
90
|
red_t("red.common.health_status.critical")
|
|
87
91
|
end
|
|
@@ -44,6 +44,11 @@ module RailsErrorDashboard
|
|
|
44
44
|
def self.delete_with_dependents(scope)
|
|
45
45
|
ids = scope.select(:id)
|
|
46
46
|
|
|
47
|
+
# Each occurrence's stored context. Named here for the same reason as the
|
|
48
|
+
# event counts below: delete_all fires no :dependent callback.
|
|
49
|
+
if ErrorOccurrenceContext.table_exists?
|
|
50
|
+
ErrorOccurrenceContext.where(error_log_id: ids).in_batches(of: 1000).delete_all
|
|
51
|
+
end
|
|
47
52
|
ErrorOccurrence.where(error_log_id: ids).in_batches(of: 1000).delete_all
|
|
48
53
|
# Hour buckets for storm-shed events. Listed explicitly because delete_all
|
|
49
54
|
# does not fire the has_many :dependent callback on ErrorLog -- without
|
|
@@ -36,6 +36,8 @@ module RailsErrorDashboard
|
|
|
36
36
|
|
|
37
37
|
# Association for tracking individual error occurrences
|
|
38
38
|
has_many :error_occurrences, class_name: "RailsErrorDashboard::ErrorOccurrence", dependent: :destroy
|
|
39
|
+
# Deleted before the occurrences, without loading their payloads
|
|
40
|
+
has_many :occurrence_contexts, class_name: "RailsErrorDashboard::ErrorOccurrenceContext", dependent: :delete_all
|
|
39
41
|
|
|
40
42
|
# Hour buckets for storm-shed events. delete_all, not destroy: these rows
|
|
41
43
|
# are pure counters with no callbacks, and a group can own one per hour.
|
|
@@ -179,6 +181,14 @@ module RailsErrorDashboard
|
|
|
179
181
|
Services::SeverityClassifier.critical?(error_type)
|
|
180
182
|
end
|
|
181
183
|
|
|
184
|
+
# Whether the backtrace this row shows was sent by a client through
|
|
185
|
+
# ManualErrorReporter. Its frames describe code that ran on the client, so
|
|
186
|
+
# View Source, git blame and the Markdown formatter read no file for them.
|
|
187
|
+
# nil (a row from before the column, or an app not yet migrated) is server.
|
|
188
|
+
def client_backtrace?
|
|
189
|
+
has_attribute?(:backtrace_source) && backtrace_source == "client"
|
|
190
|
+
end
|
|
191
|
+
|
|
182
192
|
# Check if error is recent (< 1 hour)
|
|
183
193
|
def recent?
|
|
184
194
|
occurred_at >= 1.hour.ago
|
|
@@ -339,14 +349,7 @@ module RailsErrorDashboard
|
|
|
339
349
|
[]
|
|
340
350
|
end
|
|
341
351
|
|
|
342
|
-
lines.first(20).map
|
|
343
|
-
# Extract file path and method name, ignore line numbers
|
|
344
|
-
if line =~ %r{([^/]+\.rb):.*?in `(.+)'$}
|
|
345
|
-
"#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
|
|
346
|
-
elsif line =~ %r{([^/]+\.rb)}
|
|
347
|
-
Regexp.last_match(1)
|
|
348
|
-
end
|
|
349
|
-
end.compact.uniq
|
|
352
|
+
lines.first(20).map { |line| Services::BacktraceProcessor.frame_key(line) }.compact.uniq
|
|
350
353
|
end
|
|
351
354
|
|
|
352
355
|
# Calculate backtrace signature — delegates to Service
|
|
@@ -10,6 +10,7 @@ module RailsErrorDashboard
|
|
|
10
10
|
self.table_name = "rails_error_dashboard_error_occurrences"
|
|
11
11
|
|
|
12
12
|
belongs_to :error_log, class_name: "RailsErrorDashboard::ErrorLog"
|
|
13
|
+
has_one :context, class_name: "RailsErrorDashboard::ErrorOccurrenceContext", dependent: :delete
|
|
13
14
|
|
|
14
15
|
# Only define user association if User model exists
|
|
15
16
|
if defined?(::User)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# The moment-of-failure context of one occurrence: breadcrumbs, system
|
|
5
|
+
# health, local and instance variables, the same JSON strings the error row
|
|
6
|
+
# holds for its latest capture. Commands::RecordOccurrenceContext writes them
|
|
7
|
+
# and keeps the newest config.occurrence_context_limit per error.
|
|
8
|
+
class ErrorOccurrenceContext < ErrorLogsRecord
|
|
9
|
+
self.table_name = "rails_error_dashboard_error_occurrence_contexts"
|
|
10
|
+
|
|
11
|
+
belongs_to :error_occurrence, class_name: "RailsErrorDashboard::ErrorOccurrence"
|
|
12
|
+
belongs_to :error_log, class_name: "RailsErrorDashboard::ErrorLog"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -2514,7 +2514,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
2514
2514
|
case 'toggle-next':
|
|
2515
2515
|
var nextEl = actionEl.nextElementSibling;
|
|
2516
2516
|
if (nextEl) {
|
|
2517
|
-
|
|
2517
|
+
// The computed display: an open element usually has no inline display
|
|
2518
|
+
// at all, and reading '' as hidden made the first click a no-op.
|
|
2519
|
+
var isHidden = window.getComputedStyle(nextEl).display === 'none';
|
|
2518
2520
|
nextEl.style.display = isHidden ? (actionEl.dataset.redDisplay || 'block') : 'none';
|
|
2519
2521
|
var chev = actionEl.querySelector('.chev');
|
|
2520
2522
|
if (chev) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!-- Captured occurrences: the ones whose context is still stored -->
|
|
2
|
+
<% if occurrences.any? %>
|
|
3
|
+
<div class="card mb-4" id="section-captured-occurrences">
|
|
4
|
+
<div class="card-header">
|
|
5
|
+
<h5 class="mb-0">
|
|
6
|
+
<i class="bi bi-camera"></i> <%= red_t("red.errors.captured_occurrences.title") %>
|
|
7
|
+
</h5>
|
|
8
|
+
<small class="text-muted"><%= red_t("red.errors.captured_occurrences.hint") %></small>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="card-body p-0">
|
|
11
|
+
<div class="table-responsive">
|
|
12
|
+
<table class="table table-hover mb-0">
|
|
13
|
+
<thead class="table-light">
|
|
14
|
+
<tr>
|
|
15
|
+
<th><%= red_t("red.errors.captured_occurrences.column_occurrence") %></th>
|
|
16
|
+
<th><%= red_t("red.errors.captured_occurrences.column_occurred_at") %></th>
|
|
17
|
+
<th><%= red_t("red.errors.captured_occurrences.column_version") %></th>
|
|
18
|
+
<th></th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% occurrences.each do |occurrence| %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td>
|
|
25
|
+
<code>#<%= occurrence.id %></code>
|
|
26
|
+
<% if occurrence.id == current_id %>
|
|
27
|
+
<span class="badge bg-info text-dark ms-1"><%= red_t("red.errors.captured_occurrences.viewing") %></span>
|
|
28
|
+
<% end %>
|
|
29
|
+
</td>
|
|
30
|
+
<td><%= local_time(occurrence.occurred_at) %></td>
|
|
31
|
+
<td>
|
|
32
|
+
<% version = occurrence.app_version.presence || occurrence.git_sha.to_s.first(7).presence %>
|
|
33
|
+
<small class="text-muted"><%= version || red_t("red.common.not_available") %></small>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="text-end">
|
|
36
|
+
<%= link_to red_t("red.errors.captured_occurrences.view"), error_path(error, occurrence: occurrence.id, **app_context), class: "btn btn-sm btn-outline-primary" %>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
would be served to every other one. %>
|
|
4
4
|
<%# Plus the source-integration configuration and a TTL, and not at all
|
|
5
5
|
while coverage is live: see BacktraceHelper#source_view_cache_key. %>
|
|
6
|
-
<% cache_if source_view_cacheable?, [error, 'error_details_v3', red_locale, source_view_cache_key], **source_view_cache_options do %>
|
|
6
|
+
<% cache_if source_view_cacheable?, [error, 'error_details_v3', red_locale, source_view_cache_key, error.client_backtrace?], **source_view_cache_options do %>
|
|
7
7
|
<div class="card mb-4" id="section-error-info">
|
|
8
8
|
<div class="card-header bg-danger text-white">
|
|
9
9
|
<div class="d-flex justify-content-between align-items-center">
|
|
@@ -108,6 +108,8 @@
|
|
|
108
108
|
<% if error.backtrace.present? %>
|
|
109
109
|
<% frames = parse_backtrace(error.backtrace) %>
|
|
110
110
|
<% app_frames = filter_app_code(frames) %>
|
|
111
|
+
<%# A client's frames describe code that ran on the client: no file is read for them %>
|
|
112
|
+
<% source_viewable = RailsErrorDashboard.configuration.enable_source_code_integration && !error.client_backtrace? %>
|
|
111
113
|
<% framework_frames = filter_framework_code(frames) %>
|
|
112
114
|
|
|
113
115
|
<% if app_frames.any? %>
|
|
@@ -122,6 +124,11 @@
|
|
|
122
124
|
</div>
|
|
123
125
|
</div>
|
|
124
126
|
<div class="card-body p-0">
|
|
127
|
+
<% if RailsErrorDashboard.configuration.enable_source_code_integration && error.client_backtrace? %>
|
|
128
|
+
<div class="alert alert-info m-3 mb-0" id="client-backtrace-note" style="font-size: 12px;">
|
|
129
|
+
<i class="bi bi-phone"></i> <%= red_t("red.errors.info.backtrace.client_report_note") %>
|
|
130
|
+
</div>
|
|
131
|
+
<% end %>
|
|
125
132
|
<div class="code-block p-3" style="max-height: none; overflow-y: visible; overflow-x: auto;">
|
|
126
133
|
<% app_frames.each_with_index do |frame, index| %>
|
|
127
134
|
<div class="backtrace-frame-wrapper mb-2">
|
|
@@ -133,7 +140,7 @@
|
|
|
133
140
|
<%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %>
|
|
134
141
|
</span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span>
|
|
135
142
|
</div>
|
|
136
|
-
<% if
|
|
143
|
+
<% if source_viewable && frame[:category] == :app %>
|
|
137
144
|
<button class="btn btn-sm btn-outline-secondary" type="button"
|
|
138
145
|
data-bs-toggle="collapse"
|
|
139
146
|
data-bs-target="#source-code-<%= index %>"
|
|
@@ -145,7 +152,7 @@
|
|
|
145
152
|
</div>
|
|
146
153
|
|
|
147
154
|
<!-- Collapsible source code (only for app frames) -->
|
|
148
|
-
<% if
|
|
155
|
+
<% if source_viewable && frame[:category] == :app %>
|
|
149
156
|
<div class="collapse" id="source-code-<%= index %>">
|
|
150
157
|
<%= render "source_code", frame: frame, error: error, index: index %>
|
|
151
158
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if occurrence_context %>
|
|
2
|
+
<%# Only the context cards follow ?occurrence=; the rest of the page is the error row's latest capture %>
|
|
3
|
+
<div class="alert alert-info mb-3" id="occurrence-banner" style="display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4);">
|
|
4
|
+
<div style="font-size: 12px;">
|
|
5
|
+
<i class="bi bi-clock-history"></i>
|
|
6
|
+
<strong><%= red_t("red.errors.show.occurrence.title", id: occurrence_context.error_occurrence_id) %></strong>
|
|
7
|
+
· <%= local_time(occurrence_context.error_occurrence&.occurred_at || occurrence_context.created_at) %>
|
|
8
|
+
<div><%= red_t("red.errors.show.occurrence.body") %></div>
|
|
9
|
+
</div>
|
|
10
|
+
<%= link_to red_t("red.errors.show.occurrence.show_latest"), error_path(error, **app_context), class: "btn btn-sm" %>
|
|
11
|
+
</div>
|
|
12
|
+
<% end %>
|
|
@@ -201,14 +201,17 @@
|
|
|
201
201
|
morphology applied to an identifier — it renders "iOS" as
|
|
202
202
|
"IOS" in some engines and mangles the Turkish dotless i. %>
|
|
203
203
|
<span style="font-size: 13px; font-weight: 600;"><%= platform %></span>
|
|
204
|
-
<span class="badge bg-<%= health_status_color(health[:health_status]) %>"
|
|
205
|
-
</div>
|
|
206
|
-
<div style="margin-bottom: var(--space-2);">
|
|
207
|
-
<div style="font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;"><%= red_t("red.errors.overview_page.stability_score_html", score: content_tag(:span, health[:stability_score])) %></div>
|
|
208
|
-
</div>
|
|
209
|
-
<div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
|
|
210
|
-
<div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
|
|
204
|
+
<span class="badge bg-<%= health_status_color(health[:health_status]) %>"<% if health[:health_status] == :not_rated %> title="<%= red_t("red.common.health_status.not_rated_hint") %>"<% end %>><%= health_status_text(health[:health_status]) %></span>
|
|
211
205
|
</div>
|
|
206
|
+
<%# A lone platform has no score: a rating compares platforms %>
|
|
207
|
+
<% if health[:stability_score] %>
|
|
208
|
+
<div style="margin-bottom: var(--space-2);">
|
|
209
|
+
<div style="font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;"><%= red_t("red.errors.overview_page.stability_score_html", score: content_tag(:span, health[:stability_score])) %></div>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
|
|
212
|
+
<div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
|
|
213
|
+
</div>
|
|
214
|
+
<% end %>
|
|
212
215
|
<div style="font-size: 12px; color: var(--text-tertiary);">
|
|
213
216
|
<%= red_t("red.errors.overview_page.platform_summary", total: health[:total_errors], critical: health[:critical_errors], unresolved: health[:unresolved_errors]) %>
|
|
214
217
|
</div>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<div class="row mb-4">
|
|
37
37
|
<% @platform_health.each do |platform, health| %>
|
|
38
38
|
<div class="col-md-6 col-lg-3 mb-3">
|
|
39
|
-
<div class="card h-100 <%=
|
|
39
|
+
<div class="card h-100 <%= { healthy: 'border-success', warning: 'border-warning', not_rated: 'border-secondary' }.fetch(health[:health_status], 'border-danger') %>">
|
|
40
40
|
<div class="card-body">
|
|
41
41
|
<h5 class="card-title text-capitalize">
|
|
42
42
|
<%= platform || red_t("red.common.unknown") %>
|
|
@@ -44,23 +44,28 @@
|
|
|
44
44
|
<span class="badge bg-success float-end"><%= red_t("red.analytics.platform_comparison.health_card.healthy") %></span>
|
|
45
45
|
<% elsif health[:health_status] == :warning %>
|
|
46
46
|
<span class="badge bg-warning float-end"><%= red_t("red.analytics.platform_comparison.health_card.warning") %></span>
|
|
47
|
+
<% elsif health[:health_status] == :not_rated %>
|
|
48
|
+
<span class="badge bg-secondary float-end" title="<%= red_t("red.common.health_status.not_rated_hint") %>"><%= red_t("red.common.health_status.not_rated") %></span>
|
|
47
49
|
<% else %>
|
|
48
50
|
<span class="badge bg-danger float-end"><%= red_t("red.analytics.platform_comparison.health_card.critical") %></span>
|
|
49
51
|
<% end %>
|
|
50
52
|
</h5>
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
<%# A lone platform has no score: a rating compares platforms %>
|
|
55
|
+
<% if health[:stability_score] %>
|
|
56
|
+
<div class="mb-3">
|
|
57
|
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
58
|
+
<small class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.stability_score") %></small>
|
|
59
|
+
<strong><%= health[:stability_score] %>/100</strong>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="progress" style="height: 8px;">
|
|
62
|
+
<div class="progress-bar <%= health[:stability_score] >= 80 ? 'bg-success' : health[:stability_score] >= 60 ? 'bg-warning' : 'bg-danger' %>"
|
|
63
|
+
role="progressbar"
|
|
64
|
+
style="width: <%= health[:stability_score] %>%">
|
|
65
|
+
</div>
|
|
61
66
|
</div>
|
|
62
67
|
</div>
|
|
63
|
-
|
|
68
|
+
<% end %>
|
|
64
69
|
|
|
65
70
|
<div class="small">
|
|
66
71
|
<div class="d-flex justify-content-between mb-1">
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
enable_middleware: { type: :boolean },
|
|
11
11
|
enable_error_subscriber: { type: :boolean },
|
|
12
12
|
retention_days: { type: :retention_days },
|
|
13
|
+
occurrence_context_limit: { type: :integer, unit: :occurrences },
|
|
13
14
|
max_backtrace_lines: { type: :integer, unit: :lines },
|
|
14
15
|
sampling_rate: { type: :percentage }
|
|
15
16
|
}
|
|
@@ -195,20 +196,20 @@
|
|
|
195
196
|
|
|
196
197
|
<!-- Dynamically render setting groups -->
|
|
197
198
|
<% setting_groups.each do |group_slug, group_config| %>
|
|
199
|
+
<%# One flag, or every flag in an array, must be on for a row to show. The
|
|
200
|
+
count and the rows both use this, so the count says what is shown. %>
|
|
201
|
+
<% shown_settings = group_config[:settings].select { |_key, meta| Array(meta[:show_if]).all? { |flag| @config.send(flag) } } %>
|
|
198
202
|
<div class="card" style="margin-bottom: var(--space-4);">
|
|
199
203
|
<button type="button" data-red-action="toggle-next" data-red-display="block" style="display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--space-4) var(--space-6); background: none; border: none; cursor: pointer; border-bottom: 1px solid var(--border-primary);">
|
|
200
204
|
<span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="<%= group_config[:icon] %>" style="margin-right: 6px;"></i> <%= red_t("red.settings.groups.#{group_slug}") %></span>
|
|
201
205
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
202
|
-
<span style="font-size: 11px; color: var(--text-tertiary);"><%= red_tp("red.settings.option_count", count:
|
|
206
|
+
<span style="font-size: 11px; color: var(--text-tertiary);"><%= red_tp("red.settings.option_count", count: shown_settings.size) %></span>
|
|
203
207
|
<i class="bi bi-chevron-up chev" style="font-size: 12px; color: var(--text-tertiary);"></i>
|
|
204
208
|
</div>
|
|
205
209
|
</button>
|
|
206
210
|
<div>
|
|
207
|
-
<%
|
|
211
|
+
<% shown_settings.each do |setting_key, setting_meta| %>
|
|
208
212
|
<%
|
|
209
|
-
# One flag, or every flag in an array, must be on for the row to show.
|
|
210
|
-
should_show = Array(setting_meta[:show_if]).all? { |flag| @config.send(flag) }
|
|
211
|
-
next unless should_show
|
|
212
213
|
value = @config.send(setting_meta[:method] || setting_key)
|
|
213
214
|
# A credential is shown as Set / Not set whatever shape it has. The
|
|
214
215
|
# per-row :type documents the value's shape; this one rule is what
|
|
@@ -98,6 +98,8 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
100
100
|
|
|
101
|
+
<%= render "occurrence_banner", error: @error, occurrence_context: @occurrence_context %>
|
|
102
|
+
|
|
101
103
|
<!-- Main content: tabs + sidebar -->
|
|
102
104
|
<div class="red-detail-grid">
|
|
103
105
|
<!-- Left: tabbed content -->
|
|
@@ -142,18 +144,19 @@
|
|
|
142
144
|
<% end %>
|
|
143
145
|
|
|
144
146
|
<%= render "error_info", error: @error %>
|
|
145
|
-
<%= render "local_variables", error: @
|
|
146
|
-
<%= render "instance_variables", error: @
|
|
147
|
+
<%= render "local_variables", error: @snapshot %>
|
|
148
|
+
<%= render "instance_variables", error: @snapshot %>
|
|
147
149
|
</div>
|
|
148
150
|
|
|
149
151
|
<!-- Tab: Context -->
|
|
150
152
|
<div id="panel-context">
|
|
151
153
|
<%= render "request_context", error: @error %>
|
|
152
|
-
<%= render "breadcrumbs_group", error: @
|
|
154
|
+
<%= render "breadcrumbs_group", error: @snapshot %>
|
|
153
155
|
</div>
|
|
154
156
|
|
|
155
157
|
<!-- Tab: History -->
|
|
156
158
|
<div id="panel-history">
|
|
159
|
+
<%= render "captured_occurrences", error: @error, occurrences: @captured_occurrences, current_id: @occurrence_context&.error_occurrence_id %>
|
|
157
160
|
<%= render "timeline" %>
|
|
158
161
|
<% unless RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
159
162
|
<%= render "discussion", error: @error %>
|
|
@@ -175,7 +178,7 @@
|
|
|
175
178
|
|
|
176
179
|
<!-- Right: sidebar -->
|
|
177
180
|
<div class="error-sidebar" style="display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; overflow-wrap: break-word;">
|
|
178
|
-
<%= render "sidebar_metadata", error: @
|
|
181
|
+
<%= render "sidebar_metadata", error: @snapshot, related_errors: @related_errors %>
|
|
179
182
|
|
|
180
183
|
<%= render "llm_summary", error: @error %>
|
|
181
184
|
|
data/config/locales/de.yml
CHANGED
|
@@ -36,6 +36,8 @@ de:
|
|
|
36
36
|
healthy: "✅ Fehlerfrei"
|
|
37
37
|
warning: "⚠️ Warnung"
|
|
38
38
|
critical: "\U0001F534 Kritisch"
|
|
39
|
+
not_rated: "Nicht bewertet"
|
|
40
|
+
not_rated_hint: "Eine Bewertung vergleicht Plattformen und braucht daher Fehler auf mindestens zwei."
|
|
39
41
|
device_type:
|
|
40
42
|
mobile: Mobil
|
|
41
43
|
tablet: Tablet
|
|
@@ -242,6 +244,10 @@ de:
|
|
|
242
244
|
inactive_body: aktivieren, um ausgeführte Zeilen zu sehen
|
|
243
245
|
enable: Aktivieren
|
|
244
246
|
disable: Deaktivieren
|
|
247
|
+
occurrence:
|
|
248
|
+
title: "Vorkommen #%{id}"
|
|
249
|
+
body: "Breadcrumbs, Systemzustand und Variablen stammen aus diesem Vorkommen. Alles andere zeigt die neueste Erfassung."
|
|
250
|
+
show_latest: "Neueste anzeigen"
|
|
245
251
|
baselines:
|
|
246
252
|
title: Referenzstatistiken
|
|
247
253
|
anomaly_label: 'Anomalie:'
|
|
@@ -497,6 +503,14 @@ de:
|
|
|
497
503
|
one: "+%{count} weiterer"
|
|
498
504
|
other: "+%{count} weitere"
|
|
499
505
|
view_errors: Fehler ansehen
|
|
506
|
+
captured_occurrences:
|
|
507
|
+
title: "Erfasste Vorkommen"
|
|
508
|
+
hint: "Breadcrumbs, Systemzustand und Variablen werden für die neuesten Vorkommen aufbewahrt (occurrence_context_limit). Öffnen Sie eines, um sie zu sehen."
|
|
509
|
+
column_occurrence: "Vorkommen"
|
|
510
|
+
column_occurred_at: "Aufgetreten"
|
|
511
|
+
column_version: "Version"
|
|
512
|
+
view: "Anzeigen"
|
|
513
|
+
viewing: "Angezeigt"
|
|
500
514
|
co_occurring:
|
|
501
515
|
title: Gemeinsam auftretende Fehler
|
|
502
516
|
badge: Zeitliche Muster
|
|
@@ -741,6 +755,7 @@ de:
|
|
|
741
755
|
other: "%{count} Frames"
|
|
742
756
|
click_to_expand: "(zum Aufklappen klicken)"
|
|
743
757
|
view_source: Quellcode ansehen
|
|
758
|
+
client_report_note: "Diese Frames stammen aus einem Client-Bericht (ManualErrorReporter). Sie beschreiben Code, der auf dem Client lief, daher öffnet RED für sie keine Quelldatei, zeigt keinen Ausschnitt und führt kein git blame aus."
|
|
744
759
|
none: Kein Backtrace verfügbar
|
|
745
760
|
row:
|
|
746
761
|
new_badge: NEU
|
|
@@ -1101,6 +1116,9 @@ de:
|
|
|
1101
1116
|
events:
|
|
1102
1117
|
one: "%{count} Ereignis"
|
|
1103
1118
|
other: "%{count} Ereignisse"
|
|
1119
|
+
occurrences:
|
|
1120
|
+
one: "%{count} Vorkommen"
|
|
1121
|
+
other: "%{count} Vorkommen"
|
|
1104
1122
|
queries:
|
|
1105
1123
|
one: "%{count} Abfrage"
|
|
1106
1124
|
other: "%{count} Abfragen"
|
|
@@ -1145,6 +1163,7 @@ de:
|
|
|
1145
1163
|
enable_middleware: Fängt unbehandelte Exceptions ab
|
|
1146
1164
|
enable_error_subscriber: Abonniert Rails-Fehlerereignisse
|
|
1147
1165
|
retention_days: "Fehler löschen, die so viele Tage nicht mehr aufgetreten sind. Durchgesetzt vom RetentionCleanupJob, den das Gem selbst nicht einplant"
|
|
1166
|
+
occurrence_context_limit: "Für wie viele der neuesten Vorkommen je Fehler Breadcrumbs, Systemzustand und Variablen aufbewahrt werden (0 = aus)"
|
|
1148
1167
|
max_backtrace_lines: Maximale Backtrace-Tiefe
|
|
1149
1168
|
sampling_rate: Anteil der erfassten Fehler
|
|
1150
1169
|
application_name: Name dieser Anwendung (wird automatisch erkannt, wenn nicht
|