rails_error_dashboard 0.10.0 → 0.11.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -3
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +3 -0
  4. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +8 -1
  5. data/app/models/rails_error_dashboard/error_log.rb +1 -0
  6. data/app/views/layouts/rails_error_dashboard.html.erb +30 -0
  7. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +5 -0
  8. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -0
  9. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +8 -0
  10. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +9 -0
  11. data/app/views/rails_error_dashboard/errors/analytics.html.erb +51 -1
  12. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +1 -1
  13. data/app/views/rails_error_dashboard/errors/index.html.erb +8 -1
  14. data/app/views/rails_error_dashboard/errors/overview.html.erb +2 -2
  15. data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -0
  16. data/config/locales/de.yml +19 -2
  17. data/config/locales/en.yml +17 -0
  18. data/config/locales/es.yml +18 -2
  19. data/config/locales/fr.yml +19 -2
  20. data/config/locales/it.yml +27 -3
  21. data/config/locales/ja.yml +17 -3
  22. data/config/locales/pl.yml +19 -2
  23. data/config/locales/pt-BR.yml +18 -2
  24. data/config/locales/ru.yml +18 -2
  25. data/config/locales/uk.yml +18 -2
  26. data/config/locales/zh-CN.yml +15 -3
  27. data/db/migrate/20260826000001_add_environment_to_error_logs.rb +26 -0
  28. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +11 -0
  29. data/lib/rails_error_dashboard/commands/backfill_environments.rb +58 -0
  30. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +55 -29
  31. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +40 -9
  32. data/lib/rails_error_dashboard/commands/log_error.rb +17 -0
  33. data/lib/rails_error_dashboard/configuration.rb +52 -0
  34. data/lib/rails_error_dashboard/queries/analytics_stats.rb +9 -0
  35. data/lib/rails_error_dashboard/queries/errors_list.rb +8 -0
  36. data/lib/rails_error_dashboard/queries/filter_options.rb +9 -0
  37. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +8 -0
  38. data/lib/rails_error_dashboard/services/error_broadcaster.rb +6 -2
  39. data/lib/rails_error_dashboard/services/issue_body_formatter.rb +1 -0
  40. data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +1 -0
  41. data/lib/rails_error_dashboard/services/notification_throttler.rb +23 -0
  42. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +1 -0
  43. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +9 -0
  44. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -0
  45. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +1 -0
  46. data/lib/rails_error_dashboard/version.rb +1 -1
  47. data/lib/rails_error_dashboard.rb +1 -0
  48. data/lib/tasks/rails_error_dashboard_tasks.rake +6 -0
  49. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7065559d38e3152511024cd4a44698c3b215cc6cd60e4917d52cf4d893180b6c
4
- data.tar.gz: 0d9237d4e89f4cb08392aecfe109dbb38bc1aaf673e834d827441bebf84a6e3f
3
+ metadata.gz: defed3f836bc0e812c76abfb81b95e5e3f229e5338bfa40cde4604643eb95582
4
+ data.tar.gz: 7657043dbf617694e5318a5b0e2829ecca78ade0c67412ede16fbecabc696c36
5
5
  SHA512:
6
- metadata.gz: 996b72e8406365ca4ee064464faa785b7a43dfa5f643ed9f2ca3be39d4d98b3dd13ed6426a1dbef3eb53fad3d6433d89b70b3f8a3d8e40434aad1e882cff0eb1
7
- data.tar.gz: 4814ed8949397ce5ca0cd27cf4e9f52f5ba2e5bf243eff411eb0e7c80539fef978c0dc87505c02bb0e2a259725a8fec97a5897c62073eae2a9142c0f0b2c2ea8
6
+ metadata.gz: 79992d38956c257a116c1cfcb153429cbf3a082892bc71664c20ba57e801ba33325ae69f43bd0ef8404a3cd08a6d5df9727a989a0f8977826786a179ad70b448
7
+ data.tar.gz: 3bbcc477eff8db567415c727037a58d7e27bd8e01ad602674beb05cd6891f75bb06e665273698fed1b49c20806e4f07dbea586647583f6b7da3052a744d58fae
data/README.md CHANGED
@@ -23,7 +23,7 @@ gem 'rails_error_dashboard'
23
23
 
24
24
  **[rails-error-dashboard.anjan.dev](https://rails-error-dashboard.anjan.dev)** — Username: `gandalf` · Password: `youshallnotpass`
25
25
 
26
- > **Beta Software** — Functional and tested (2,700+ tests passing), but the API may change before v1.0. Supports Rails 7.0-8.1 and Ruby 3.2-4.0.
26
+ > **Beta Software** — Functional and tested (4,200+ tests passing), but the API may change before v1.0. Supports Rails 7.0-8.1 and Ruby 3.2-4.0.
27
27
 
28
28
  ### Screenshots
29
29
 
@@ -372,7 +372,7 @@ config.llm_model = "gpt-5"
372
372
  <details>
373
373
  <summary><strong>Notifications — Slack, Discord, PagerDuty, Email, Webhooks</strong></summary>
374
374
 
375
- Multi-channel alerting with severity filters, per-error cooldown, and milestone threshold alerts to prevent alert fatigue.
375
+ Multi-channel alerting with severity filters, per-error cooldown, milestone threshold alerts, and a per-environment allowlist (`config.notification_environments = %w[production]`) so a staging deploy never pages anyone.
376
376
 
377
377
  ```ruby
378
378
  config.enable_slack_notifications = true
@@ -382,6 +382,19 @@ config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
382
382
  [Notification setup guide →](docs/guides/NOTIFICATIONS.md)
383
383
  </details>
384
384
 
385
+ <details>
386
+ <summary><strong>Environment Awareness — Filter, Badge, Notify per Environment</strong></summary>
387
+
388
+ Every error records the environment it came from — `production`, `staging`, `uat`, `preprod`, any name your deploys use. The errors index filters by it, rows and the detail page carry a badge, the analytics page breaks errors down by environment, and every notification names it. The same error in staging and production is two rows with independent status, so resolving one never hides the other.
389
+
390
+ ```ruby
391
+ config.environment = ENV.fetch("ERROR_DASHBOARD_ENVIRONMENT", Rails.env) # free-form, defaults to Rails.env
392
+ config.notification_environments = %w[production] # nil = notify everywhere
393
+ ```
394
+
395
+ Errors captured before v0.11.0 show no badge until they recur (the next occurrence claims the row) or you run `rails rails_error_dashboard:backfill_environments`.
396
+ </details>
397
+
385
398
  <details>
386
399
  <summary><strong>Advanced Analytics</strong></summary>
387
400
 
@@ -709,7 +722,7 @@ Built with **CQRS (Command/Query Responsibility Segregation)**:
709
722
 
710
723
  ## Testing
711
724
 
712
- 2,600+ tests covering unit, integration, and browser-based system tests.
725
+ 4,200+ tests covering unit, integration, and browser-based system tests.
713
726
 
714
727
  ```bash
715
728
  bundle exec rspec # Full suite
@@ -11,6 +11,7 @@ module RailsErrorDashboard
11
11
  error_type
12
12
  unresolved
13
13
  platform
14
+ environment
14
15
  application_id
15
16
  search
16
17
  severity
@@ -94,6 +95,7 @@ module RailsErrorDashboard
94
95
  filter_options = Queries::FilterOptions.call(application_id: @current_application_id)
95
96
  @error_types = filter_options[:error_types]
96
97
  @platforms = filter_options[:platforms]
98
+ @environments = filter_options[:environments]
97
99
  @assignees = filter_options[:assignees]
98
100
  end
99
101
 
@@ -242,6 +244,7 @@ module RailsErrorDashboard
242
244
  @errors_over_time = analytics[:errors_over_time]
243
245
  @errors_by_type = analytics[:errors_by_type]
244
246
  @errors_by_platform = analytics[:errors_by_platform]
247
+ @errors_by_environment = analytics[:errors_by_environment]
245
248
  @errors_by_hour = analytics[:errors_by_hour]
246
249
  @top_users = analytics[:top_users]
247
250
  @resolution_rate = analytics[:resolution_rate]
@@ -20,12 +20,19 @@ module RailsErrorDashboard
20
20
  # The subject is assembled from a key rather than interpolated inline, so a
21
21
  # locale can reorder or drop the emoji. error_type and message are verbatim
22
22
  # error content and are never translated.
23
+ #
24
+ # An error that carries an environment uses subject_with_environment; a
25
+ # legacy row keeps the pre-0.11 subject so existing mail filters still match.
23
26
  def subject_for(error_log, locale)
27
+ environment = error_log.respond_to?(:environment) ? error_log.environment : nil
28
+ key = environment.present? ? "red.mailers.error_alert.subject_with_environment" : "red.mailers.error_alert.subject"
29
+
24
30
  I18nStore.translate(
25
- "red.mailers.error_alert.subject",
31
+ key,
26
32
  locale: locale,
27
33
  application: error_log.application&.name ||
28
34
  I18nStore.translate("red.mailers.shared.unknown_application", locale: locale),
35
+ environment: environment,
29
36
  error_type: error_log.error_type,
30
37
  message: truncate_subject(error_log.message)
31
38
  )
@@ -61,6 +61,7 @@ module RailsErrorDashboard
61
61
  scope :by_error_type, ->(type) { where(error_type: type) }
62
62
  scope :by_type, ->(type) { where(error_type: type) }
63
63
  scope :by_platform, ->(platform) { where(platform: platform) }
64
+ scope :by_environment, ->(environment) { where(environment: environment) }
64
65
  scope :last_24_hours, -> { where("occurred_at >= ?", 24.hours.ago) }
65
66
  scope :last_week, -> { where("occurred_at >= ?", 1.week.ago) }
66
67
 
@@ -660,7 +660,16 @@ dd { color: var(--text-primary); }
660
660
  height: 48px; flex-shrink: 0;
661
661
  background: var(--surface-primary);
662
662
  border-bottom: 1px solid var(--border-primary);
663
+ /* A flex item's default min-width is auto, meaning "never shrink below my
664
+ content". Both navbar groups therefore kept their full intrinsic width on a
665
+ narrow screen and pushed the right-hand controls past the viewport — the
666
+ dashboard scrolled sideways at 375px in EVERY locale, English included.
667
+ min-width:0 lets them shrink; overflow:hidden keeps whatever still cannot
668
+ fit from extending the page rather than being clipped inside the bar. */
669
+ min-width: 0;
670
+ overflow: hidden;
663
671
  }
672
+ .red-navbar > * { min-width: 0; }
664
673
  .red-search-input {
665
674
  width: 240px; padding: 5px 10px 5px 30px; font-size: 13px;
666
675
  border: 1px solid var(--border-primary); border-radius: var(--radius-sm);
@@ -720,6 +729,21 @@ dd { color: var(--text-primary); }
720
729
  .red-sidebar.collapsed { display: none; }
721
730
  }
722
731
 
732
+ /* Stat grids
733
+ These were inline `grid-template-columns: repeat(N, 1fr)`, which cannot carry
734
+ a media query, so three or four cards stayed side by side at 375px and pushed
735
+ the page to 488px — it scrolled sideways on a phone in EVERY locale, English
736
+ worst of all. auto-fit + minmax lets the row reflow to two columns and then
737
+ one, and minmax's 0 floor stops a long word forcing a track wider than the
738
+ screen. */
739
+ .red-stat-grid {
740
+ display: grid;
741
+ grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
742
+ gap: var(--space-4);
743
+ margin-bottom: var(--space-6);
744
+ }
745
+ .red-stat-grid > * { min-width: 0; }
746
+
723
747
  /* Error detail responsive grid */
724
748
  .red-detail-grid { display: grid; grid-template-columns: 1fr 260px; gap: var(--space-4); }
725
749
  @media (max-width: 1023.98px) { .red-detail-grid { grid-template-columns: 1fr; } }
@@ -729,7 +753,13 @@ dd { color: var(--text-primary); }
729
753
  padding: 3px 10px; font-size: 11px; font-weight: 600;
730
754
  border-radius: var(--radius-full);
731
755
  text-transform: uppercase; letter-spacing: 0.05em;
756
+ white-space: nowrap;
732
757
  }
758
+ /* Below the sidebar breakpoint the navbar has to fit a menu button, search,
759
+ language picker, shortcuts, theme toggle and the env badge. The badge is
760
+ decoration — the environment is also on the overview page — so it is the
761
+ first thing dropped rather than letting it push the page sideways. */
762
+ @media (max-width: 575.98px) { .red-env-badge { display: none; } }
733
763
 
734
764
  /* Kbd shortcut */
735
765
  kbd {
@@ -148,6 +148,11 @@
148
148
  </div>
149
149
  <% end %>
150
150
 
151
+ <% if @error_log.respond_to?(:environment) && @error_log.environment.present? %>
152
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.environment") %>:</div>
153
+ <div class="value"><strong><%= @error_log.environment %></strong></div>
154
+ <% end %>
155
+
151
156
  <div class="label"><%= red_t("red.mailers.error_alert.labels.occurred_at") %>:</div>
152
157
  <div class="value"><%= red_mail_time(@error_log.occurred_at) %></div>
153
158
 
@@ -10,6 +10,7 @@
10
10
  <%= red_mail_t("red.mailers.error_alert.labels.application") %>: <%= @error_log.application&.name || red_mail_t("red.mailers.shared.unknown_application") %>
11
11
  <%= red_mail_t("red.mailers.error_alert.labels.error_type") %>: <%= @error_log.error_type %>
12
12
  <% if @error_log.platform.present? %><%= red_mail_t("red.mailers.error_alert.labels.platform") %>: <%= @error_log.platform %><% end %>
13
+ <% if @error_log.respond_to?(:environment) && @error_log.environment.present? %><%= red_mail_t("red.mailers.error_alert.labels.environment") %>: <%= @error_log.environment %><% end %>
13
14
  <%= red_mail_t("red.mailers.error_alert.labels.occurred_at") %>: <%= red_mail_time(@error_log.occurred_at) %>
14
15
  <% if @error_log.user_id.present? %><%= red_mail_t("red.mailers.error_alert.labels.user_id") %>: <%= @error_log.user_id %><% end %>
15
16
  <% if @error_log.ip_address.present? %><%= red_mail_t("red.mailers.error_alert.labels.ip_address") %>: <%= @error_log.ip_address %><% end %>
@@ -99,4 +99,12 @@
99
99
  <% end %>
100
100
  </td>
101
101
  <% end %>
102
+ <% if local_assigns[:show_environment] %>
103
+ <%# Environment names are free-form data (production, staging, uat...) and are never translated. %>
104
+ <td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.environment %>">
105
+ <% if error.environment.present? %>
106
+ <span class="badge badge-api" data-environment-badge="<%= error.environment %>"><i class="bi bi-hdd-stack"></i> <%= error.environment %></span>
107
+ <% end %>
108
+ </td>
109
+ <% end %>
102
110
  </tr>
@@ -70,6 +70,15 @@
70
70
  <% end %>
71
71
  </div>
72
72
 
73
+ <% if error.respond_to?(:environment) && error.environment.present? %>
74
+ <div class="mb-3">
75
+ <small class="metadata-label d-block mb-1"><%= red_t("red.errors.sidebar.environment") %></small>
76
+ <%= link_to errors_path(environment: error.environment, unresolved: '0'), class: "text-decoration-none", title: red_t("red.errors.sidebar.view_all_environment", environment: error.environment) do %>
77
+ <span class="badge badge-api" data-environment-badge="<%= error.environment %>"><i class="bi bi-hdd-stack"></i> <%= error.environment %></span>
78
+ <% end %>
79
+ </div>
80
+ <% end %>
81
+
73
82
  <div class="mb-3">
74
83
  <small class="metadata-label d-block mb-1"><%= red_t("red.errors.sidebar.user") %></small>
75
84
  <% if error.respond_to?(:user) && error.user %>
@@ -45,7 +45,7 @@
45
45
  </div>
46
46
 
47
47
  <!-- Summary Stats -->
48
- <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin-bottom: var(--space-6);">
48
+ <div class="red-stat-grid">
49
49
  <div class="card stat-card" style="border-left: 3px solid var(--accent); padding: var(--space-5) var(--space-6);">
50
50
  <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.analytics.overview.stats.total_errors") %></div>
51
51
  <span class="stat-value"><%= @error_stats[:total] %></span>
@@ -155,6 +155,56 @@
155
155
  </div>
156
156
  <% end %>
157
157
 
158
+ <%# Errors by environment: same shape as the platform chart, shown only when
159
+ there is something to compare. Environment names are data, not text to
160
+ translate; NULL rows (captured before the column existed) are :unknown. %>
161
+ <% environment_counts = (@errors_by_environment || {}).transform_keys { |env| env == :unknown ? red_t("red.analytics.overview.environment_chart.unknown") : env } %>
162
+ <% if environment_counts.size > 1 %>
163
+ <div class="row g-4 mb-4">
164
+ <div class="col-md-12">
165
+ <div class="card">
166
+ <div class="card-header">
167
+ <h5 class="mb-0"><i class="bi bi-hdd-stack"></i> <%= red_t("red.analytics.overview.environment_chart.title") %></h5>
168
+ </div>
169
+ <div class="card-body">
170
+ <div id="errors-by-environment-chart"></div>
171
+ <script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
172
+ document.addEventListener('DOMContentLoaded', function() {
173
+ const colors = window.getChartColors();
174
+ const environmentColors = ["#EF4444", "#F59E0B", "#2563EB", "#10B981", "#8B5CF6", "#6B7280"];
175
+
176
+ new Chartkick.PieChart("errors-by-environment-chart", <%= raw environment_counts.to_json %>, {
177
+ colors: environmentColors,
178
+ height: "300px",
179
+ legend: "bottom",
180
+ donut: true,
181
+ library: {
182
+ plugins: {
183
+ legend: {
184
+ labels: {
185
+ color: colors.textColor,
186
+ font: { size: 12, weight: 'bold' }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ });
192
+ });
193
+ </script>
194
+ </div>
195
+ <div class="card-footer border-top-0">
196
+ <div class="d-flex gap-2 flex-wrap">
197
+ <small class="text-muted me-2"><%= red_t("red.analytics.overview.environment_chart.quick_links") %></small>
198
+ <% (@errors_by_environment || {}).keys.reject { |env| env == :unknown }.each do |environment| %>
199
+ <%= link_to environment, errors_path(environment: environment, unresolved: '0'), class: "btn btn-sm btn-outline-secondary" %>
200
+ <% end %>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ <% end %>
207
+
158
208
  <!-- Charts Row 2 -->
159
209
  <div class="row g-4 mb-4">
160
210
  <div class="col-md-12">
@@ -91,7 +91,7 @@
91
91
  <% if @live_health[:postgresql] %>
92
92
  <%# Summary cards for PostgreSQL %>
93
93
  <% total_db_size = (@host_tables + @gem_tables).sum { |t| t[:total_bytes] } %>
94
- <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); margin-bottom: var(--space-4);">
94
+ <div class="red-stat-grid" style="margin-bottom: var(--space-4);">
95
95
  <div class="card stat-card" style="padding: var(--space-4) var(--space-5); text-align: center;">
96
96
  <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.health.database.summary.total_db_size") %></div>
97
97
  <div class="stat-value"><%= number_to_human_size(total_db_size) %></div>
@@ -157,6 +157,9 @@
157
157
  <% if @platforms.size > 1 %>
158
158
  <%= select_tag :platform, options_for_select([[red_t("red.errors.index.filters.all_platforms"), '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select", style: "width: auto; min-width: 120px;" %>
159
159
  <% end %>
160
+ <% if @environments.size > 1 %>
161
+ <%= select_tag :environment, options_for_select([[red_t("red.errors.index.filters.all_environments"), '']] + @environments.map { |e| [e, e] }, params[:environment]), class: "form-select", style: "width: auto; min-width: 120px;" %>
162
+ <% end %>
160
163
  <%= select_tag :error_type, options_for_select([[red_t("red.errors.index.filters.all_types"), '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select", style: "width: auto; min-width: 120px;" %>
161
164
  <%
162
165
  # Built from the same keys the active chips read, so a label can never
@@ -217,6 +220,7 @@
217
220
  active_filters << { label: red_t("red.errors.index.filters.chips.application", value: app_name), param: :application_id } if app_name
218
221
  end
219
222
  active_filters << { label: red_t("red.errors.index.filters.chips.platform", value: params[:platform]), param: :platform } if params[:platform].present?
223
+ active_filters << { label: red_t("red.errors.index.filters.chips.environment", value: params[:environment]), param: :environment } if params[:environment].present?
220
224
  active_filters << { label: red_t("red.errors.index.filters.chips.error_type", value: params[:error_type]), param: :error_type } if params[:error_type].present?
221
225
  active_filters << { label: red_t("red.errors.index.filters.chips.severity", value: severity_label.call(params[:severity])), param: :severity } if params[:severity].present?
222
226
  active_filters << { label: red_t("red.errors.index.filters.chips.timeframe", value: timeframe_label.call(params[:timeframe])), param: :timeframe } if params[:timeframe].present?
@@ -287,11 +291,14 @@
287
291
  <% if @platforms.size > 1 %>
288
292
  <th style="padding: var(--space-3) var(--space-4); width: 100px;"><%= red_t("red.errors.index.columns.platform") %></th>
289
293
  <% end %>
294
+ <% if @environments.size > 1 %>
295
+ <th style="padding: var(--space-3) var(--space-4); width: 110px;"><%= red_t("red.errors.index.columns.environment") %></th>
296
+ <% end %>
290
297
  </tr>
291
298
  </thead>
292
299
  <tbody id="error_list">
293
300
  <% @errors.each do |error| %>
294
- <%= render "error_row", error: error, show_platform: @platforms.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
301
+ <%= render "error_row", error: error, show_platform: @platforms.size > 1, show_environment: @environments.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
295
302
  <% end %>
296
303
  </tbody>
297
304
  </table>
@@ -32,7 +32,7 @@
32
32
  trend_values = @stats[:errors_trend_7d].values rescue []
33
33
  trend_max = [ trend_values.max || 1, 1 ].max
34
34
  %>
35
- <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin-bottom: var(--space-6);">
35
+ <div class="red-stat-grid">
36
36
  <!-- Error Rate -->
37
37
  <div class="card stat-card" style="border-left: 3px solid var(--status-critical); padding: var(--space-5) var(--space-6);">
38
38
  <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.error_rate") %></div>
@@ -89,7 +89,7 @@
89
89
  </div>
90
90
 
91
91
  <!-- Secondary Stats (3 cards) -->
92
- <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin-bottom: var(--space-6);">
92
+ <div class="red-stat-grid">
93
93
  <!-- Affected Users -->
94
94
  <div class="card stat-card" style="padding: var(--space-5) var(--space-6);">
95
95
  <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.affected_users") %></div>
@@ -19,6 +19,7 @@
19
19
  color: "info",
20
20
  settings: {
21
21
  application_name: { type: :application_name },
22
+ environment: { type: :string },
22
23
  database: { type: :database_name },
23
24
  use_separate_database: { type: :boolean }
24
25
  }
@@ -45,6 +46,7 @@
45
46
  icon: "bi-bell",
46
47
  color: "info",
47
48
  settings: {
49
+ notification_environments: { type: :array },
48
50
  enable_slack_notifications: { type: :boolean },
49
51
  slack_webhook_url: { type: :url, show_if: :enable_slack_notifications },
50
52
  enable_email_notifications: { type: :boolean },
@@ -144,6 +144,7 @@ de:
144
144
  any: Beliebiger Schweregrad
145
145
  all_apps: Alle Anwendungen
146
146
  all_platforms: Alle Plattformen
147
+ all_environments: Alle Umgebungen
147
148
  all_types: Alle Typen
148
149
  all_timeframes: Gesamter Zeitraum
149
150
  all_frequencies: Alle Häufigkeiten
@@ -172,6 +173,7 @@ de:
172
173
  search: 'Suche: %{value}'
173
174
  application: 'Anwendung: %{value}'
174
175
  platform: 'Plattform: %{value}'
176
+ environment: 'Umgebung: %{value}'
175
177
  error_type: 'Typ: %{value}'
176
178
  severity: 'Schweregrad: %{value}'
177
179
  timeframe: 'Zeitraum: %{value}'
@@ -187,6 +189,7 @@ de:
187
189
  last_seen: Zuletzt gesehen
188
190
  application: Anwendung
189
191
  platform: Plattform
192
+ environment: Umgebung
190
193
  select_all: Alle Fehler auswählen
191
194
  batch:
192
195
  selected:
@@ -625,6 +628,7 @@ de:
625
628
  low: NIEDRIG
626
629
  platform: Plattform
627
630
  view_all_platform: Alle %{platform}-Fehler ansehen
631
+ view_all_environment: Alle Fehler aus %{environment} ansehen
628
632
  user: Benutzer
629
633
  view_user_errors: Alle Fehler dieses Benutzers ansehen
630
634
  user_id: 'ID: %{id}'
@@ -841,8 +845,9 @@ de:
841
845
  column_ips: IPs
842
846
  column_top_path: Häufigster Pfad
843
847
  column_top_agent: Häufigster Agent
844
- overflow_notice_html: >-
845
- %{count} Ereignisse konnten keiner Regel zugeordnet werden, da der Puffer des Trackers voll war. Erhöhen Sie %{option}, um mehr zu erfassen.
848
+ overflow_notice_html: "%{count} Ereignisse konnten keiner Regel zugeordnet
849
+ werden, da der Puffer des Trackers voll war. Erhöhen Sie %{option}, um mehr
850
+ zu erfassen."
846
851
  match_type:
847
852
  blocklist: blocklist
848
853
  throttle: throttle
@@ -1070,6 +1075,10 @@ de:
1070
1075
  sampling_rate: Anteil der erfassten Fehler
1071
1076
  application_name: Name dieser Anwendung (wird automatisch erkannt, wenn nicht
1072
1077
  gesetzt)
1078
+ environment: Umgebung, der Fehler zugeordnet werden (Rails.env, falls nicht
1079
+ gesetzt)
1080
+ notification_environments: Nur für diese Umgebungen benachrichtigen (alle,
1081
+ falls nicht gesetzt)
1073
1082
  database: Aktive Datenbank für Fehlerprotokolle
1074
1083
  use_separate_database: Eigene Datenbank für Fehlerprotokolle verwenden
1075
1084
  user_model: Modellname für Benutzerzuordnungen (wird automatisch erkannt,
@@ -1237,11 +1246,14 @@ de:
1237
1246
  unknown_application: Unbekannt
1238
1247
  error_alert:
1239
1248
  subject: "\U0001F6A8 [%{application}] %{error_type}: %{message}"
1249
+ subject_with_environment: "\U0001F6A8 [%{application} · %{environment}] %{error_type}:
1250
+ %{message}"
1240
1251
  heading: "\U0001F6A8 Fehlermeldung"
1241
1252
  labels:
1242
1253
  application: Anwendung
1243
1254
  error_type: Fehlertyp
1244
1255
  platform: Plattform
1256
+ environment: Umgebung
1245
1257
  occurred_at: Aufgetreten am
1246
1258
  user_id: Benutzer-ID
1247
1259
  ip_address: IP-Adresse
@@ -1307,6 +1319,7 @@ de:
1307
1319
  application: Anwendung
1308
1320
  error_type: Fehlertyp
1309
1321
  platform: Plattform
1322
+ environment: Umgebung
1310
1323
  occurred: Aufgetreten
1311
1324
  message: Meldung
1312
1325
  user: Benutzer
@@ -1641,6 +1654,10 @@ de:
1641
1654
  platform_chart:
1642
1655
  title: Fehler nach Plattform
1643
1656
  quick_links: 'Schnellzugriff:'
1657
+ environment_chart:
1658
+ title: Fehler nach Umgebung
1659
+ quick_links: 'Schnellzugriff:'
1660
+ unknown: Unbekannt
1644
1661
  type_chart:
1645
1662
  title: Top 10 Fehlertypen
1646
1663
  hour_chart:
@@ -249,6 +249,7 @@ en:
249
249
  # noun — languages differ on agreement between the two.
250
250
  all_apps: "All Apps"
251
251
  all_platforms: "All Platforms"
252
+ all_environments: "All Environments"
252
253
  all_types: "All Types"
253
254
  all_timeframes: "All Time"
254
255
  all_frequencies: "All Frequencies"
@@ -290,6 +291,7 @@ en:
290
291
  search: "Search: %{value}"
291
292
  application: "App: %{value}"
292
293
  platform: "Platform: %{value}"
294
+ environment: "Environment: %{value}"
293
295
  error_type: "Type: %{value}"
294
296
  severity: "Severity: %{value}"
295
297
  timeframe: "Timeframe: %{value}"
@@ -306,6 +308,7 @@ en:
306
308
  last_seen: "Last seen"
307
309
  application: "App"
308
310
  platform: "Platform"
311
+ environment: "Environment"
309
312
  select_all: "Select all errors"
310
313
 
311
314
  batch:
@@ -876,6 +879,8 @@ en:
876
879
  platform: "Platform"
877
880
  # %{platform} is a platform name (iOS, Android, Web) and is not translated.
878
881
  view_all_platform: "View all %{platform} errors"
882
+ # %{environment} is an environment name (production, staging, uat) and is not translated.
883
+ view_all_environment: "View all %{environment} errors"
879
884
 
880
885
  user: "User"
881
886
  view_user_errors: "View all errors for this user"
@@ -1484,6 +1489,8 @@ en:
1484
1489
  max_backtrace_lines: "Stack trace depth limit"
1485
1490
  sampling_rate: "Percentage of errors captured"
1486
1491
  application_name: "Name of this application (auto-detected if not set)"
1492
+ environment: "Environment errors are attributed to (Rails.env if not set)"
1493
+ notification_environments: "Only notify for these environments (all if not set)"
1487
1494
  database: "Active database for error logs"
1488
1495
  use_separate_database: "Use dedicated DB for error logs"
1489
1496
  user_model: "Model name for user associations (auto-detected if not set)"
@@ -1702,11 +1709,15 @@ en:
1702
1709
  # an exception class name and %{message} is its message — both are
1703
1710
  # verbatim error content and are not translated.
1704
1711
  subject: "🚨 [%{application}] %{error_type}: %{message}"
1712
+ # Used instead of `subject` when the error carries an environment.
1713
+ # %{environment} is an environment name and is not translated.
1714
+ subject_with_environment: "🚨 [%{application} · %{environment}] %{error_type}: %{message}"
1705
1715
  heading: "🚨 Error Alert"
1706
1716
  labels:
1707
1717
  application: "Application"
1708
1718
  error_type: "Error Type"
1709
1719
  platform: "Platform"
1720
+ environment: "Environment"
1710
1721
  occurred_at: "Occurred At"
1711
1722
  user_id: "User ID"
1712
1723
  ip_address: "IP Address"
@@ -1806,6 +1817,7 @@ en:
1806
1817
  application: "Application"
1807
1818
  error_type: "Error Type"
1808
1819
  platform: "Platform"
1820
+ environment: "Environment"
1809
1821
  occurred: "Occurred"
1810
1822
  message: "Message"
1811
1823
  user: "User"
@@ -2215,6 +2227,11 @@ en:
2215
2227
  platform_chart:
2216
2228
  title: "Errors by Platform"
2217
2229
  quick_links: "Quick Links:"
2230
+ environment_chart:
2231
+ title: "Errors by Environment"
2232
+ quick_links: "Quick Links:"
2233
+ # Label for errors captured before the environment column existed.
2234
+ unknown: "Unknown"
2218
2235
 
2219
2236
  type_chart:
2220
2237
  title: "Top 10 Error Types"
@@ -147,6 +147,7 @@ es:
147
147
  any: Cualquier gravedad
148
148
  all_apps: Todas las aplicaciones
149
149
  all_platforms: Todas las plataformas
150
+ all_environments: Todos los entornos
150
151
  all_types: Todos los tipos
151
152
  all_timeframes: Todo el periodo
152
153
  all_frequencies: Todas las frecuencias
@@ -175,6 +176,7 @@ es:
175
176
  search: 'Búsqueda: %{value}'
176
177
  application: 'Aplicación: %{value}'
177
178
  platform: 'Plataforma: %{value}'
179
+ environment: 'Entorno: %{value}'
178
180
  error_type: 'Tipo: %{value}'
179
181
  severity: 'Gravedad: %{value}'
180
182
  timeframe: 'Periodo: %{value}'
@@ -190,6 +192,7 @@ es:
190
192
  last_seen: Última aparición
191
193
  application: Aplicación
192
194
  platform: Plataforma
195
+ environment: Entorno
193
196
  select_all: Seleccionar todos los errores
194
197
  batch:
195
198
  selected:
@@ -652,6 +655,7 @@ es:
652
655
  low: BAJA
653
656
  platform: Plataforma
654
657
  view_all_platform: Ver todos los errores de %{platform}
658
+ view_all_environment: Ver todos los errores de %{environment}
655
659
  user: Usuario
656
660
  view_user_errors: Ver todos los errores de este usuario
657
661
  user_id: 'ID: %{id}'
@@ -870,8 +874,9 @@ es:
870
874
  column_ips: IP
871
875
  column_top_path: Ruta principal
872
876
  column_top_agent: Agente principal
873
- overflow_notice_html: >-
874
- No se pudieron atribuir %{count} eventos a ninguna regla porque el búfer del rastreador estaba lleno. Aumente %{option} para capturar más.
877
+ overflow_notice_html: No se pudieron atribuir %{count} eventos a ninguna regla
878
+ porque el búfer del rastreador estaba lleno. Aumente %{option} para capturar
879
+ más.
875
880
  match_type:
876
881
  blocklist: blocklist
877
882
  throttle: throttle
@@ -1112,6 +1117,9 @@ es:
1112
1117
  sampling_rate: Porcentaje de errores capturados
1113
1118
  application_name: Nombre de esta aplicación (se detecta automáticamente si
1114
1119
  no se define)
1120
+ environment: Entorno al que se atribuyen los errores (Rails.env si no se define)
1121
+ notification_environments: Notificar solo para estos entornos (todos si no
1122
+ se define)
1115
1123
  database: Base de datos activa para los registros de errores
1116
1124
  use_separate_database: Usar una base de datos dedicada para los registros
1117
1125
  de errores
@@ -1287,11 +1295,14 @@ es:
1287
1295
  unknown_application: Desconocida
1288
1296
  error_alert:
1289
1297
  subject: "\U0001F6A8 [%{application}] %{error_type}: %{message}"
1298
+ subject_with_environment: "\U0001F6A8 [%{application} · %{environment}] %{error_type}:
1299
+ %{message}"
1290
1300
  heading: "\U0001F6A8 Alerta de error"
1291
1301
  labels:
1292
1302
  application: Aplicación
1293
1303
  error_type: Tipo de error
1294
1304
  platform: Plataforma
1305
+ environment: Entorno
1295
1306
  occurred_at: Ocurrió el
1296
1307
  user_id: ID de usuario
1297
1308
  ip_address: Dirección IP
@@ -1360,6 +1371,7 @@ es:
1360
1371
  application: Aplicación
1361
1372
  error_type: Tipo de error
1362
1373
  platform: Plataforma
1374
+ environment: Entorno
1363
1375
  occurred: Ocurrió
1364
1376
  message: Mensaje
1365
1377
  user: Usuario
@@ -1696,6 +1708,10 @@ es:
1696
1708
  platform_chart:
1697
1709
  title: Errores por plataforma
1698
1710
  quick_links: 'Enlaces rápidos:'
1711
+ environment_chart:
1712
+ title: Errores por entorno
1713
+ quick_links: 'Enlaces rápidos:'
1714
+ unknown: Desconocido
1699
1715
  type_chart:
1700
1716
  title: Los 10 tipos de error más frecuentes
1701
1717
  hour_chart: