rails_error_dashboard 0.9.0 → 0.10.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/rails_error_dashboard/errors_controller.rb +6 -0
  3. data/app/helpers/rails_error_dashboard/i18n_helper.rb +26 -0
  4. data/app/jobs/rails_error_dashboard/rack_attack_flush_job.rb +7 -3
  5. data/app/models/rails_error_dashboard/rack_attack_event.rb +6 -2
  6. data/app/views/layouts/rails_error_dashboard.html.erb +55 -0
  7. data/app/views/rails_error_dashboard/errors/analytics.html.erb +7 -12
  8. data/app/views/rails_error_dashboard/errors/correlation.html.erb +4 -4
  9. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +1 -1
  10. data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +36 -3
  11. data/config/locales/de.yml +5 -0
  12. data/config/locales/en.yml +7 -0
  13. data/config/locales/es.yml +5 -0
  14. data/config/locales/fr.yml +5 -0
  15. data/config/locales/it.yml +5 -0
  16. data/config/locales/ja.yml +5 -0
  17. data/config/locales/pl.yml +5 -0
  18. data/config/locales/pt-BR.yml +5 -0
  19. data/config/locales/ru.yml +5 -0
  20. data/config/locales/uk.yml +5 -0
  21. data/config/locales/zh-CN.yml +5 -0
  22. data/db/migrate/20260824000001_add_user_agent_to_rack_attack_events.rb +19 -0
  23. data/lib/generators/rails_error_dashboard/install/install_generator.rb +12 -0
  24. data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +12 -3
  25. data/lib/rails_error_dashboard/configuration.rb +13 -3
  26. data/lib/rails_error_dashboard/engine.rb +7 -0
  27. data/lib/rails_error_dashboard/queries/rack_attack_summary.rb +31 -5
  28. data/lib/rails_error_dashboard/services/ai_agent_classifier.rb +154 -0
  29. data/lib/rails_error_dashboard/services/rack_attack_tracker.rb +91 -9
  30. data/lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb +40 -2
  31. data/lib/rails_error_dashboard/version.rb +1 -1
  32. data/lib/rails_error_dashboard.rb +1 -0
  33. metadata +15 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f85952e5800c354ae158411afbec605cddbc5e19595e8e94620d1b13fc17894b
4
- data.tar.gz: 1af4bb9352a7fcffc10fc6b913b896ca2fd75198d31e044ff156b76cb52f6afc
3
+ metadata.gz: 7065559d38e3152511024cd4a44698c3b215cc6cd60e4917d52cf4d893180b6c
4
+ data.tar.gz: 0d9237d4e89f4cb08392aecfe109dbb38bc1aaf673e834d827441bebf84a6e3f
5
5
  SHA512:
6
- metadata.gz: a6bc19522b5797095df0545fd6eca1b2552bfcd693ebb4363a17e8076ad784654340a70d1e9591f2ab3fc17a5fc9be10e65efa1b0211b329203c639b085b37a7
7
- data.tar.gz: c0885c4d27dde62b2439a40dac82acdee468f34ce9dd3d188b2f589bbceedab680c746ec7793e894cb3d5dacb7aff68908139cf90d655dc9790c0aea503ef784
6
+ metadata.gz: 996b72e8406365ca4ee064464faa785b7a43dfa5f643ed9f2ca3be39d4d98b3dd13ed6426a1dbef3eb53fad3d6433d89b70b3f8a3d8e40434aad1e882cff0eb1
7
+ data.tar.gz: 4814ed8949397ce5ca0cd27cf4e9f52f5ba2e5bf243eff411eb0e7c80539fef978c0dc87505c02bb0e2a259725a8fec97a5897c62073eae2a9142c0f0b2c2ea8
@@ -535,6 +535,12 @@ module RailsErrorDashboard
535
535
  @unique_rules = all_events.size
536
536
  @total_events = all_events.sum { |e| e[:count] }
537
537
  @unique_ips = all_events.flat_map { |e| e[:ips] }.uniq.size
538
+ # Events matched by a recognised AI agent. Counts requests, not addresses —
539
+ # one agent rotates through many IPs, so unique_ips overstates it (#170).
540
+ @ai_events = all_events.sum { |e| e[:ai_count].to_i }
541
+ # Counts the tracker's LRU eviction dropped; shown so the totals above are
542
+ # never silently understated.
543
+ @overflow_count = result[:overflow_count].to_i
538
544
 
539
545
  @pagy, @events = pagy(:offset, all_events, limit: params[:per_page] || 25)
540
546
  end
@@ -150,5 +150,31 @@ module RailsErrorDashboard
150
150
  rescue StandardError
151
151
  "%B %d, %Y %I:%M:%S %p"
152
152
  end
153
+
154
+ # A finished, localized date string for somewhere the browser will not
155
+ # re-render it.
156
+ #
157
+ # Almost every timestamp on the dashboard goes out as a <span data-format>
158
+ # that formatDateTime() localizes client-side. Chart labels cannot: they
159
+ # are serialized into a JS array and handed to Chart.js as plain strings,
160
+ # so whatever the server writes is what the axis shows. Calling strftime
161
+ # directly there is what put English month names on every locale's charts
162
+ # (#178) — Ruby's strftime is not locale-aware.
163
+ #
164
+ # Same formatter the mailers and the Slack/Discord payloads use, so a chart
165
+ # axis and an email agree about what a date looks like.
166
+ #
167
+ # @param time [Time, Date, nil]
168
+ # @param pattern [String] a strftime pattern
169
+ # @return [String] "" for nil — an axis label must never read "undefined"
170
+ def red_chart_date(time, pattern)
171
+ return "" if time.nil?
172
+
173
+ time = time.to_time if time.respond_to?(:to_time) && !time.is_a?(Time)
174
+ Services::LocalizedTimeFormatter.call(time, pattern: pattern, locale: red_locale)
175
+ rescue StandardError
176
+ # A chart with an English axis beats a chart that fails to render.
177
+ time.respond_to?(:strftime) ? time.strftime(pattern) : time.to_s
178
+ end
153
179
  end
154
180
  end
@@ -22,9 +22,13 @@ module RailsErrorDashboard
22
22
  # Mode 1: Persist provided snapshot (dispatched from tracker flush)
23
23
  Commands::FlushRackAttackEvents.call(counts: counts)
24
24
  else
25
- # Mode 2: Flush current thread's buffer (scheduled cron safety net).
26
- # sync: true because we are already off the request path.
27
- Services::RackAttackTracker.flush!(sync: true)
25
+ # Mode 2: Flush EVERY live thread's buffer (scheduled cron safety net).
26
+ #
27
+ # This used to call flush!, which only ever sees Thread.current — the
28
+ # job worker's own buffer, which is always empty. The counts live on the
29
+ # Puma threads that served the requests, so the documented safety net
30
+ # swept nothing. flush_all_threads! is what makes the promise true.
31
+ Services::RackAttackTracker.flush_all_threads!
28
32
  end
29
33
  end
30
34
  end
@@ -12,8 +12,12 @@ module RailsErrorDashboard
12
12
  class RackAttackEvent < ErrorLogsRecord
13
13
  self.table_name = "rails_error_dashboard_rack_attack_events"
14
14
 
15
- # Event types emitted by Rack::Attack (v5.0+)
16
- MATCH_TYPES = %w[throttle blocklist track safelist].freeze
15
+ # Event types emitted by Rack::Attack (v5.0+), plus "overflow" — not a
16
+ # Rack::Attack event at all, but a synthetic bucket holding counts dropped
17
+ # by the tracker's LRU eviction so totals stay truthful. Overflow rows are
18
+ # excluded from the per-rule listing and surfaced separately.
19
+ MATCH_TYPES = %w[throttle blocklist track safelist overflow].freeze
20
+ OVERFLOW_MATCH_TYPE = "overflow"
17
21
 
18
22
  belongs_to :application, optional: true
19
23
 
@@ -1751,6 +1751,51 @@ document.addEventListener('DOMContentLoaded', function() {
1751
1751
  .replace('%H',pad(h)).replace('%I',pad(h12)).replace('%M',pad(mi)).replace('%S',pad(s)).replace('%p',ampm).replace('%P',ampm.toLowerCase());
1752
1752
  }
1753
1753
 
1754
+ // Chart.js renders date axes through chartjs-adapter-date-fns, whose bundle
1755
+ // ships English locale data only. Loading eleven date-fns locales from a CDN
1756
+ // to fix that would be absurd when RED already carries localized month and
1757
+ // day names in red.js.* for formatDateTime(). So the adapter's format() is
1758
+ // redirected through the same vocabulary the rest of the dashboard uses, and
1759
+ // a chart axis, a page timestamp and an email now agree.
1760
+ //
1761
+ // Only the tokens the adapter's own format table can emit are mapped:
1762
+ // datetime "MMM d, yyyy, h:mm:ss aaaa" millisecond "h:mm:ss.SSS aaaa"
1763
+ // second "h:mm:ss aaaa" minute "h:mm aaaa" hour "ha" day "MMM d"
1764
+ // week "PP" month "MMM yyyy" quarter "qqq - yyyy" year "yyyy"
1765
+ // Anything else — a caller-supplied pattern, a future adapter version — is
1766
+ // handed back to the original implementation rather than half-translated.
1767
+ function installRedChartDateAdapter() {
1768
+ if (typeof Chart === 'undefined' || !Chart._adapters || !Chart._adapters._date) return;
1769
+ var proto = Chart._adapters._date.prototype;
1770
+ if (!proto || typeof proto.format !== 'function' || proto._redPatched) return;
1771
+
1772
+ // date-fns token -> strftime, for the patterns the adapter actually uses.
1773
+ var TOKENS = {
1774
+ 'MMM d, yyyy, h:mm:ss aaaa': '%b %d, %Y, %I:%M:%S %p',
1775
+ 'h:mm:ss.SSS aaaa': '%I:%M:%S %p',
1776
+ 'h:mm:ss aaaa': '%I:%M:%S %p',
1777
+ 'h:mm aaaa': '%I:%M %p',
1778
+ 'ha': '%I %p',
1779
+ 'MMM d': '%b %d',
1780
+ 'PP': '%b %d, %Y',
1781
+ 'MMM yyyy': '%b %Y',
1782
+ 'yyyy': '%Y'
1783
+ };
1784
+
1785
+ var original = proto.format;
1786
+ proto.format = function(time, fmt) {
1787
+ try {
1788
+ var strf = TOKENS[fmt];
1789
+ if (strf) return formatDateTime(new Date(time), strf);
1790
+ } catch (e) {
1791
+ // Fall through — a chart with English axes beats a chart that throws.
1792
+ }
1793
+ return original.call(this, time, fmt);
1794
+ };
1795
+ proto._redPatched = true;
1796
+ }
1797
+
1798
+
1754
1799
  function getTimezoneAbbreviation(date) {
1755
1800
  // RED's locale set and the browser's Intl data need not agree, so an
1756
1801
  // unsupported tag throws RangeError here rather than degrading. Catch it
@@ -1801,6 +1846,16 @@ document.addEventListener('DOMContentLoaded', function() {
1801
1846
  }
1802
1847
 
1803
1848
  convertToLocalTime();
1849
+
1850
+ // Localize Chart.js date axes. Kept out of the block above on purpose: that
1851
+ // region is extracted and run in bare node by js_date_parity_spec, where
1852
+ // `document` is a stub, so it must contain definitions only — no top-level
1853
+ // browser calls. The adapter is patched on the prototype, so charts drawn
1854
+ // later pick it up; chartkick:load re-asserts for page scripts whose own
1855
+ // DOMContentLoaded handler beat this one, and _redPatched makes that a no-op.
1856
+ installRedChartDateAdapter();
1857
+ document.addEventListener('chartkick:load', installRedChartDateAdapter);
1858
+
1804
1859
  if (typeof Turbo !== 'undefined') {
1805
1860
  document.addEventListener('turbo:load', convertToLocalTime);
1806
1861
  document.addEventListener('turbo:frame-load', convertToLocalTime);
@@ -75,8 +75,6 @@
75
75
  curve: false,
76
76
  points: true,
77
77
  height: "300px",
78
- xtitle: "Date",
79
- ytitle: "Number of Errors",
80
78
  library: {
81
79
  scales: {
82
80
  x: {
@@ -172,18 +170,19 @@
172
170
  new Chartkick.BarChart("errors-by-type-chart", <%= raw @errors_by_type.to_json %>, {
173
171
  color: "#EF4444",
174
172
  height: "400px",
175
- xtitle: "Error Type",
176
- ytitle: "Count",
177
173
  library: {
178
174
  scales: {
175
+ // Chartkick's BarChart is horizontal (it sets indexAxis:"y"),
176
+ // so the categories sit on y and the counts on x — the
177
+ // opposite of the ColumnCharts on this page. #178.
179
178
  x: {
180
179
  ticks: { color: colors.textColor },
181
- title: { color: colors.textColor, display: true, text: '<%= red_js_t("red.ui_js.charts.axis_error_type") %>' },
180
+ title: { color: colors.textColor, display: true, text: '<%= red_js_t("red.ui_js.charts.axis_count") %>' },
182
181
  grid: { color: colors.gridColor }
183
182
  },
184
183
  y: {
185
184
  ticks: { color: colors.textColor },
186
- title: { color: colors.textColor, display: true, text: '<%= red_js_t("red.ui_js.charts.axis_count") %>' },
185
+ title: { color: colors.textColor, display: true, text: '<%= red_js_t("red.ui_js.charts.axis_error_type") %>' },
187
186
  grid: { color: colors.gridColor }
188
187
  }
189
188
  },
@@ -214,8 +213,6 @@
214
213
  new Chartkick.ColumnChart("errors-by-hour-chart", <%= raw @errors_by_hour.to_json %>, {
215
214
  color: "#DC2626",
216
215
  height: "300px",
217
- xtitle: "Hour",
218
- ytitle: "Number of Errors",
219
216
  library: {
220
217
  scales: {
221
218
  x: {
@@ -422,8 +419,6 @@
422
419
  new Chartkick.ColumnChart("errors-by-version-chart", versionData, {
423
420
  colors: ["#8B5CF6", "#EF4444", "#F59E0B", "#10B981"],
424
421
  height: "300px",
425
- xtitle: "Version",
426
- ytitle: "Error Count",
427
422
  library: window.getChartLibraryOptions()
428
423
  });
429
424
  });
@@ -558,7 +553,7 @@
558
553
  const colors = window.getChartColors();
559
554
  new Chartkick.BarChart("mttr-by-platform-chart",
560
555
  <%= raw @mttr_by_platform.to_json %>, {
561
- suffix: " hours",
556
+ suffix: '<%= red_js_t("red.ui_js.charts.suffix_hours") %>',
562
557
  height: "200px",
563
558
  colors: ["#8B5CF6"],
564
559
  library: window.getChartLibraryOptions()
@@ -611,7 +606,7 @@
611
606
  const colors = window.getChartColors();
612
607
  new Chartkick.LineChart("mttr-trend-chart",
613
608
  <%= raw @mttr_stats[:mttr_trend].to_json %>, {
614
- suffix: " hours",
609
+ suffix: '<%= red_js_t("red.ui_js.charts.suffix_hours") %>',
615
610
  height: "250px",
616
611
  colors: ["#10B981"],
617
612
  curve: false,
@@ -36,8 +36,8 @@
36
36
  <div class="display-6"><%= @period_comparison[:current_period][:count] %></div>
37
37
  <small class="text-muted">
38
38
  <%= red_t("red.analytics.correlation.trend_analysis.period_range",
39
- start: @period_comparison[:current_period][:start].strftime("%b %d"),
40
- end: @period_comparison[:current_period][:end].strftime("%b %d")) %>
39
+ start: red_chart_date(@period_comparison[:current_period][:start], "%b %d"),
40
+ end: red_chart_date(@period_comparison[:current_period][:end], "%b %d")) %>
41
41
  </small>
42
42
  </div>
43
43
  <div class="col-md-4">
@@ -45,8 +45,8 @@
45
45
  <div class="display-6"><%= @period_comparison[:previous_period][:count] %></div>
46
46
  <small class="text-muted">
47
47
  <%= red_t("red.analytics.correlation.trend_analysis.period_range",
48
- start: @period_comparison[:previous_period][:start].strftime("%b %d"),
49
- end: @period_comparison[:previous_period][:end].strftime("%b %d")) %>
48
+ start: red_chart_date(@period_comparison[:previous_period][:start], "%b %d"),
49
+ end: red_chart_date(@period_comparison[:previous_period][:end], "%b %d")) %>
50
50
  </small>
51
51
  </div>
52
52
  <div class="col-md-4">
@@ -358,7 +358,7 @@
358
358
  new Chart(dailyTrendCtx, {
359
359
  type: 'line',
360
360
  data: {
361
- labels: <%= raw @daily_trends.values.first&.keys&.map { |d| d.strftime('%b %d') }&.to_json || [].to_json %>,
361
+ labels: <%= raw @daily_trends.values.first&.keys&.map { |d| red_chart_date(d, '%b %d') }&.to_json || [].to_json %>,
362
362
  datasets: datasets
363
363
  },
364
364
  options: {
@@ -49,7 +49,7 @@
49
49
  </div>
50
50
  <% else %>
51
51
  <div class="row mb-4">
52
- <div class="col-md-4">
52
+ <div class="col-md-3">
53
53
  <div class="card text-center">
54
54
  <div class="card-body">
55
55
  <div class="display-6 text-warning"><%= @unique_rules %></div>
@@ -57,7 +57,7 @@
57
57
  </div>
58
58
  </div>
59
59
  </div>
60
- <div class="col-md-4">
60
+ <div class="col-md-3">
61
61
  <div class="card text-center">
62
62
  <div class="card-body">
63
63
  <div class="display-6 text-danger"><%= @total_events %></div>
@@ -65,7 +65,7 @@
65
65
  </div>
66
66
  </div>
67
67
  </div>
68
- <div class="col-md-4">
68
+ <div class="col-md-3">
69
69
  <div class="card text-center">
70
70
  <div class="card-body">
71
71
  <div class="display-6 text-info"><%= @unique_ips %></div>
@@ -73,8 +73,27 @@
73
73
  </div>
74
74
  </div>
75
75
  </div>
76
+ <div class="col-md-3">
77
+ <div class="card text-center">
78
+ <div class="card-body">
79
+ <div class="display-6 text-primary"><%= @ai_events %></div>
80
+ <small class="text-muted"><%= red_t("red.errors.rack_attack_page.ai_events") %></small>
81
+ </div>
82
+ </div>
83
+ </div>
76
84
  </div>
77
85
 
86
+ <% if @overflow_count.to_i.positive? %>
87
+ <div class="alert alert-warning d-flex align-items-start" role="alert">
88
+ <i class="bi bi-exclamation-triangle me-2"></i>
89
+ <div>
90
+ <%= red_t("red.errors.rack_attack_page.overflow_notice_html",
91
+ count: @overflow_count,
92
+ option: content_tag(:code, "rack_attack_max_cache_size")) %>
93
+ </div>
94
+ </div>
95
+ <% end %>
96
+
78
97
  <div class="card mb-4">
79
98
  <div class="card-header d-flex justify-content-between align-items-center">
80
99
  <h5 class="mb-0">
@@ -94,6 +113,7 @@
94
113
  <th width="80"><%= red_t("red.errors.rack_attack_page.column_count") %></th>
95
114
  <th width="80"><%= red_t("red.errors.rack_attack_page.column_ips") %></th>
96
115
  <th><%= red_t("red.errors.rack_attack_page.column_top_path") %></th>
116
+ <th width="160"><%= red_t("red.errors.rack_attack_page.column_top_agent") %></th>
97
117
  <th width="140"><%= red_t("red.errors.pages.column_last_seen") %></th>
98
118
  </tr>
99
119
  </thead>
@@ -115,6 +135,19 @@
115
135
  <td><strong><%= event[:count] %></strong></td>
116
136
  <td><%= event[:unique_ips] %></td>
117
137
  <td><code><%= event[:top_path] %></code></td>
138
+ <td>
139
+ <% if event[:top_agent].present? %>
140
+ <%# AI agents are highlighted because "which agent" is the
141
+ question IP counts cannot answer — one agent is a fleet. %>
142
+ <span class="badge <%= event[:ai_count].to_i.positive? ? "bg-primary" : "bg-light text-dark" %>"
143
+ title="<%= event[:top_agent] %>"><%= truncate(event[:top_agent], length: 22) %></span>
144
+ <% if event[:unique_agents].to_i > 1 %>
145
+ <small class="text-muted">+<%= event[:unique_agents].to_i - 1 %></small>
146
+ <% end %>
147
+ <% else %>
148
+ <span class="text-muted">&mdash;</span>
149
+ <% end %>
150
+ </td>
118
151
  <td><%= local_time_ago(event[:last_seen]) %></td>
119
152
  </tr>
120
153
  <% end %>
@@ -833,12 +833,16 @@ de:
833
833
  unique_rules: Eindeutige Regeln
834
834
  total_events: Ereignisse gesamt
835
835
  unique_ips: Eindeutige IPs
836
+ ai_events: KI-Agent-Anfragen
836
837
  all_title: Rate-Limit-Ereignisse nach Regel
837
838
  column_rule: Regel
838
839
  column_type: Typ
839
840
  column_count: Anzahl
840
841
  column_ips: IPs
841
842
  column_top_path: Häufigster Pfad
843
+ 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.
842
846
  match_type:
843
847
  blocklist: blocklist
844
848
  throttle: throttle
@@ -2003,5 +2007,6 @@ de:
2003
2007
  axis_count: Anzahl
2004
2008
  axis_number_of_errors: Anzahl der Fehler
2005
2009
  axis_hours: Stunden
2010
+ suffix_hours: " Stunden"
2006
2011
  series_total_errors: Fehler gesamt
2007
2012
  series_hours_to_resolve: Stunden bis zur Behebung
@@ -1172,12 +1172,16 @@ en:
1172
1172
  unique_rules: "Unique Rules"
1173
1173
  total_events: "Total Events"
1174
1174
  unique_ips: "Unique IPs"
1175
+ ai_events: "AI Agent Requests"
1175
1176
  all_title: "Rate Limit Events by Rule"
1176
1177
  column_rule: "Rule"
1177
1178
  column_type: "Type"
1178
1179
  column_count: "Count"
1179
1180
  column_ips: "IPs"
1180
1181
  column_top_path: "Top Path"
1182
+ column_top_agent: "Top Agent"
1183
+ overflow_notice_html: >-
1184
+ %{count} events could not be attributed to a rule because the tracker's buffer was full. Raise %{option} to capture more.
1181
1185
  # Rack Attack match types. These are machine values the view also
1182
1186
  # compares by string equality, so the display is a lookup and the
1183
1187
  # comparison keeps the English literal.
@@ -2715,5 +2719,8 @@ en:
2715
2719
  axis_count: "Count"
2716
2720
  axis_number_of_errors: "Number of Errors"
2717
2721
  axis_hours: "Hours"
2722
+ # Chartkick concatenates this after a number on the MTTR charts, so it
2723
+ # carries its own leading space and is a bare unit, not a sentence.
2724
+ suffix_hours: " hours"
2718
2725
  series_total_errors: "Total Errors"
2719
2726
  series_hours_to_resolve: "Hours to Resolve"
@@ -862,12 +862,16 @@ es:
862
862
  unique_rules: Reglas únicas
863
863
  total_events: Total de eventos
864
864
  unique_ips: IP únicas
865
+ ai_events: Solicitudes de agentes de IA
865
866
  all_title: Eventos de límite de tasa por regla
866
867
  column_rule: Regla
867
868
  column_type: Tipo
868
869
  column_count: Cantidad
869
870
  column_ips: IP
870
871
  column_top_path: Ruta principal
872
+ 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.
871
875
  match_type:
872
876
  blocklist: blocklist
873
877
  throttle: throttle
@@ -2075,5 +2079,6 @@ es:
2075
2079
  axis_count: Cantidad
2076
2080
  axis_number_of_errors: Número de errores
2077
2081
  axis_hours: Horas
2082
+ suffix_hours: " horas"
2078
2083
  series_total_errors: Total de errores
2079
2084
  series_hours_to_resolve: Horas hasta la resolución
@@ -861,12 +861,16 @@ fr:
861
861
  unique_rules: Règles uniques
862
862
  total_events: Total des événements
863
863
  unique_ips: IP uniques
864
+ ai_events: Requêtes d'agents IA
864
865
  all_title: Événements de limitation de débit par règle
865
866
  column_rule: Règle
866
867
  column_type: Type
867
868
  column_count: Nombre
868
869
  column_ips: IP
869
870
  column_top_path: Chemin principal
871
+ column_top_agent: Agent principal
872
+ overflow_notice_html: >-
873
+ %{count} événements n'ont pas pu être attribués à une règle car la mémoire tampon du traceur était pleine. Augmentez %{option} pour en capturer davantage.
870
874
  match_type:
871
875
  blocklist: blocklist
872
876
  throttle: throttle
@@ -2078,5 +2082,6 @@ fr:
2078
2082
  axis_count: Nombre
2079
2083
  axis_number_of_errors: Nombre d'erreurs
2080
2084
  axis_hours: Heures
2085
+ suffix_hours: " heures"
2081
2086
  series_total_errors: Total des erreurs
2082
2087
  series_hours_to_resolve: Heures avant résolution
@@ -847,12 +847,16 @@ it:
847
847
  unique_rules: Regole uniche
848
848
  total_events: Eventi totali
849
849
  unique_ips: IP unici
850
+ ai_events: Richieste di agenti IA
850
851
  all_title: Eventi di limite di richieste per regola
851
852
  column_rule: Regola
852
853
  column_type: Tipo
853
854
  column_count: Conteggio
854
855
  column_ips: Indirizzi IP
855
856
  column_top_path: Percorso principale
857
+ column_top_agent: Agente principale
858
+ overflow_notice_html: >-
859
+ %{count} eventi non sono stati attribuiti a nessuna regola perché il buffer del tracker era pieno. Aumenta %{option} per acquisirne di più.
856
860
  match_type:
857
861
  blocklist: blocklist
858
862
  throttle: throttle
@@ -2056,5 +2060,6 @@ it:
2056
2060
  axis_count: Conteggio
2057
2061
  axis_number_of_errors: Numero di errori
2058
2062
  axis_hours: Ore
2063
+ suffix_hours: " ore"
2059
2064
  series_total_errors: Errori totali
2060
2065
  series_hours_to_resolve: Ore per la risoluzione
@@ -755,12 +755,16 @@ ja:
755
755
  unique_rules: ルール数
756
756
  total_events: 総イベント数
757
757
  unique_ips: ユニーク IP 数
758
+ ai_events: AI エージェントのリクエスト数
758
759
  all_title: ルール別レート制限イベント
759
760
  column_rule: ルール
760
761
  column_type: 種類
761
762
  column_count: 件数
762
763
  column_ips: IP 数
763
764
  column_top_path: 上位パス
765
+ column_top_agent: 主要エージェント
766
+ overflow_notice_html: >-
767
+ トラッカーのバッファが満杯だったため、%{count} 件のイベントをルールに割り当てられませんでした。より多く記録するには %{option} を増やしてください。
764
768
  match_type:
765
769
  blocklist: blocklist
766
770
  throttle: throttle
@@ -1798,5 +1802,6 @@ ja:
1798
1802
  axis_count: 件数
1799
1803
  axis_number_of_errors: エラー件数
1800
1804
  axis_hours: 時間
1805
+ suffix_hours: "時間"
1801
1806
  series_total_errors: 総エラー数
1802
1807
  series_hours_to_resolve: 解決までの時間
@@ -869,12 +869,16 @@ pl:
869
869
  unique_rules: Unikalne reguły
870
870
  total_events: Łącznie zdarzeń
871
871
  unique_ips: Unikalne adresy IP
872
+ ai_events: Żądania agentów AI
872
873
  all_title: Zdarzenia limitu żądań wg reguły
873
874
  column_rule: Reguła
874
875
  column_type: Typ
875
876
  column_count: Liczba
876
877
  column_ips: Adresy IP
877
878
  column_top_path: Najczęstsza ścieżka
879
+ column_top_agent: Główny agent
880
+ overflow_notice_html: >-
881
+ Nie udało się przypisać %{count} zdarzeń do żadnej reguły, ponieważ bufor mechanizmu śledzenia był pełny. Zwiększ %{option}, aby rejestrować więcej.
878
882
  match_type:
879
883
  blocklist: blocklist
880
884
  throttle: throttle
@@ -2104,5 +2108,6 @@ pl:
2104
2108
  axis_count: Liczba
2105
2109
  axis_number_of_errors: Liczba błędów
2106
2110
  axis_hours: Godziny
2111
+ suffix_hours: " godz."
2107
2112
  series_total_errors: Łącznie błędów
2108
2113
  series_hours_to_resolve: Godzin do rozwiązania
@@ -859,12 +859,16 @@ pt-BR:
859
859
  unique_rules: Regras únicas
860
860
  total_events: Total de eventos
861
861
  unique_ips: IPs únicos
862
+ ai_events: Solicitações de agentes de IA
862
863
  all_title: Eventos de limite de taxa por regra
863
864
  column_rule: Regra
864
865
  column_type: Tipo
865
866
  column_count: Quantidade
866
867
  column_ips: IPs
867
868
  column_top_path: Caminho principal
869
+ column_top_agent: Agente principal
870
+ overflow_notice_html: >-
871
+ Não foi possível atribuir %{count} eventos a nenhuma regra porque o buffer do rastreador estava cheio. Aumente %{option} para capturar mais.
868
872
  match_type:
869
873
  blocklist: blocklist
870
874
  throttle: throttle
@@ -2060,5 +2064,6 @@ pt-BR:
2060
2064
  axis_count: Quantidade
2061
2065
  axis_number_of_errors: Número de erros
2062
2066
  axis_hours: Horas
2067
+ suffix_hours: " horas"
2063
2068
  series_total_errors: Total de erros
2064
2069
  series_hours_to_resolve: Horas até a resolução
@@ -871,12 +871,16 @@ ru:
871
871
  unique_rules: Уникальные правила
872
872
  total_events: Всего событий
873
873
  unique_ips: Уникальные IP
874
+ ai_events: Запросы ИИ-агентов
874
875
  all_title: События ограничения частоты по правилам
875
876
  column_rule: Правило
876
877
  column_type: Тип
877
878
  column_count: Количество
878
879
  column_ips: IP-адреса
879
880
  column_top_path: Частый путь
881
+ column_top_agent: Основной агент
882
+ overflow_notice_html: >-
883
+ Не удалось отнести %{count} событий ни к одному правилу, так как буфер трекера был заполнен. Увеличьте %{option}, чтобы записывать больше.
880
884
  match_type:
881
885
  blocklist: blocklist
882
886
  throttle: throttle
@@ -2105,5 +2109,6 @@ ru:
2105
2109
  axis_count: Количество
2106
2110
  axis_number_of_errors: Количество ошибок
2107
2111
  axis_hours: Часы
2112
+ suffix_hours: " ч"
2108
2113
  series_total_errors: Всего ошибок
2109
2114
  series_hours_to_resolve: Часов до решения
@@ -869,12 +869,16 @@ uk:
869
869
  unique_rules: Унікальні правила
870
870
  total_events: Усього подій
871
871
  unique_ips: Унікальні IP
872
+ ai_events: Запити ШІ-агентів
872
873
  all_title: Події обмеження запитів за правилами
873
874
  column_rule: Правило
874
875
  column_type: Тип
875
876
  column_count: Кількість
876
877
  column_ips: IP-адреси
877
878
  column_top_path: Найчастіший шлях
879
+ column_top_agent: Основний агент
880
+ overflow_notice_html: >-
881
+ Не вдалося віднести %{count} подій до жодного правила, оскільки буфер трекера був заповнений. Збільште %{option}, щоб записувати більше.
878
882
  match_type:
879
883
  blocklist: blocklist
880
884
  throttle: throttle
@@ -2102,5 +2106,6 @@ uk:
2102
2106
  axis_count: Кількість
2103
2107
  axis_number_of_errors: Кількість помилок
2104
2108
  axis_hours: Години
2109
+ suffix_hours: " год"
2105
2110
  series_total_errors: Усього помилок
2106
2111
  series_hours_to_resolve: Годин до вирішення
@@ -751,12 +751,16 @@ zh-CN:
751
751
  unique_rules: 规则数
752
752
  total_events: 事件总数
753
753
  unique_ips: 独立 IP 数
754
+ ai_events: AI 代理请求数
754
755
  all_title: 按规则查看限流事件
755
756
  column_rule: 规则
756
757
  column_type: 类型
757
758
  column_count: 数量
758
759
  column_ips: IP 地址
759
760
  column_top_path: 主要路径
761
+ column_top_agent: 主要代理
762
+ overflow_notice_html: >-
763
+ 由于跟踪器缓冲区已满,%{count} 个事件无法归属到任何规则。请调高 %{option} 以捕获更多事件。
760
764
  match_type:
761
765
  blocklist: blocklist
762
766
  throttle: throttle
@@ -1787,5 +1791,6 @@ zh-CN:
1787
1791
  axis_count: 数量
1788
1792
  axis_number_of_errors: 错误数量
1789
1793
  axis_hours: 小时
1794
+ suffix_hours: "小时"
1790
1795
  series_total_errors: 错误总数
1791
1796
  series_hours_to_resolve: 解决耗时(小时)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddUserAgentToRackAttackEvents < ActiveRecord::Migration[7.0]
4
+ def change
5
+ # Guard against the squashed schema migration having already added this
6
+ # column — without it, every later migration is silently cancelled.
7
+ return if column_exists?(:rails_error_dashboard_rack_attack_events, :user_agent)
8
+
9
+ # Capped at 191 to match the other free-text columns on this table.
10
+ #
11
+ # Deliberately NOT added to index_rack_attack_events_upsert_key. That index
12
+ # is already budgeted at 250+50+191+191 chars = 2736 bytes against MySQL's
13
+ # 3072-byte utf8mb4 limit (see the create migration); a fourth 191-char
14
+ # column would blow it. User agents are also extremely high cardinality, so
15
+ # indexing them would fragment the hourly buckets this table exists to
16
+ # aggregate. It is stored first-write-wins per bucket, like http_method.
17
+ add_column :rails_error_dashboard_rack_attack_events, :user_agent, :string, limit: 191
18
+ end
19
+ end