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
@@ -5,32 +5,32 @@
5
5
  <%= form_with url: resolve_error_path(error), method: :post do |f| %>
6
6
  <div class="modal-header">
7
7
  <h5 class="modal-title" id="resolveModalLabel">
8
- <i class="bi bi-check-circle"></i> Mark Error as Resolved
8
+ <i class="bi bi-check-circle"></i> <%= red_t("red.errors.modals.resolve.title") %>
9
9
  </h5>
10
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
10
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= red_t("red.common.close") %>"></button>
11
11
  </div>
12
12
  <div class="modal-body">
13
13
  <div class="mb-3">
14
- <label for="resolved_by_name" class="form-label">Your Name <span class="text-danger">*</span></label>
15
- <%= text_field_tag :resolved_by_name, nil, class: "form-control", placeholder: "e.g., John Doe", required: true %>
16
- <small class="text-muted">Who is resolving this error?</small>
14
+ <label for="resolved_by_name" class="form-label"><%= red_t("red.errors.modals.resolve.name_label") %> <span class="text-danger">*</span></label>
15
+ <%= text_field_tag :resolved_by_name, nil, class: "form-control", placeholder: red_t("red.errors.modals.name_placeholder"), required: true %>
16
+ <small class="text-muted"><%= red_t("red.errors.modals.resolve.name_hint") %></small>
17
17
  </div>
18
18
 
19
19
  <div class="mb-3">
20
- <label for="resolution_reference" class="form-label">Reference (Optional)</label>
21
- <%= text_field_tag :resolution_reference, nil, class: "form-control", placeholder: "e.g., JIRA-123, PR #456, GitHub Issue #789" %>
22
- <small class="text-muted">Link to ticket, PR, or issue</small>
20
+ <label for="resolution_reference" class="form-label"><%= red_t("red.errors.modals.resolve.reference_label") %></label>
21
+ <%= text_field_tag :resolution_reference, nil, class: "form-control", placeholder: red_t("red.errors.modals.resolve.reference_placeholder") %>
22
+ <small class="text-muted"><%= red_t("red.errors.modals.resolve.reference_hint") %></small>
23
23
  </div>
24
24
 
25
25
  <div class="mb-3">
26
- <label for="resolution_comment" class="form-label">Resolution Notes (Optional)</label>
27
- <%= text_area_tag :resolution_comment, nil, class: "form-control", rows: 4, placeholder: "Describe what was done to fix this error..." %>
28
- <small class="text-muted">What was done to resolve this error?</small>
26
+ <label for="resolution_comment" class="form-label"><%= red_t("red.errors.modals.resolve.notes_label") %></label>
27
+ <%= text_area_tag :resolution_comment, nil, class: "form-control", rows: 4, placeholder: red_t("red.errors.modals.resolve.notes_placeholder") %>
28
+ <small class="text-muted"><%= red_t("red.errors.modals.resolve.notes_hint") %></small>
29
29
  </div>
30
30
  </div>
31
31
  <div class="modal-footer">
32
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
33
- <%= submit_tag "Mark as Resolved", class: "btn btn-success", data: { action: "click->loading#click" } %>
32
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= red_t("red.errors.modals.cancel") %></button>
33
+ <%= submit_tag red_t("red.errors.modals.resolve.submit"), class: "btn btn-success", data: { action: "click->loading#click" } %>
34
34
  </div>
35
35
  <% end %>
36
36
  </div>
@@ -44,20 +44,20 @@
44
44
  <%= form_with url: assign_error_path(error), method: :post do |f| %>
45
45
  <div class="modal-header">
46
46
  <h5 class="modal-title" id="assignModalLabel">
47
- <i class="bi bi-person-plus"></i> Assign Error
47
+ <i class="bi bi-person-plus"></i> <%= red_t("red.errors.modals.assign.title") %>
48
48
  </h5>
49
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
49
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= red_t("red.common.close") %>"></button>
50
50
  </div>
51
51
  <div class="modal-body">
52
52
  <div class="mb-3">
53
- <label for="assigned_to" class="form-label">Assign To <span class="text-danger">*</span></label>
54
- <%= text_field_tag :assigned_to, nil, class: "form-control", placeholder: "e.g., John Doe", required: true, autofocus: true %>
55
- <small class="text-muted">Who should investigate this error?</small>
53
+ <label for="assigned_to" class="form-label"><%= red_t("red.errors.modals.assign.label") %> <span class="text-danger">*</span></label>
54
+ <%= text_field_tag :assigned_to, nil, class: "form-control", placeholder: red_t("red.errors.modals.name_placeholder"), required: true, autofocus: true %>
55
+ <small class="text-muted"><%= red_t("red.errors.modals.assign.hint") %></small>
56
56
  </div>
57
57
  </div>
58
58
  <div class="modal-footer">
59
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
60
- <%= submit_tag "Assign", class: "btn btn-primary", data: { action: "click->loading#click" } %>
59
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= red_t("red.errors.modals.cancel") %></button>
60
+ <%= submit_tag red_t("red.errors.modals.assign.submit"), class: "btn btn-primary", data: { action: "click->loading#click" } %>
61
61
  </div>
62
62
  <% end %>
63
63
  </div>
@@ -71,25 +71,25 @@
71
71
  <%= form_with url: update_priority_error_path(error), method: :post do |f| %>
72
72
  <div class="modal-header">
73
73
  <h5 class="modal-title" id="priorityModalLabel">
74
- <i class="bi bi-flag"></i> Update Priority
74
+ <i class="bi bi-flag"></i> <%= red_t("red.errors.modals.priority.title") %>
75
75
  </h5>
76
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
76
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= red_t("red.common.close") %>"></button>
77
77
  </div>
78
78
  <div class="modal-body">
79
79
  <div class="mb-3">
80
- <label for="priority_level" class="form-label">Priority Level <span class="text-danger">*</span></label>
80
+ <label for="priority_level" class="form-label"><%= red_t("red.errors.modals.priority.label") %> <span class="text-danger">*</span></label>
81
81
  <%= select_tag :priority_level,
82
82
  options_for_select(
83
83
  RailsErrorDashboard::ErrorLog.priority_options,
84
84
  error.priority_level
85
85
  ),
86
86
  class: "form-select", required: true %>
87
- <small class="text-muted">Current: <%= error.priority_label %></small>
87
+ <small class="text-muted"><%= red_t("red.errors.modals.priority.current", value: error.priority_label) %></small>
88
88
  </div>
89
89
  </div>
90
90
  <div class="modal-footer">
91
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
92
- <%= submit_tag "Update Priority", class: "btn btn-warning", data: { action: "click->loading#click" } %>
91
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= red_t("red.errors.modals.cancel") %></button>
92
+ <%= submit_tag red_t("red.errors.modals.priority.submit"), class: "btn btn-warning", data: { action: "click->loading#click" } %>
93
93
  </div>
94
94
  <% end %>
95
95
  </div>
@@ -103,35 +103,44 @@
103
103
  <%= form_with url: snooze_error_path(error), method: :post do |f| %>
104
104
  <div class="modal-header">
105
105
  <h5 class="modal-title" id="snoozeModalLabel">
106
- <i class="bi bi-alarm"></i> Snooze Error
106
+ <i class="bi bi-alarm"></i> <%= red_t("red.errors.modals.snooze.title") %>
107
107
  </h5>
108
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
108
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= red_t("red.common.close") %>"></button>
109
109
  </div>
110
110
  <div class="modal-body">
111
111
  <div class="mb-3">
112
- <label for="hours" class="form-label">Snooze Duration <span class="text-danger">*</span></label>
112
+ <label for="hours" class="form-label"><%= red_t("red.errors.modals.snooze.label") %> <span class="text-danger">*</span></label>
113
+ <%# The longer spans are written twice in English ("24 hours (1 day)").
114
+ The halves pluralize on different counts, so each is its own
115
+ plural key and hours_with_equivalent composes the pair. %>
116
+ <% snooze_hours = ->(count) { red_tp("red.errors.modals.snooze.hours", count: count) } %>
117
+ <% snooze_pair = lambda do |hours, unit_key, unit_count|
118
+ red_t("red.errors.modals.snooze.hours_with_equivalent",
119
+ hours: snooze_hours.call(hours),
120
+ equivalent: red_tp("red.errors.modals.snooze.#{unit_key}", count: unit_count))
121
+ end %>
113
122
  <%= select_tag :hours,
114
123
  options_for_select([
115
- ['1 hour', 1],
116
- ['4 hours', 4],
117
- ['8 hours', 8],
118
- ['24 hours (1 day)', 24],
119
- ['48 hours (2 days)', 48],
120
- ['168 hours (1 week)', 168]
124
+ [ snooze_hours.call(1), 1 ],
125
+ [ snooze_hours.call(4), 4 ],
126
+ [ snooze_hours.call(8), 8 ],
127
+ [ snooze_pair.call(24, "days", 1), 24 ],
128
+ [ snooze_pair.call(48, "days", 2), 48 ],
129
+ [ snooze_pair.call(168, "weeks", 1), 168 ]
121
130
  ], 24),
122
131
  class: "form-select", required: true %>
123
- <small class="text-muted">Hide this error from active views</small>
132
+ <small class="text-muted"><%= red_t("red.errors.modals.snooze.hint") %></small>
124
133
  </div>
125
134
 
126
135
  <div class="mb-3">
127
- <label for="reason" class="form-label">Reason (Optional)</label>
128
- <%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: "Why are you snoozing this error?" %>
129
- <small class="text-muted">Reason will be added as a comment</small>
136
+ <label for="reason" class="form-label"><%= red_t("red.errors.modals.snooze.reason_label") %></label>
137
+ <%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: red_t("red.errors.modals.snooze.reason_placeholder") %>
138
+ <small class="text-muted"><%= red_t("red.errors.modals.snooze.reason_hint") %></small>
130
139
  </div>
131
140
  </div>
132
141
  <div class="modal-footer">
133
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
134
- <%= submit_tag "Snooze", class: "btn btn-warning", data: { action: "click->loading#click" } %>
142
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= red_t("red.errors.modals.cancel") %></button>
143
+ <%= submit_tag red_t("red.errors.modals.snooze.submit"), class: "btn btn-warning", data: { action: "click->loading#click" } %>
135
144
  </div>
136
145
  <% end %>
137
146
  </div>
@@ -145,28 +154,27 @@
145
154
  <%= form_with url: mute_error_path(error), method: :post do |f| %>
146
155
  <div class="modal-header">
147
156
  <h5 class="modal-title" id="muteModalLabel">
148
- <i class="bi bi-bell-slash"></i> Mute Notifications
157
+ <i class="bi bi-bell-slash"></i> <%= red_t("red.errors.modals.mute.title") %>
149
158
  </h5>
150
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
159
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= red_t("red.common.close") %>"></button>
151
160
  </div>
152
161
  <div class="modal-body">
153
162
  <p class="text-muted">
154
- Muted errors still appear in the dashboard but will not trigger any notifications
155
- (Slack, email, Discord, PagerDuty, webhooks).
163
+ <%= red_t("red.errors.modals.mute.description") %>
156
164
  </p>
157
165
  <div class="mb-3">
158
- <label for="muted_by" class="form-label">Your Name (Optional)</label>
159
- <%= text_field_tag :muted_by, nil, class: "form-control", placeholder: "e.g., John Doe" %>
166
+ <label for="muted_by" class="form-label"><%= red_t("red.errors.modals.mute.name_label") %></label>
167
+ <%= text_field_tag :muted_by, nil, class: "form-control", placeholder: red_t("red.errors.modals.name_placeholder") %>
160
168
  </div>
161
169
  <div class="mb-3">
162
- <label for="reason" class="form-label">Reason (Optional)</label>
163
- <%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: "Why are you muting this error?" %>
164
- <small class="text-muted">Reason will be added as a comment</small>
170
+ <label for="reason" class="form-label"><%= red_t("red.errors.modals.mute.reason_label") %></label>
171
+ <%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: red_t("red.errors.modals.mute.reason_placeholder") %>
172
+ <small class="text-muted"><%= red_t("red.errors.modals.mute.reason_hint") %></small>
165
173
  </div>
166
174
  </div>
167
175
  <div class="modal-footer">
168
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
169
- <%= submit_tag "Mute Notifications", class: "btn btn-secondary", data: { action: "click->loading#click" } %>
176
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= red_t("red.errors.modals.cancel") %></button>
177
+ <%= submit_tag red_t("red.errors.modals.mute.submit"), class: "btn btn-secondary", data: { action: "click->loading#click" } %>
170
178
  </div>
171
179
  <% end %>
172
180
  </div>
@@ -1,64 +1,73 @@
1
1
  <% if RailsErrorDashboard.configuration.enable_occurrence_patterns %>
2
2
  <%
3
- # Display occurrence patterns and bursts for an error
4
- pattern_data = @error.occurrence_pattern(days: 30)
5
- bursts = @error.error_bursts(days: 7)
3
+ # Display occurrence patterns and bursts for an error.
4
+ #
5
+ # The windows and the peak threshold are named because each is cited in
6
+ # prose as well as passed to a query. Written twice they drift, and the
7
+ # copy that drifts is the one the user reads.
8
+ pattern_window_days = 30
9
+ burst_window_days = 7
10
+ peak_hour_multiplier = 2
11
+ shown_bursts = 10
12
+
13
+ pattern_data = @error.occurrence_pattern(days: pattern_window_days)
14
+ bursts = @error.error_bursts(days: burst_window_days)
6
15
  %>
7
16
 
8
17
  <% if pattern_data.present? && pattern_data[:total_errors] > 0 %>
9
18
  <div class="card mb-4" id="section-occurrence-patterns">
10
19
  <div class="card-header">
11
20
  <h5 class="mb-0">
12
- <i class="bi bi-graph-up"></i> Occurrence Patterns
13
- <small class="text-muted">(Last 30 days)</small>
21
+ <i class="bi bi-graph-up"></i> <%= red_t("red.errors.patterns.title") %>
22
+ <small class="text-muted"><%= red_t("red.errors.patterns.window", days: pattern_window_days) %></small>
14
23
  </h5>
15
24
  </div>
16
25
  <div class="card-body">
17
26
  <!-- Pattern Type Summary -->
18
27
  <div class="row mb-4">
19
28
  <div class="col-md-6">
20
- <h6 class="text-muted mb-2">Pattern Type</h6>
29
+ <h6 class="text-muted mb-2"><%= red_t("red.errors.patterns.pattern_type") %></h6>
21
30
  <% case pattern_data[:pattern_type] %>
22
31
  <% when :business_hours %>
23
32
  <span class="badge bg-info fs-6">
24
- <i class="bi bi-clock"></i> Business Hours Pattern
33
+ <i class="bi bi-clock"></i> <%= red_t("red.errors.patterns.types.business_hours.label") %>
25
34
  </span>
26
35
  <p class="small text-muted mt-2">
27
- Errors peak during business hours (9am-5pm), suggesting user activity correlation
36
+ <%= red_t("red.errors.patterns.types.business_hours.description") %>
28
37
  </p>
29
38
  <% when :night %>
30
39
  <span class="badge bg-dark fs-6">
31
- <i class="bi bi-moon-fill"></i> Night Pattern
40
+ <i class="bi bi-moon-fill"></i> <%= red_t("red.errors.patterns.types.night.label") %>
32
41
  </span>
33
42
  <p class="small text-muted mt-2">
34
- Errors peak during night hours (midnight-6am), possibly from background jobs or automated tasks
43
+ <%= red_t("red.errors.patterns.types.night.description") %>
35
44
  </p>
36
45
  <% when :weekend %>
37
46
  <span class="badge bg-primary fs-6">
38
- <i class="bi bi-calendar-week"></i> Weekend Pattern
47
+ <i class="bi bi-calendar-week"></i> <%= red_t("red.errors.patterns.types.weekend.label") %>
39
48
  </span>
40
49
  <p class="small text-muted mt-2">
41
- Most errors occur on weekends, suggesting weekend-specific usage patterns
50
+ <%= red_t("red.errors.patterns.types.weekend.description") %>
42
51
  </p>
43
52
  <% when :uniform %>
44
53
  <span class="badge bg-secondary fs-6">
45
- <i class="bi bi-distribute-horizontal"></i> Uniform Pattern
54
+ <i class="bi bi-distribute-horizontal"></i> <%= red_t("red.errors.patterns.types.uniform.label") %>
46
55
  </span>
47
56
  <p class="small text-muted mt-2">
48
- Errors distributed evenly throughout the day with no clear pattern
57
+ <%= red_t("red.errors.patterns.types.uniform.description") %>
49
58
  </p>
50
59
  <% else %>
51
60
  <span class="badge bg-light text-dark fs-6">
52
- <i class="bi bi-question-circle"></i> No Pattern Detected
61
+ <i class="bi bi-question-circle"></i> <%= red_t("red.errors.patterns.types.none.label") %>
53
62
  </span>
54
63
  <p class="small text-muted mt-2">
55
- Not enough data to identify a clear occurrence pattern
64
+ <%= red_t("red.errors.patterns.types.none.description") %>
56
65
  </p>
57
66
  <% end %>
58
67
  </div>
59
68
 
60
69
  <div class="col-md-6">
61
- <h6 class="text-muted mb-2">Pattern Strength</h6>
70
+ <h6 class="text-muted mb-2"><%= red_t("red.errors.patterns.strength") %></h6>
62
71
  <div class="d-flex align-items-center">
63
72
  <div class="progress flex-grow-1" style="height: 25px;">
64
73
  <div class="progress-bar <%= pattern_data[:pattern_strength] >= 0.7 ? 'bg-success' : pattern_data[:pattern_strength] >= 0.4 ? 'bg-warning' : 'bg-secondary' %>"
@@ -71,11 +80,11 @@
71
80
  </div>
72
81
  <p class="small text-muted mt-2">
73
82
  <% if pattern_data[:pattern_strength] >= 0.7 %>
74
- <strong>Strong pattern</strong> - Errors highly concentrated in specific hours
83
+ <strong><%= red_t("red.errors.patterns.strengths.strong.label") %></strong> - <%= red_t("red.errors.patterns.strengths.strong.description") %>
75
84
  <% elsif pattern_data[:pattern_strength] >= 0.4 %>
76
- <strong>Moderate pattern</strong> - Some concentration in specific hours
85
+ <strong><%= red_t("red.errors.patterns.strengths.moderate.label") %></strong> - <%= red_t("red.errors.patterns.strengths.moderate.description") %>
77
86
  <% else %>
78
- <strong>Weak pattern</strong> - Errors distributed fairly evenly
87
+ <strong><%= red_t("red.errors.patterns.strengths.weak.label") %></strong> - <%= red_t("red.errors.patterns.strengths.weak.description") %>
79
88
  <% end %>
80
89
  </p>
81
90
  </div>
@@ -83,7 +92,7 @@
83
92
 
84
93
  <!-- Hourly Distribution Heatmap -->
85
94
  <% if pattern_data[:hourly_distribution].present? %>
86
- <h6 class="text-muted mb-3">Hourly Distribution Heatmap</h6>
95
+ <h6 class="text-muted mb-3"><%= red_t("red.errors.patterns.heatmap_title") %></h6>
87
96
  <div class="mb-4">
88
97
  <div class="d-flex flex-wrap">
89
98
  <% (0..23).each do |hour| %>
@@ -94,7 +103,7 @@
94
103
  <div class="text-center m-1 heatmap-cell-wrapper" style="width: 45px;">
95
104
  <div class="rounded p-2 heatmap-cell <%= is_peak ? 'border border-danger border-2' : '' %>"
96
105
  style="background-color: rgba(220, 53, 69, <%= intensity / 100.0 %>); min-height: 50px; display: flex; align-items: center; justify-content: center;"
97
- title="<%= hour %>:00 - <%= count %> errors">
106
+ title="<%= red_tp("red.errors.patterns.heatmap_cell", count: count, hour: "#{hour}:00") %>">
98
107
  <small class="fw-bold heatmap-count <%= intensity > 50 ? 'text-white' : 'text-dark' %>">
99
108
  <%= count %>
100
109
  </small>
@@ -105,7 +114,7 @@
105
114
  </div>
106
115
  <div class="mt-2 small text-muted">
107
116
  <i class="bi bi-info-circle"></i>
108
- Darker cells = more errors. Red borders indicate peak hours (>2x average).
117
+ <%= red_t("red.errors.patterns.heatmap_legend", multiplier: peak_hour_multiplier) %>
109
118
  </div>
110
119
  </div>
111
120
  <% end %>
@@ -113,11 +122,11 @@
113
122
  <!-- Peak Hours -->
114
123
  <% if pattern_data[:peak_hours].present? %>
115
124
  <div class="alert alert-info">
116
- <strong><i class="bi bi-clock-fill"></i> Peak Hours:</strong>
125
+ <strong><i class="bi bi-clock-fill"></i> <%= red_t("red.errors.patterns.peak_hours") %></strong>
117
126
  <%= pattern_data[:peak_hours].map { |h| "#{h}:00" }.join(", ") %>
118
127
  <br>
119
128
  <small>
120
- These hours have more than 2x the average error rate. Consider monitoring deployments or load during these times.
129
+ <%= red_t("red.errors.patterns.peak_hours_hint") %>
121
130
  </small>
122
131
  </div>
123
132
  <% end %>
@@ -130,48 +139,49 @@
130
139
  <div class="card mb-4">
131
140
  <div class="card-header">
132
141
  <h5 class="mb-0">
133
- <i class="bi bi-lightning-charge-fill text-warning"></i> Error Bursts
142
+ <i class="bi bi-lightning-charge-fill text-warning"></i> <%= red_t("red.errors.patterns.bursts_title") %>
134
143
  <span class="badge bg-warning text-dark"><%= bursts.count %></span>
135
- <small class="text-muted">(Last 7 days)</small>
144
+ <small class="text-muted"><%= red_t("red.errors.patterns.bursts_window", days: burst_window_days) %></small>
136
145
  </h5>
137
146
  </div>
138
147
  <div class="card-body">
139
148
  <p class="text-muted">
140
- Error bursts are rapid sequences where multiple errors occur within minutes. This may indicate cascading failures or load spikes.
149
+ <%= red_t("red.errors.patterns.bursts_hint") %>
141
150
  </p>
142
151
 
143
152
  <div class="table-responsive">
144
153
  <table class="table table-hover">
145
154
  <thead>
146
155
  <tr>
147
- <th>Start Time</th>
148
- <th>Duration</th>
149
- <th>Error Count</th>
150
- <th>Intensity</th>
156
+ <th><%= red_t("red.errors.patterns.column_start_time") %></th>
157
+ <th><%= red_t("red.errors.patterns.column_duration") %></th>
158
+ <th><%= red_t("red.errors.patterns.column_error_count") %></th>
159
+ <th><%= red_t("red.errors.patterns.column_intensity") %></th>
151
160
  </tr>
152
161
  </thead>
153
162
  <tbody>
154
- <% bursts.first(10).each do |burst| %>
163
+ <% bursts.first(shown_bursts).each do |burst| %>
155
164
  <tr>
156
165
  <td>
157
- <%= burst[:start_time].strftime("%b %d, %Y %I:%M %p") %>
166
+ <%= burst[:start_time].strftime(red_time_format(:full)) %>
158
167
  </td>
159
168
  <td>
160
169
  <% duration_minutes = (burst[:duration_seconds] / 60.0).round(1) %>
161
- <%= duration_minutes %> min
162
- <small class="text-muted">(<%= burst[:duration_seconds].round %>s)</small>
170
+ <%= red_t("red.errors.patterns.burst_duration", minutes: duration_minutes) %>
171
+ <small class="text-muted"><%= red_t("red.errors.patterns.burst_duration_seconds", seconds: burst[:duration_seconds].round) %></small>
163
172
  </td>
164
173
  <td>
165
- <strong><%= burst[:error_count] %></strong> errors
174
+ <%= red_tp("red.errors.patterns.burst_errors_html", count: burst[:error_count],
175
+ value: tag.strong(burst[:error_count])) %>
166
176
  </td>
167
177
  <td>
168
178
  <% case burst[:burst_intensity] %>
169
179
  <% when :high %>
170
- <span class="badge bg-danger">High (20+ errors)</span>
180
+ <span class="badge bg-danger"><%= red_t("red.errors.patterns.intensity_high") %></span>
171
181
  <% when :medium %>
172
- <span class="badge bg-warning text-dark">Medium (10-19 errors)</span>
182
+ <span class="badge bg-warning text-dark"><%= red_t("red.errors.patterns.intensity_medium") %></span>
173
183
  <% when :low %>
174
- <span class="badge bg-info">Low (5-9 errors)</span>
184
+ <span class="badge bg-info"><%= red_t("red.errors.patterns.intensity_low") %></span>
175
185
  <% end %>
176
186
  </td>
177
187
  </tr>
@@ -180,20 +190,20 @@
180
190
  </table>
181
191
  </div>
182
192
 
183
- <% if bursts.count > 10 %>
193
+ <% if bursts.count > shown_bursts %>
184
194
  <p class="text-muted small mb-0">
185
- Showing 10 of <%= bursts.count %> detected bursts
195
+ <%= red_t("red.errors.patterns.bursts_truncated", shown: shown_bursts, total: bursts.count) %>
186
196
  </p>
187
197
  <% end %>
188
198
 
189
199
  <div class="alert alert-warning mt-3">
190
- <strong><i class="bi bi-exclamation-triangle"></i> Recommendation:</strong>
191
- Multiple error bursts detected. Investigate:
200
+ <strong><i class="bi bi-exclamation-triangle"></i> <%= red_t("red.errors.patterns.recommendation_label") %></strong>
201
+ <%= red_t("red.errors.patterns.recommendation_intro") %>
192
202
  <ul class="mb-0 mt-2">
193
- <li>Recent deployments or configuration changes around burst times</li>
194
- <li>Load spikes or traffic patterns</li>
195
- <li>Cascading failures from related errors</li>
196
- <li>Background job failures or retry storms</li>
203
+ <li><%= red_t("red.errors.patterns.recommendation_deploys") %></li>
204
+ <li><%= red_t("red.errors.patterns.recommendation_load") %></li>
205
+ <li><%= red_t("red.errors.patterns.recommendation_cascades") %></li>
206
+ <li><%= red_t("red.errors.patterns.recommendation_jobs") %></li>
197
207
  </ul>
198
208
  </div>
199
209
  </div>
@@ -203,7 +213,7 @@
203
213
  <% if (pattern_data.blank? || pattern_data[:total_errors] == 0) && bursts.blank? %>
204
214
  <div class="alert alert-info">
205
215
  <i class="bi bi-info-circle"></i>
206
- No occurrence patterns or bursts detected. More data may be needed for pattern analysis.
216
+ <%= red_t("red.errors.patterns.empty") %>
207
217
  </div>
208
218
  <% end %>
209
219
  <% end %>
@@ -1,55 +1,58 @@
1
1
  <!-- Request Context -->
2
- <% cache [error, 'request_context_v3'] do %>
2
+ <%# The locale is part of the cache key: this fragment contains translated
3
+ text, and without it the first locale to render an error would be
4
+ served to every other one. %>
5
+ <% cache [error, 'request_context_v5', red_locale] do %>
3
6
  <div class="card mb-4" id="section-request-context">
4
7
  <div class="card-header">
5
- <h5 class="mb-0"><i class="bi bi-globe"></i> Request Context</h5>
8
+ <h5 class="mb-0"><i class="bi bi-globe"></i> <%= red_t("red.errors.request_context.title") %></h5>
6
9
  </div>
7
10
  <div class="card-body">
8
11
  <table class="table table-sm">
9
12
  <tr>
10
- <th width="200">Request URL:</th>
13
+ <th width="200"><%= red_t("red.errors.request_context.url") %></th>
11
14
  <td>
12
15
  <% if error.respond_to?(:http_method) && error.http_method.present? %>
13
16
  <span class="badge bg-primary me-1"><%= error.http_method %></span>
14
17
  <% end %>
15
- <code><%= error.request_url || 'N/A' %></code>
18
+ <code><%= error.request_url || red_t("red.common.not_available") %></code>
16
19
  </td>
17
20
  </tr>
18
21
  <% if error.respond_to?(:hostname) && error.hostname.present? %>
19
22
  <tr>
20
- <th>Hostname:</th>
23
+ <th><%= red_t("red.errors.request_context.hostname") %></th>
21
24
  <td><code><%= error.hostname %></code></td>
22
25
  </tr>
23
26
  <% end %>
24
27
  <% if error.respond_to?(:content_type) && error.content_type.present? %>
25
28
  <tr>
26
- <th>Content Type:</th>
29
+ <th><%= red_t("red.errors.request_context.content_type") %></th>
27
30
  <td><code><%= error.content_type %></code></td>
28
31
  </tr>
29
32
  <% end %>
30
33
  <% if error.respond_to?(:request_duration_ms) && error.request_duration_ms.present? %>
31
34
  <tr>
32
- <th>Request Duration:</th>
35
+ <th><%= red_t("red.errors.request_context.duration") %></th>
33
36
  <td>
34
37
  <% duration = error.request_duration_ms %>
35
38
  <span class="badge bg-<%= duration > 5000 ? 'danger' : duration > 1000 ? 'warning' : 'success' %>">
36
- <%= duration > 1000 ? "#{(duration / 1000.0).round(1)}s" : "#{duration}ms" %>
39
+ <%= duration > 1000 ? red_t("red.errors.request_context.duration_seconds", value: (duration / 1000.0).round(1)) : red_t("red.errors.request_context.duration_ms", value: duration) %>
37
40
  </span>
38
41
  </td>
39
42
  </tr>
40
43
  <% end %>
41
44
  <tr>
42
- <th>Request Params:</th>
45
+ <th><%= red_t("red.errors.request_context.params") %></th>
43
46
  <td>
44
47
  <% if error.request_params.present? %>
45
48
  <pre class="mb-0"><code><%= JSON.pretty_generate(JSON.parse(error.request_params)) rescue error.request_params %></code></pre>
46
49
  <% else %>
47
- <span class="text-muted">N/A</span>
50
+ <span class="text-muted"><%= red_t("red.common.not_available") %></span>
48
51
  <% end %>
49
52
  </td>
50
53
  </tr>
51
54
  <tr>
52
- <th>User Agent:</th>
55
+ <th><%= red_t("red.errors.request_context.user_agent") %></th>
53
56
  <td>
54
57
  <% if error.user_agent.present? %>
55
58
  <% ua_info = parse_user_agent(error.user_agent) %>
@@ -64,39 +67,39 @@
64
67
  </div>
65
68
  <details>
66
69
  <summary class="text-muted" style="cursor: pointer;">
67
- <small>Raw User Agent</small>
70
+ <small><%= red_t("red.errors.request_context.raw_user_agent") %></small>
68
71
  </summary>
69
72
  <small class="text-muted"><%= error.user_agent %></small>
70
73
  </details>
71
74
  <% else %>
72
- <span class="text-muted">N/A</span>
75
+ <span class="text-muted"><%= red_t("red.common.not_available") %></span>
73
76
  <% end %>
74
77
  </td>
75
78
  </tr>
76
79
  <tr>
77
- <th>IP Address:</th>
78
- <td><code><%= error.ip_address || 'N/A' %></code></td>
80
+ <th><%= red_t("red.errors.request_context.ip_address") %></th>
81
+ <td><code><%= error.ip_address || red_t("red.common.not_available") %></code></td>
79
82
  </tr>
80
83
  <% curl_cmd = RailsErrorDashboard::Services::CurlGenerator.call(error) %>
81
84
  <% rspec_cmd = RailsErrorDashboard::Services::RspecGenerator.call(error) %>
82
85
  <% if curl_cmd.present? || rspec_cmd.present? %>
83
86
  <tr>
84
- <th>Replay Request:</th>
87
+ <th><%= red_t("red.errors.request_context.replay") %></th>
85
88
  <td>
86
89
  <% if curl_cmd.present? %>
87
90
  <button type="button" class="btn btn-sm btn-outline-primary me-2"
88
91
  data-red-action="copy"
89
92
  data-red-copy-text="<%= h curl_cmd %>"
90
- title="Copy curl command to clipboard">
91
- <i class="bi bi-terminal"></i> Copy as curl
93
+ title="<%= red_t("red.errors.request_context.copy_curl_title") %>">
94
+ <i class="bi bi-terminal"></i> <%= red_t("red.errors.request_context.copy_curl") %>
92
95
  </button>
93
96
  <% end %>
94
97
  <% if rspec_cmd.present? %>
95
98
  <button type="button" class="btn btn-sm btn-outline-success"
96
99
  data-red-action="copy"
97
100
  data-red-copy-text="<%= h rspec_cmd %>"
98
- title="Copy RSpec request spec to clipboard">
99
- <i class="bi bi-code-slash"></i> Copy as RSpec
101
+ title="<%= red_t("red.errors.request_context.copy_rspec_title") %>">
102
+ <i class="bi bi-code-slash"></i> <%= red_t("red.errors.request_context.copy_rspec") %>
100
103
  </button>
101
104
  <% end %>
102
105
  </td>