rails_error_dashboard 0.8.3 → 0.9.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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +156 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +95 -73
  5. data/app/controllers/rails_error_dashboard/locales_controller.rb +87 -0
  6. data/app/helpers/rails_error_dashboard/application_helper.rb +20 -14
  7. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +15 -6
  8. data/app/helpers/rails_error_dashboard/i18n_helper.rb +154 -0
  9. data/app/helpers/rails_error_dashboard/mailer_i18n_helper.rb +69 -0
  10. data/app/helpers/rails_error_dashboard/overview_helper.rb +18 -6
  11. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +25 -11
  12. data/app/jobs/rails_error_dashboard/application_job.rb +5 -0
  13. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +18 -16
  14. data/app/jobs/rails_error_dashboard/concerns/localized_job.rb +83 -0
  15. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -2
  16. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +4 -2
  17. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +6 -2
  18. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +8 -3
  19. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +6 -4
  20. data/app/jobs/rails_error_dashboard/storm_notification_job.rb +26 -8
  21. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +4 -2
  22. data/app/mailers/rails_error_dashboard/application_mailer.rb +14 -0
  23. data/app/mailers/rails_error_dashboard/digest_mailer.rb +18 -2
  24. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +19 -2
  25. data/app/views/layouts/rails_error_dashboard.html.erb +255 -92
  26. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +23 -22
  27. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +30 -19
  28. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +18 -16
  29. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +21 -14
  30. data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +11 -7
  31. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +45 -38
  32. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +17 -14
  33. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +27 -9
  34. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +25 -21
  35. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +25 -22
  36. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +24 -12
  37. data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +12 -11
  38. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +19 -13
  39. data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +14 -14
  40. data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +8 -8
  41. data/app/views/rails_error_dashboard/errors/_modals.html.erb +60 -52
  42. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +59 -49
  43. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +23 -20
  44. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +14 -14
  45. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +118 -93
  46. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +18 -12
  47. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -5
  48. data/app/views/rails_error_dashboard/errors/_stats.html.erb +5 -5
  49. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +7 -7
  50. data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +15 -12
  51. data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +25 -25
  52. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -28
  53. data/app/views/rails_error_dashboard/errors/analytics.html.erb +82 -82
  54. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +27 -27
  55. data/app/views/rails_error_dashboard/errors/correlation.html.erb +61 -57
  56. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +64 -65
  57. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +23 -23
  58. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +48 -36
  59. data/app/views/rails_error_dashboard/errors/index.html.erb +121 -70
  60. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +30 -30
  61. data/app/views/rails_error_dashboard/errors/llm_health_summary.html.erb +38 -39
  62. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +28 -25
  63. data/app/views/rails_error_dashboard/errors/overview.html.erb +45 -39
  64. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +45 -44
  65. data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +39 -28
  66. data/app/views/rails_error_dashboard/errors/releases.html.erb +46 -47
  67. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +48 -44
  68. data/app/views/rails_error_dashboard/errors/settings.html.erb +122 -112
  69. data/app/views/rails_error_dashboard/errors/show.html.erb +44 -37
  70. data/app/views/rails_error_dashboard/errors/storms.html.erb +30 -24
  71. data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +26 -26
  72. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +31 -30
  73. data/config/locales/de.yml +2007 -0
  74. data/config/locales/en.yml +2719 -0
  75. data/config/locales/es.yml +2079 -0
  76. data/config/locales/fr.yml +2082 -0
  77. data/config/locales/it.yml +2060 -0
  78. data/config/locales/ja.yml +1802 -0
  79. data/config/locales/pl.yml +2108 -0
  80. data/config/locales/pt-BR.yml +2064 -0
  81. data/config/locales/ru.yml +2109 -0
  82. data/config/locales/uk.yml +2106 -0
  83. data/config/locales/zh-CN.yml +1791 -0
  84. data/config/routes.rb +5 -0
  85. data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +9 -3
  86. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +3 -1
  87. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +4 -2
  88. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +4 -2
  89. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +4 -2
  90. data/lib/rails_error_dashboard/commands/create_issue.rb +5 -3
  91. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +4 -2
  92. data/lib/rails_error_dashboard/commands/log_error.rb +1 -1
  93. data/lib/rails_error_dashboard/configuration.rb +23 -0
  94. data/lib/rails_error_dashboard/current.rb +56 -0
  95. data/lib/rails_error_dashboard/i18n_store.rb +293 -0
  96. data/lib/rails_error_dashboard/private_backend.rb +213 -0
  97. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +89 -22
  98. data/lib/rails_error_dashboard/services/digest_builder.rb +21 -7
  99. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +27 -15
  100. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +10 -5
  101. data/lib/rails_error_dashboard/services/localized_time_formatter.rb +117 -0
  102. data/lib/rails_error_dashboard/services/notification_helpers.rb +87 -4
  103. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +22 -5
  104. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +48 -28
  105. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -1
  106. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +15 -1
  107. data/lib/rails_error_dashboard/translation.rb +39 -0
  108. data/lib/rails_error_dashboard/version.rb +1 -1
  109. data/lib/rails_error_dashboard.rb +4 -0
  110. data/lib/tasks/error_dashboard.rake +5 -1
  111. metadata +24 -4
@@ -1,22 +1,22 @@
1
- <% content_for :page_title, "LLM Health" %>
1
+ <% content_for :page_title, red_t("red.health.llm.title") %>
2
2
 
3
3
  <div>
4
4
  <div class="d-flex justify-content-between align-items-center mb-4">
5
5
  <h1 style="font-size: 20px; font-weight: 700; margin: 0;">
6
6
  <i class="bi bi-cpu-fill me-2"></i>
7
- LLM Health
7
+ <%= red_t("red.health.llm.title") %>
8
8
  </h1>
9
9
 
10
10
  <% unless @feature_disabled %>
11
11
  <div class="btn-group" role="group">
12
12
  <%= link_to llm_health_summary_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
13
- 7 Days
13
+ <%= red_t("red.common.days_filter.7") %>
14
14
  <% end %>
15
15
  <%= link_to llm_health_summary_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
16
- 30 Days
16
+ <%= red_t("red.common.days_filter.30") %>
17
17
  <% end %>
18
18
  <%= link_to llm_health_summary_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
19
- 90 Days
19
+ <%= red_t("red.common.days_filter.90") %>
20
20
  <% end %>
21
21
  </div>
22
22
  <% end %>
@@ -25,13 +25,13 @@
25
25
  <% if @feature_disabled %>
26
26
  <div class="red-empty-state">
27
27
  <i class="bi bi-cpu-fill display-1 text-muted mb-3"></i>
28
- <div class="red-empty-state-title">LLM Observability Not Enabled</div>
28
+ <div class="red-empty-state-title"><%= red_t("red.health.llm.disabled.title") %></div>
29
29
  <p class="text-muted">
30
- To see per-model LLM stats here, enable LLM observability and breadcrumbs.
30
+ <%= red_t("red.health.llm.disabled.message") %>
31
31
  </p>
32
32
  <div class="card mx-auto" style="max-width: 600px;">
33
33
  <div class="card-body text-start">
34
- <h6>Enable in <code>config/initializers/rails_error_dashboard.rb</code>:</h6>
34
+ <h6><%= red_t("red.health.llm.disabled.enable_in_html") %></h6>
35
35
  <pre class="mb-0" style="background: var(--bg-subtle); padding: var(--space-3); border-radius: var(--radius-md); font-size: 12px;"><code>RailsErrorDashboard.configure do |config|
36
36
  config.enable_breadcrumbs = true
37
37
  config.enable_llm_observability = true
@@ -43,10 +43,9 @@ end</code></pre>
43
43
  <% elsif @totals[:total_calls] == 0 && @totals[:total_tool_calls] == 0 %>
44
44
  <div class="red-empty-state">
45
45
  <i class="bi bi-cpu-fill display-1 text-success mb-3"></i>
46
- <div class="red-empty-state-title">No LLM Calls Detected</div>
46
+ <div class="red-empty-state-title"><%= red_t("red.health.llm.empty.title") %></div>
47
47
  <p class="text-muted">
48
- No LLM API calls were captured in error breadcrumbs over the last <%= @days %> days.
49
- Instrument LLM calls in one of three ways:
48
+ <%= red_t("red.health.llm.empty.message", days: @days) %>
50
49
  </p>
51
50
 
52
51
  <div class="card mx-auto" style="max-width: 720px;">
@@ -54,41 +53,41 @@ end</code></pre>
54
53
  <ul class="nav nav-tabs" role="tablist" id="llmPathTabs">
55
54
  <li class="nav-item" role="presentation">
56
55
  <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#llmPathOtel" type="button" role="tab">
57
- <i class="bi bi-diagram-3 me-1"></i> OpenTelemetry
56
+ <i class="bi bi-diagram-3 me-1"></i> <%= red_t("red.health.llm.empty.tab_otel") %>
58
57
  </button>
59
58
  </li>
60
59
  <li class="nav-item" role="presentation">
61
60
  <button class="nav-link" data-bs-toggle="tab" data-bs-target="#llmPathFaraday" type="button" role="tab">
62
- <i class="bi bi-arrow-left-right me-1"></i> Faraday / RubyLLM
61
+ <i class="bi bi-arrow-left-right me-1"></i> <%= red_t("red.health.llm.empty.tab_faraday") %>
63
62
  </button>
64
63
  </li>
65
64
  <li class="nav-item" role="presentation">
66
65
  <button class="nav-link" data-bs-toggle="tab" data-bs-target="#llmPathManual" type="button" role="tab">
67
- <i class="bi bi-pencil-square me-1"></i> Manual
66
+ <i class="bi bi-pencil-square me-1"></i> <%= red_t("red.health.llm.empty.tab_manual") %>
68
67
  </button>
69
68
  </li>
70
69
  </ul>
71
70
 
72
71
  <div class="tab-content p-3 text-start">
73
72
  <div class="tab-pane fade show active" id="llmPathOtel" role="tabpanel">
74
- <p class="mb-2"><small>Already running an OTel collector? Drop in this gem and we'll subscribe automatically.</small></p>
73
+ <p class="mb-2"><small><%= red_t("red.health.llm.empty.otel_hint") %></small></p>
75
74
  <pre class="mb-2" style="background: var(--bg-subtle); padding: var(--space-3); border-radius: var(--radius-md); font-size: 12px;"><code># Gemfile
76
75
  gem "opentelemetry-instrumentation-ruby_llm"
77
76
 
78
77
  # Then nothing else — OTel spans flow into RED breadcrumbs.</code></pre>
79
- <small class="text-muted">Requires <code>enable_llm_observability = true</code>.</small>
78
+ <small class="text-muted"><%= red_t("red.health.llm.empty.otel_requires_html") %></small>
80
79
  </div>
81
80
 
82
81
  <div class="tab-pane fade" id="llmPathFaraday" role="tabpanel">
83
- <p class="mb-2"><small>Using <code>ruby_llm</code>, <code>openai</code>, <code>anthropic</code>, or any Faraday-based SDK? Insert the middleware once.</small></p>
82
+ <p class="mb-2"><small><%= red_t("red.health.llm.empty.faraday_hint") %></small></p>
84
83
  <pre class="mb-2" style="background: var(--bg-subtle); padding: var(--space-3); border-radius: var(--radius-md); font-size: 12px;"><code>Faraday.default_connection_options.builder.tap do |b|
85
84
  b.use RailsErrorDashboard::Integrations::LlmMiddleware
86
85
  end</code></pre>
87
- <small class="text-muted">RED auto-detects provider/model and tokens from the request/response.</small>
86
+ <small class="text-muted"><%= red_t("red.health.llm.empty.faraday_note") %></small>
88
87
  </div>
89
88
 
90
89
  <div class="tab-pane fade" id="llmPathManual" role="tabpanel">
91
- <p class="mb-2"><small>Direct <code>Net::HTTP</code>, gRPC, local inference, or anything else — instrument by hand.</small></p>
90
+ <p class="mb-2"><small><%= red_t("red.health.llm.empty.manual_hint") %></small></p>
92
91
  <pre class="mb-2" style="background: var(--bg-subtle); padding: var(--space-3); border-radius: var(--radius-md); font-size: 12px;"><code>ActiveSupport::Notifications.instrument(
93
92
  "red.llm_call",
94
93
  provider: "anthropic",
@@ -98,7 +97,7 @@ end</code></pre>
98
97
  ) do
99
98
  call_anthropic(...)
100
99
  end</code></pre>
101
- <small class="text-muted">Cost is auto-estimated from tokens — payload can override with <code>cost_usd_estimate:</code>.</small>
100
+ <small class="text-muted"><%= red_t("red.health.llm.empty.manual_note_html") %></small>
102
101
  </div>
103
102
  </div>
104
103
  </div>
@@ -111,7 +110,7 @@ end</code></pre>
111
110
  <div class="card text-center">
112
111
  <div class="card-body">
113
112
  <div class="display-6 text-primary"><%= @totals[:total_calls] %></div>
114
- <small class="text-muted">LLM Calls</small>
113
+ <small class="text-muted"><%= red_t("red.health.llm.stats.calls") %></small>
115
114
  </div>
116
115
  </div>
117
116
  </div>
@@ -119,7 +118,7 @@ end</code></pre>
119
118
  <div class="card text-center">
120
119
  <div class="card-body">
121
120
  <div class="display-6 text-secondary"><%= @totals[:model_count] %></div>
122
- <small class="text-muted">Models</small>
121
+ <small class="text-muted"><%= red_t("red.health.llm.stats.models") %></small>
123
122
  </div>
124
123
  </div>
125
124
  </div>
@@ -128,7 +127,7 @@ end</code></pre>
128
127
  <div class="card-body">
129
128
  <% err_color = @totals[:error_rate] >= 10 ? "danger" : (@totals[:error_rate] >= 5 ? "warning" : "success") %>
130
129
  <div class="display-6 text-<%= err_color %>"><%= @totals[:unique_error_count] %></div>
131
- <small class="text-muted">Errors with LLM (<%= @totals[:error_rate] %>%)</small>
130
+ <small class="text-muted"><%= red_t("red.health.llm.stats.errors_with_llm", rate: @totals[:error_rate]) %></small>
132
131
  </div>
133
132
  </div>
134
133
  </div>
@@ -136,7 +135,7 @@ end</code></pre>
136
135
  <div class="card text-center">
137
136
  <div class="card-body">
138
137
  <div class="display-6 text-info">$<%= format("%.2f", @totals[:total_cost_usd]) %></div>
139
- <small class="text-muted">Total Cost</small>
138
+ <small class="text-muted"><%= red_t("red.health.llm.stats.total_cost") %></small>
140
139
  </div>
141
140
  </div>
142
141
  </div>
@@ -146,7 +145,7 @@ end</code></pre>
146
145
  <div class="card-header d-flex justify-content-between align-items-center">
147
146
  <h5 class="mb-0">
148
147
  <i class="bi bi-cpu-fill text-primary me-2"></i>
149
- Performance by Model
148
+ <%= red_t("red.health.llm.by_model.title") %>
150
149
  <span class="badge bg-primary"><%= @totals[:model_count] %></span>
151
150
  </h5>
152
151
  <small class="text-muted"><%== @pagy.info_tag %></small>
@@ -156,15 +155,15 @@ end</code></pre>
156
155
  <table class="table table-hover mb-0">
157
156
  <thead class="table-light">
158
157
  <tr>
159
- <th>Provider · Model</th>
160
- <th width="90">Calls</th>
161
- <th width="80">Tools</th>
162
- <th width="130">Avg tokens</th>
163
- <th width="110">Avg latency</th>
164
- <th width="100">Error rate</th>
165
- <th width="100">Cost</th>
166
- <th>Top error</th>
167
- <th width="140">Last seen</th>
158
+ <th><%= red_t("red.health.llm.by_model.column_provider_model") %></th>
159
+ <th width="90"><%= red_t("red.health.llm.by_model.column_calls") %></th>
160
+ <th width="80"><%= red_t("red.health.llm.by_model.column_tools") %></th>
161
+ <th width="130"><%= red_t("red.health.llm.by_model.column_avg_tokens") %></th>
162
+ <th width="110"><%= red_t("red.health.llm.by_model.column_avg_latency") %></th>
163
+ <th width="100"><%= red_t("red.health.llm.by_model.column_error_rate") %></th>
164
+ <th width="100"><%= red_t("red.health.llm.by_model.column_cost") %></th>
165
+ <th><%= red_t("red.health.llm.by_model.column_top_error") %></th>
166
+ <th width="140"><%= red_t("red.health.llm.by_model.column_last_seen") %></th>
168
167
  </tr>
169
168
  </thead>
170
169
  <tbody>
@@ -185,8 +184,8 @@ end</code></pre>
185
184
  <td>
186
185
  <% if entry[:avg_input_tokens] || entry[:avg_output_tokens] %>
187
186
  <small>
188
- in <strong><%= number_with_delimiter(entry[:avg_input_tokens] || 0) %></strong><br>
189
- out <strong><%= number_with_delimiter(entry[:avg_output_tokens] || 0) %></strong>
187
+ <%= red_t("red.health.llm.by_model.avg_tokens_in") %> <strong><%= number_with_delimiter(entry[:avg_input_tokens] || 0) %></strong><br>
188
+ <%= red_t("red.health.llm.by_model.avg_tokens_out") %> <strong><%= number_with_delimiter(entry[:avg_output_tokens] || 0) %></strong>
190
189
  </small>
191
190
  <% else %>
192
191
  <span class="text-muted">—</span>
@@ -204,7 +203,7 @@ end</code></pre>
204
203
  <% sev_color = entry[:severity] == :danger ? "danger" : (entry[:severity] == :warning ? "warning" : "success") %>
205
204
  <span class="badge bg-<%= sev_color %>"><%= entry[:error_rate] %>%</span>
206
205
  <% if entry[:error_count].positive? %>
207
- <br><small class="text-muted"><%= entry[:error_count] %> failed</small>
206
+ <br><small class="text-muted"><%= red_t("red.health.llm.by_model.failed_count", count: entry[:error_count]) %></small>
208
207
  <% end %>
209
208
  </td>
210
209
  <td>
@@ -232,11 +231,11 @@ end</code></pre>
232
231
  <div class="card-footer border-top d-flex justify-content-between align-items-center">
233
232
  <div>
234
233
  <small class="text-muted">
235
- <i class="bi bi-lightbulb text-warning"></i> High error rates on a model often signal quota or rate-limit issues. Slow latency (&gt;5s) suggests provider degradation.
234
+ <i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.health.llm.by_model.tip_html") %>
236
235
  </small>
237
236
  <small class="ms-3">
238
237
  <a href="https://github.com/thoughtbot/opentelemetry-instrumentation-ruby_llm" target="_blank" rel="noopener" class="text-decoration-none">
239
- <i class="bi bi-book"></i> Instrumentation Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
238
+ <i class="bi bi-book"></i> <%= red_t("red.health.llm.by_model.guide_link") %> <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
240
239
  </a>
241
240
  </small>
242
241
  </div>
@@ -1,21 +1,21 @@
1
- <% content_for :page_title, "N+1 Query Patterns" %>
1
+ <% content_for :page_title, red_t("red.errors.n_plus_one_page.title") %>
2
2
 
3
3
  <div>
4
4
  <div class="d-flex justify-content-between align-items-center mb-4">
5
5
  <h1 style="font-size: 20px; font-weight: 700; margin: 0;">
6
6
  <i class="bi bi-arrow-repeat me-2"></i>
7
- N+1 Query Patterns
7
+ <%= red_t("red.errors.n_plus_one_page.title") %>
8
8
  </h1>
9
9
 
10
10
  <div class="btn-group" role="group">
11
11
  <%= link_to n_plus_one_summary_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
- 7 Days
12
+ <%= red_t("red.common.days_filter.7") %>
13
13
  <% end %>
14
14
  <%= link_to n_plus_one_summary_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
- 30 Days
15
+ <%= red_t("red.common.days_filter.30") %>
16
16
  <% end %>
17
17
  <%= link_to n_plus_one_summary_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
- 90 Days
18
+ <%= red_t("red.common.days_filter.90") %>
19
19
  <% end %>
20
20
  </div>
21
21
  </div>
@@ -23,18 +23,21 @@
23
23
  <% if @unique_count == 0 %>
24
24
  <div class="red-empty-state">
25
25
  <div class="red-empty-state-icon" style="background: var(--status-success-bg); color: var(--status-success);"><i class="bi bi-check-lg"></i></div>
26
- <div class="red-empty-state-title">No N+1 Query Patterns Found</div>
26
+ <div class="red-empty-state-title"><%= red_t("red.errors.n_plus_one_page.empty_title") %></div>
27
27
  <p class="text-muted">
28
- No N+1 query patterns were detected in error breadcrumbs over the last <%= @days %> days.
28
+ <%= red_t("red.errors.n_plus_one_page.empty_body", days: @days) %>
29
29
  </p>
30
30
  <div class="card mx-auto" style="max-width: 500px;">
31
31
  <div class="card-body text-start">
32
- <h6>How N+1 detection works:</h6>
32
+ <h6><%= red_t("red.errors.n_plus_one_page.how_title") %></h6>
33
33
  <ul class="mb-0">
34
- <li>Breadcrumbs must be enabled (<code>enable_breadcrumbs = true</code>)</li>
35
- <li>SQL queries are captured as breadcrumbs during requests that produce errors</li>
36
- <li>Queries with the same shape appearing >= <code>n_plus_one_threshold</code> times are flagged</li>
37
- <li>Default threshold is 3 repetitions</li>
34
+ <li><%= red_t("red.errors.pages.how_breadcrumbs_html", option: content_tag(:code, "enable_breadcrumbs = true")) %></li>
35
+ <li><%= red_t("red.errors.n_plus_one_page.how_captured") %></li>
36
+ <li><%= red_t("red.errors.n_plus_one_page.how_threshold_html", threshold: content_tag(:code, "n_plus_one_threshold")) %></li>
37
+ <%# The default is read from the configuration rather than written
38
+ here too: a prose copy of a config value drifts the moment the
39
+ default changes, and the copy the user reads is the stale one. %>
40
+ <li><%= red_tp("red.errors.n_plus_one_page.how_default", count: RailsErrorDashboard.configuration.n_plus_one_threshold) %></li>
38
41
  </ul>
39
42
  </div>
40
43
  </div>
@@ -45,7 +48,7 @@
45
48
  <div class="card text-center">
46
49
  <div class="card-body">
47
50
  <div class="display-6 text-danger"><%= @unique_count %></div>
48
- <small class="text-muted">Unique Patterns</small>
51
+ <small class="text-muted"><%= red_t("red.errors.n_plus_one_page.unique_patterns") %></small>
49
52
  </div>
50
53
  </div>
51
54
  </div>
@@ -53,7 +56,7 @@
53
56
  <div class="card text-center">
54
57
  <div class="card-body">
55
58
  <div class="display-6 text-warning"><%= @total_count %></div>
56
- <small class="text-muted">Total Occurrences</small>
59
+ <small class="text-muted"><%= red_t("red.errors.pages.total_occurrences") %></small>
57
60
  </div>
58
61
  </div>
59
62
  </div>
@@ -61,7 +64,7 @@
61
64
  <div class="card text-center">
62
65
  <div class="card-body">
63
66
  <div class="display-6 text-info"><%= @affected_count %></div>
64
- <small class="text-muted">Affected Errors</small>
67
+ <small class="text-muted"><%= red_t("red.errors.pages.affected_errors") %></small>
65
68
  </div>
66
69
  </div>
67
70
  </div>
@@ -71,7 +74,7 @@
71
74
  <div class="card-header d-flex justify-content-between align-items-center">
72
75
  <h5 class="mb-0">
73
76
  <i class="bi bi-arrow-repeat text-danger me-2"></i>
74
- All N+1 Query Patterns
77
+ <%= red_t("red.errors.n_plus_one_page.all_title") %>
75
78
  <span class="badge bg-danger"><%= @unique_count %></span>
76
79
  </h5>
77
80
  <small class="text-muted"><%== @pagy.info_tag %></small>
@@ -81,11 +84,11 @@
81
84
  <table class="table table-hover mb-0">
82
85
  <thead class="table-light">
83
86
  <tr>
84
- <th>Query Pattern</th>
85
- <th width="120">Occurrences</th>
86
- <th width="250">Errors</th>
87
- <th width="120">Total Time</th>
88
- <th width="140">Last Seen</th>
87
+ <th><%= red_t("red.errors.n_plus_one.column_query") %></th>
88
+ <th width="120"><%= red_t("red.errors.pages.column_occurrences") %></th>
89
+ <th width="250"><%= red_t("red.errors.pages.column_errors") %></th>
90
+ <th width="120"><%= red_t("red.errors.n_plus_one.column_total_time") %></th>
91
+ <th width="140"><%= red_t("red.errors.pages.column_last_seen") %></th>
89
92
  </tr>
90
93
  </thead>
91
94
  <tbody>
@@ -99,12 +102,12 @@
99
102
  <%= link_to "##{eid}", error_path(eid), class: "text-decoration-none" %><%= "," unless i == shown_ids.size - 1 %>
100
103
  <% end %>
101
104
  <% if pattern[:error_ids].size > 5 %>
102
- <small class="text-muted">+<%= pattern[:error_ids].size - 5 %> more</small>
105
+ <small class="text-muted"><%= red_tp("red.errors.pages.more_errors", count: pattern[:error_ids].size - 5) %></small>
103
106
  <% end %>
104
107
  </td>
105
108
  <td>
106
109
  <span class="<%= pattern[:total_duration_ms] > 100 ? 'text-danger fw-bold' : '' %>">
107
- <%= pattern[:total_duration_ms] %> ms
110
+ <%= red_t("red.errors.n_plus_one_page.duration_ms", value: pattern[:total_duration_ms]) %>
108
111
  </span>
109
112
  </td>
110
113
  <td><%= local_time_ago(pattern[:last_seen]) %></td>
@@ -117,11 +120,11 @@
117
120
  <div class="card-footer border-top d-flex justify-content-between align-items-center">
118
121
  <div>
119
122
  <small class="text-muted">
120
- <i class="bi bi-lightbulb text-warning"></i> Use <code>includes</code>, <code>preload</code>, or <code>eager_load</code> to fix N+1 queries.
123
+ <i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.errors.n_plus_one_page.footer_tip_html", includes: content_tag(:code, "includes"), preload: content_tag(:code, "preload"), eager_load: content_tag(:code, "eager_load")) %>
121
124
  </small>
122
125
  <small class="ms-3">
123
126
  <a href="https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations" target="_blank" rel="noopener" class="text-decoration-none">
124
- <i class="bi bi-book"></i> Rails Eager Loading Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
127
+ <i class="bi bi-book"></i> <%= red_t("red.errors.n_plus_one.eager_loading_guide") %> <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
125
128
  </a>
126
129
  </small>
127
130
  </div>