rails_error_dashboard 0.10.0 → 0.11.1

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 +123 -49
  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 +76 -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 +24 -27
@@ -147,6 +147,7 @@ pt-BR:
147
147
  any: Qualquer gravidade
148
148
  all_apps: Todas as aplicações
149
149
  all_platforms: Todas as plataformas
150
+ all_environments: Todos os ambientes
150
151
  all_types: Todos os tipos
151
152
  all_timeframes: Todo o período
152
153
  all_frequencies: Todas as frequências
@@ -175,6 +176,7 @@ pt-BR:
175
176
  search: 'Busca: %{value}'
176
177
  application: 'Aplicação: %{value}'
177
178
  platform: 'Plataforma: %{value}'
179
+ environment: 'Ambiente: %{value}'
178
180
  error_type: 'Tipo: %{value}'
179
181
  severity: 'Gravidade: %{value}'
180
182
  timeframe: 'Período: %{value}'
@@ -190,6 +192,7 @@ pt-BR:
190
192
  last_seen: Última ocorrência
191
193
  application: Aplicação
192
194
  platform: Plataforma
195
+ environment: Ambiente
193
196
  select_all: Selecionar todos os erros
194
197
  batch:
195
198
  selected:
@@ -650,6 +653,7 @@ pt-BR:
650
653
  low: BAIXA
651
654
  platform: Plataforma
652
655
  view_all_platform: Ver todos os erros de %{platform}
656
+ view_all_environment: Ver todos os erros de %{environment}
653
657
  user: Usuário
654
658
  view_user_errors: Ver todos os erros deste usuário
655
659
  user_id: 'ID: %{id}'
@@ -867,8 +871,9 @@ pt-BR:
867
871
  column_ips: IPs
868
872
  column_top_path: Caminho principal
869
873
  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.
874
+ overflow_notice_html: Não foi possível atribuir %{count} eventos a nenhuma
875
+ regra porque o buffer do rastreador estava cheio. Aumente %{option} para
876
+ capturar mais.
872
877
  match_type:
873
878
  blocklist: blocklist
874
879
  throttle: throttle
@@ -1107,6 +1112,9 @@ pt-BR:
1107
1112
  sampling_rate: Porcentagem de erros capturados
1108
1113
  application_name: Nome desta aplicação (detectado automaticamente se não for
1109
1114
  definido)
1115
+ environment: Ambiente ao qual os erros são atribuídos (Rails.env se não definido)
1116
+ notification_environments: Notificar apenas para estes ambientes (todos se
1117
+ não definido)
1110
1118
  database: Banco de dados ativo para os logs de erros
1111
1119
  use_separate_database: Usar um banco de dados dedicado para os logs de erros
1112
1120
  user_model: Nome do modelo para as associações de usuário (detectado automaticamente
@@ -1277,11 +1285,14 @@ pt-BR:
1277
1285
  unknown_application: Desconhecida
1278
1286
  error_alert:
1279
1287
  subject: "\U0001F6A8 [%{application}] %{error_type}: %{message}"
1288
+ subject_with_environment: "\U0001F6A8 [%{application} · %{environment}] %{error_type}:
1289
+ %{message}"
1280
1290
  heading: "\U0001F6A8 Alerta de erro"
1281
1291
  labels:
1282
1292
  application: Aplicação
1283
1293
  error_type: Tipo de erro
1284
1294
  platform: Plataforma
1295
+ environment: Ambiente
1285
1296
  occurred_at: Ocorreu em
1286
1297
  user_id: ID do usuário
1287
1298
  ip_address: Endereço IP
@@ -1350,6 +1361,7 @@ pt-BR:
1350
1361
  application: Aplicação
1351
1362
  error_type: Tipo de erro
1352
1363
  platform: Plataforma
1364
+ environment: Ambiente
1353
1365
  occurred: Ocorreu
1354
1366
  message: Mensagem
1355
1367
  user: Usuário
@@ -1682,6 +1694,10 @@ pt-BR:
1682
1694
  platform_chart:
1683
1695
  title: Erros por plataforma
1684
1696
  quick_links: 'Links rápidos:'
1697
+ environment_chart:
1698
+ title: Erros por ambiente
1699
+ quick_links: 'Links rápidos:'
1700
+ unknown: Desconhecido
1685
1701
  type_chart:
1686
1702
  title: Os 10 tipos de erro mais frequentes
1687
1703
  hour_chart:
@@ -147,6 +147,7 @@ ru:
147
147
  any: Любая важность
148
148
  all_apps: Все приложения
149
149
  all_platforms: Все платформы
150
+ all_environments: Все окружения
150
151
  all_types: Все типы
151
152
  all_timeframes: За всё время
152
153
  all_frequencies: Любая частота
@@ -175,6 +176,7 @@ ru:
175
176
  search: 'Поиск: %{value}'
176
177
  application: 'Приложение: %{value}'
177
178
  platform: 'Платформа: %{value}'
179
+ environment: 'Окружение: %{value}'
178
180
  error_type: 'Тип: %{value}'
179
181
  severity: 'Важность: %{value}'
180
182
  timeframe: 'Период: %{value}'
@@ -190,6 +192,7 @@ ru:
190
192
  last_seen: Последнее появление
191
193
  application: Приложение
192
194
  platform: Платформа
195
+ environment: Окружение
193
196
  select_all: Выбрать все ошибки
194
197
  batch:
195
198
  selected:
@@ -659,6 +662,7 @@ ru:
659
662
  low: НИЗКАЯ
660
663
  platform: Платформа
661
664
  view_all_platform: 'Все ошибки: %{platform}'
665
+ view_all_environment: 'Все ошибки: %{environment}'
662
666
  user: Пользователь
663
667
  view_user_errors: Все ошибки этого пользователя
664
668
  user_id: 'ID: %{id}'
@@ -879,8 +883,9 @@ ru:
879
883
  column_ips: IP-адреса
880
884
  column_top_path: Частый путь
881
885
  column_top_agent: Основной агент
882
- overflow_notice_html: >-
883
- Не удалось отнести %{count} событий ни к одному правилу, так как буфер трекера был заполнен. Увеличьте %{option}, чтобы записывать больше.
886
+ overflow_notice_html: Не удалось отнести %{count} событий ни к одному правилу,
887
+ так как буфер трекера был заполнен. Увеличьте %{option}, чтобы записывать
888
+ больше.
884
889
  match_type:
885
890
  blocklist: blocklist
886
891
  throttle: throttle
@@ -1136,6 +1141,9 @@ ru:
1136
1141
  sampling_rate: Доля захватываемых ошибок
1137
1142
  application_name: Имя этого приложения (определяется автоматически, если не
1138
1143
  задано)
1144
+ environment: Окружение, к которому относятся ошибки (Rails.env, если не задано)
1145
+ notification_environments: Уведомлять только для этих окружений (все, если
1146
+ не задано)
1139
1147
  database: Активная база данных для журнала ошибок
1140
1148
  use_separate_database: Использовать отдельную БД для журнала ошибок
1141
1149
  user_model: Имя модели для связи с пользователями (определяется автоматически,
@@ -1309,11 +1317,14 @@ ru:
1309
1317
  unknown_application: Неизвестно
1310
1318
  error_alert:
1311
1319
  subject: "\U0001F6A8 [%{application}] %{error_type}: %{message}"
1320
+ subject_with_environment: "\U0001F6A8 [%{application} · %{environment}] %{error_type}:
1321
+ %{message}"
1312
1322
  heading: "\U0001F6A8 Оповещение об ошибке"
1313
1323
  labels:
1314
1324
  application: Приложение
1315
1325
  error_type: Тип ошибки
1316
1326
  platform: Платформа
1327
+ environment: Окружение
1317
1328
  occurred_at: Время возникновения
1318
1329
  user_id: ID пользователя
1319
1330
  ip_address: IP-адрес
@@ -1385,6 +1396,7 @@ ru:
1385
1396
  application: Приложение
1386
1397
  error_type: Тип ошибки
1387
1398
  platform: Платформа
1399
+ environment: Окружение
1388
1400
  occurred: Возникла
1389
1401
  message: Сообщение
1390
1402
  user: Пользователь
@@ -1715,6 +1727,10 @@ ru:
1715
1727
  platform_chart:
1716
1728
  title: Ошибки по платформам
1717
1729
  quick_links: 'Быстрые ссылки:'
1730
+ environment_chart:
1731
+ title: Ошибки по окружениям
1732
+ quick_links: 'Быстрые ссылки:'
1733
+ unknown: Неизвестно
1718
1734
  type_chart:
1719
1735
  title: Топ-10 типов ошибок
1720
1736
  hour_chart:
@@ -147,6 +147,7 @@ uk:
147
147
  any: Будь-яка критичність
148
148
  all_apps: Усі застосунки
149
149
  all_platforms: Усі платформи
150
+ all_environments: Усі середовища
150
151
  all_types: Усі типи
151
152
  all_timeframes: За весь час
152
153
  all_frequencies: Усі частоти
@@ -175,6 +176,7 @@ uk:
175
176
  search: 'Пошук: %{value}'
176
177
  application: 'Застосунок: %{value}'
177
178
  platform: 'Платформа: %{value}'
179
+ environment: 'Середовище: %{value}'
178
180
  error_type: 'Тип: %{value}'
179
181
  severity: 'Критичність: %{value}'
180
182
  timeframe: 'Період: %{value}'
@@ -190,6 +192,7 @@ uk:
190
192
  last_seen: Востаннє помічено
191
193
  application: Застосунок
192
194
  platform: Платформа
195
+ environment: Середовище
193
196
  select_all: Вибрати всі помилки
194
197
  batch:
195
198
  selected:
@@ -657,6 +660,7 @@ uk:
657
660
  low: НИЗЬКА
658
661
  platform: Платформа
659
662
  view_all_platform: Переглянути всі помилки %{platform}
663
+ view_all_environment: Переглянути всі помилки %{environment}
660
664
  user: Користувач
661
665
  view_user_errors: Переглянути всі помилки цього користувача
662
666
  user_id: 'ID: %{id}'
@@ -877,8 +881,9 @@ uk:
877
881
  column_ips: IP-адреси
878
882
  column_top_path: Найчастіший шлях
879
883
  column_top_agent: Основний агент
880
- overflow_notice_html: >-
881
- Не вдалося віднести %{count} подій до жодного правила, оскільки буфер трекера був заповнений. Збільште %{option}, щоб записувати більше.
884
+ overflow_notice_html: Не вдалося віднести %{count} подій до жодного правила,
885
+ оскільки буфер трекера був заповнений. Збільште %{option}, щоб записувати
886
+ більше.
882
887
  match_type:
883
888
  blocklist: blocklist
884
889
  throttle: throttle
@@ -1132,6 +1137,9 @@ uk:
1132
1137
  sampling_rate: Відсоток захоплених помилок
1133
1138
  application_name: Назва цього застосунку (визначається автоматично, якщо не
1134
1139
  задано)
1140
+ environment: Середовище, до якого належать помилки (Rails.env, якщо не задано)
1141
+ notification_environments: Сповіщати лише для цих середовищ (усі, якщо не
1142
+ задано)
1135
1143
  database: Активна база даних для журналів помилок
1136
1144
  use_separate_database: Використовувати окрему БД для журналів помилок
1137
1145
  user_model: Назва моделі для звʼязків із користувачами (визначається автоматично,
@@ -1307,11 +1315,14 @@ uk:
1307
1315
  unknown_application: Невідомо
1308
1316
  error_alert:
1309
1317
  subject: "\U0001F6A8 [%{application}] %{error_type}: %{message}"
1318
+ subject_with_environment: "\U0001F6A8 [%{application} · %{environment}] %{error_type}:
1319
+ %{message}"
1310
1320
  heading: "\U0001F6A8 Сповіщення про помилку"
1311
1321
  labels:
1312
1322
  application: Застосунок
1313
1323
  error_type: Тип помилки
1314
1324
  platform: Платформа
1325
+ environment: Середовище
1315
1326
  occurred_at: Час виникнення
1316
1327
  user_id: ID користувача
1317
1328
  ip_address: IP-адреса
@@ -1383,6 +1394,7 @@ uk:
1383
1394
  application: Застосунок
1384
1395
  error_type: Тип помилки
1385
1396
  platform: Платформа
1397
+ environment: Середовище
1386
1398
  occurred: Сталася
1387
1399
  message: Повідомлення
1388
1400
  user: Користувач
@@ -1714,6 +1726,10 @@ uk:
1714
1726
  platform_chart:
1715
1727
  title: Помилки за платформами
1716
1728
  quick_links: 'Швидкі посилання:'
1729
+ environment_chart:
1730
+ title: Помилки за середовищами
1731
+ quick_links: 'Швидкі посилання:'
1732
+ unknown: Невідомо
1717
1733
  type_chart:
1718
1734
  title: 10 найчастіших типів помилок
1719
1735
  hour_chart:
@@ -137,6 +137,7 @@ zh-CN:
137
137
  any: 任意严重程度
138
138
  all_apps: 全部应用
139
139
  all_platforms: 全部平台
140
+ all_environments: 全部环境
140
141
  all_types: 全部类型
141
142
  all_timeframes: 全部时间
142
143
  all_frequencies: 全部频率
@@ -165,6 +166,7 @@ zh-CN:
165
166
  search: 搜索:%{value}
166
167
  application: 应用:%{value}
167
168
  platform: 平台:%{value}
169
+ environment: 环境:%{value}
168
170
  error_type: 类型:%{value}
169
171
  severity: 严重程度:%{value}
170
172
  timeframe: 时间范围:%{value}
@@ -180,6 +182,7 @@ zh-CN:
180
182
  last_seen: 最近出现
181
183
  application: 应用
182
184
  platform: 平台
185
+ environment: 环境
183
186
  select_all: 选择全部错误
184
187
  batch:
185
188
  selected:
@@ -566,6 +569,7 @@ zh-CN:
566
569
  low: 低
567
570
  platform: 平台
568
571
  view_all_platform: 查看 %{platform} 的所有错误
572
+ view_all_environment: 查看 %{environment} 的所有错误
569
573
  user: 用户
570
574
  view_user_errors: 查看该用户的所有错误
571
575
  user_id: ID:%{id}
@@ -759,8 +763,7 @@ zh-CN:
759
763
  column_ips: IP 地址
760
764
  column_top_path: 主要路径
761
765
  column_top_agent: 主要代理
762
- overflow_notice_html: >-
763
- 由于跟踪器缓冲区已满,%{count} 个事件无法归属到任何规则。请调高 %{option} 以捕获更多事件。
766
+ overflow_notice_html: 由于跟踪器缓冲区已满,%{count} 个事件无法归属到任何规则。请调高 %{option} 以捕获更多事件。
764
767
  match_type:
765
768
  blocklist: blocklist
766
769
  throttle: throttle
@@ -950,6 +953,8 @@ zh-CN:
950
953
  max_backtrace_lines: 堆栈跟踪深度上限
951
954
  sampling_rate: 错误捕获的采样比例
952
955
  application_name: 本应用的名称(未设置时自动检测)
956
+ environment: 错误所属的环境(未设置时为 Rails.env)
957
+ notification_environments: 仅对这些环境发送通知(未设置时为全部)
953
958
  database: 错误日志使用的数据库
954
959
  use_separate_database: 为错误日志使用独立数据库
955
960
  user_model: 用户关联所用的模型名(未设置时自动检测)
@@ -1087,11 +1092,13 @@ zh-CN:
1087
1092
  unknown_application: 未知
1088
1093
  error_alert:
1089
1094
  subject: "\U0001F6A8 [%{application}] %{error_type}:%{message}"
1095
+ subject_with_environment: "\U0001F6A8 [%{application} · %{environment}] %{error_type}:%{message}"
1090
1096
  heading: "\U0001F6A8 错误告警"
1091
1097
  labels:
1092
1098
  application: 应用
1093
1099
  error_type: 错误类型
1094
1100
  platform: 平台
1101
+ environment: 环境
1095
1102
  occurred_at: 发生时间
1096
1103
  user_id: 用户 ID
1097
1104
  ip_address: IP 地址
@@ -1154,6 +1161,7 @@ zh-CN:
1154
1161
  application: 应用
1155
1162
  error_type: 错误类型
1156
1163
  platform: 平台
1164
+ environment: 环境
1157
1165
  occurred: 发生时间
1158
1166
  message: 消息
1159
1167
  user: 用户
@@ -1452,6 +1460,10 @@ zh-CN:
1452
1460
  platform_chart:
1453
1461
  title: 按平台统计错误
1454
1462
  quick_links: 快捷链接:
1463
+ environment_chart:
1464
+ title: 按环境统计的错误
1465
+ quick_links: 快捷链接:
1466
+ unknown: 未知
1455
1467
  type_chart:
1456
1468
  title: 出现最多的 10 种错误类型
1457
1469
  hour_chart:
@@ -1791,6 +1803,6 @@ zh-CN:
1791
1803
  axis_count: 数量
1792
1804
  axis_number_of_errors: 错误数量
1793
1805
  axis_hours: 小时
1794
- suffix_hours: "小时"
1806
+ suffix_hours: 小时
1795
1807
  series_total_errors: 错误总数
1796
1808
  series_hours_to_resolve: 解决耗时(小时)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Records which environment an error came from (production, staging, uat, ...).
4
+ #
5
+ # RED had this column in v0.1.x and dropped it in the v1.0-launch refactor. It
6
+ # returns as a nullable string: NULL means "captured before this column
7
+ # existed", which is information the dashboard uses (no badge, matched as a
8
+ # wildcard by FindOrIncrementError and adopted on the next occurrence), not a
9
+ # gap to paper over with a default. `rails_error_dashboard:backfill_environments`
10
+ # fills NULLs from environment_info.rails_env for anyone who wants history too.
11
+ class AddEnvironmentToErrorLogs < ActiveRecord::Migration[7.0]
12
+ def change
13
+ # Guard against the squashed schema migration having already added this
14
+ # column -- without it, every later migration is silently cancelled.
15
+ return if column_exists?(:rails_error_dashboard_error_logs, :environment)
16
+
17
+ # 64 chars: long enough for "preprod-eu-west-2-canary", short enough that
18
+ # the composite index below stays cheap on MySQL's utf8mb4 key limit.
19
+ add_column :rails_error_dashboard_error_logs, :environment, :string, limit: 64
20
+
21
+ # Mirrors index_error_logs_on_platform_and_occurred_at: the index filter is
22
+ # "this environment, newest first".
23
+ add_index :rails_error_dashboard_error_logs, [ :environment, :occurred_at ],
24
+ name: "index_error_logs_on_environment_and_occurred_at"
25
+ end
26
+ end
@@ -11,6 +11,17 @@ RailsErrorDashboard.configure do |config|
11
11
  config.dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "gandalf")
12
12
  config.dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "youshallnotpass")
13
13
 
14
+ # Environment awareness (v0.11.0). Every error records the environment it
15
+ # came from, the index can filter by it, and staging can be kept out of
16
+ # your pager. Defaults to Rails.env; override when a deploy runs under
17
+ # RAILS_ENV=production but is really "staging", "uat", "preprod"... names
18
+ # are free-form, never an enum.
19
+ # config.environment = ENV.fetch("ERROR_DASHBOARD_ENVIRONMENT", "staging")
20
+ #
21
+ # Only notify (Slack, email, Discord, PagerDuty, webhooks, storm and
22
+ # baseline alerts) for these environments. nil = every environment.
23
+ # config.notification_environments = %w[production]
24
+
14
25
  # === Custom Authentication (optional) ===
15
26
  # Use your app's existing auth instead of HTTP Basic Auth.
16
27
  # The lambda runs in controller context (via instance_exec), giving access to
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Commands
5
+ # Command: fill in `environment` on error logs captured before the column
6
+ # existed, using the rails_env recorded in environment_info at capture time.
7
+ #
8
+ # Opt-in (rails_error_dashboard:backfill_environments). Live errors do not
9
+ # need it — FindOrIncrementError adopts a NULL row on its next occurrence —
10
+ # but history that never recurs would otherwise stay unbadged and
11
+ # unfilterable forever.
12
+ #
13
+ # Runs in batches, one UPDATE per row: the value is inside a JSON text
14
+ # column, so it has to be parsed in Ruby and no portable single UPDATE can
15
+ # do it across PostgreSQL, MySQL and SQLite.
16
+ class BackfillEnvironments
17
+ BATCH_SIZE = 500
18
+ COLUMN_LIMIT = 64
19
+
20
+ def self.call(batch_size: BATCH_SIZE)
21
+ new(batch_size: batch_size).call
22
+ end
23
+
24
+ def initialize(batch_size: BATCH_SIZE)
25
+ @batch_size = batch_size
26
+ end
27
+
28
+ # @return [Integer] rows updated
29
+ def call
30
+ return 0 unless ErrorLog.column_names.include?("environment")
31
+
32
+ updated = 0
33
+ ErrorLog.where(environment: nil).where.not(environment_info: nil)
34
+ .in_batches(of: @batch_size) do |batch|
35
+ batch.pluck(:id, :environment_info).each do |id, raw|
36
+ env = rails_env_from(raw)
37
+ next if env.nil?
38
+
39
+ updated += ErrorLog.where(id: id, environment: nil).update_all(environment: env)
40
+ end
41
+ end
42
+ updated
43
+ end
44
+
45
+ private
46
+
47
+ def rails_env_from(raw)
48
+ parsed = JSON.parse(raw.to_s)
49
+ return nil unless parsed.is_a?(Hash)
50
+
51
+ value = parsed["rails_env"].to_s.strip
52
+ value.empty? ? nil : value[0, COLUMN_LIMIT]
53
+ rescue JSON::ParserError
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -9,7 +9,24 @@ module RailsErrorDashboard
9
9
  # 1. Unresolved errors with same hash within 24 hours → increment occurrence count
10
10
  # 2. Resolved/wont_fix errors with same hash (any age) → reopen and increment
11
11
  # 3. No match → create new error record
12
+ #
13
+ # Environment is a MATCH dimension, not part of the hash: the same error in
14
+ # staging and production is two rows with independent status. A row with a
15
+ # NULL environment predates the column; it matches as a wildcard and is
16
+ # stamped by the first occurrence that claims it, so history migrates
17
+ # itself without a backfill. An exact match always wins over a NULL one.
12
18
  class FindOrIncrementError
19
+ # Context that describes THIS occurrence rather than the error as a
20
+ # group. It is refreshed on every recurrence so the row always shows the
21
+ # latest moment of failure, not the first one in the 24 h window. Keys
22
+ # absent from @attributes (feature disabled, column not migrated, or a
23
+ # storm :lite capture that shed context) leave the stored payload alone —
24
+ # a shed capture must never blank out a good snapshot.
25
+ REFRESHED_CONTEXT = %i[
26
+ breadcrumbs system_health local_variables instance_variables
27
+ http_method hostname content_type request_duration_ms
28
+ ].freeze
29
+
13
30
  def self.call(error_hash, attributes = {})
14
31
  new(error_hash, attributes).call
15
32
  end
@@ -35,23 +52,47 @@ module RailsErrorDashboard
35
52
  private
36
53
 
37
54
  def find_unresolved
38
- ErrorLog.unresolved
39
- .where(error_hash: @error_hash)
40
- .where(application_id: @attributes[:application_id])
41
- .where("occurred_at >= ?", 24.hours.ago)
42
- .lock
43
- .order(last_seen_at: :desc)
44
- .first
55
+ with_environment(
56
+ ErrorLog.unresolved
57
+ .where(error_hash: @error_hash)
58
+ .where(application_id: @attributes[:application_id])
59
+ .where("occurred_at >= ?", 24.hours.ago)
60
+ ).lock.order(last_seen_at: :desc).first
45
61
  end
46
62
 
47
63
  def find_resolved
48
- ErrorLog
49
- .where(error_hash: @error_hash)
50
- .where(application_id: @attributes[:application_id])
51
- .where(status: %w[resolved wont_fix])
52
- .lock
53
- .order(last_seen_at: :desc)
54
- .first
64
+ with_environment(
65
+ ErrorLog
66
+ .where(error_hash: @error_hash)
67
+ .where(application_id: @attributes[:application_id])
68
+ .where(status: %w[resolved wont_fix])
69
+ ).lock.order(last_seen_at: :desc).first
70
+ end
71
+
72
+ # Restrict to this occurrence's environment or a legacy NULL row, exact
73
+ # first. Literal SQL, no interpolation. A blank environment (column not
74
+ # migrated yet, or an attribute-less caller) leaves the scope unchanged.
75
+ def with_environment(scope)
76
+ env = @attributes[:environment]
77
+ return scope if env.blank? || !ErrorLog.column_names.include?("environment")
78
+
79
+ scope.where(environment: [ env, nil ])
80
+ .order(Arel.sql("CASE WHEN environment IS NULL THEN 1 ELSE 0 END"))
81
+ end
82
+
83
+ # The subset of REFRESHED_CONTEXT this occurrence actually captured.
84
+ def latest_context
85
+ REFRESHED_CONTEXT.each_with_object({}) do |key, refreshed|
86
+ refreshed[key] = @attributes[key] unless @attributes[key].nil?
87
+ end
88
+ end
89
+
90
+ # {} unless this is a legacy NULL-environment row being claimed.
91
+ def environment_adoption(error)
92
+ return {} unless ErrorLog.column_names.include?("environment")
93
+ return {} if error.environment.present? || @attributes[:environment].blank?
94
+
95
+ { environment: @attributes[:environment] }
55
96
  end
56
97
 
57
98
  def increment_existing(error)
@@ -62,7 +103,9 @@ module RailsErrorDashboard
62
103
  request_url: @attributes[:request_url] || error.request_url,
63
104
  request_params: @attributes[:request_params] || error.request_params,
64
105
  user_agent: @attributes[:user_agent] || error.user_agent,
65
- ip_address: @attributes[:ip_address] || error.ip_address
106
+ ip_address: @attributes[:ip_address] || error.ip_address,
107
+ **latest_context,
108
+ **environment_adoption(error)
66
109
  )
67
110
  error
68
111
  end
@@ -78,7 +121,9 @@ module RailsErrorDashboard
78
121
  request_url: @attributes[:request_url] || error.request_url,
79
122
  request_params: @attributes[:request_params] || error.request_params,
80
123
  user_agent: @attributes[:user_agent] || error.user_agent,
81
- ip_address: @attributes[:ip_address] || error.ip_address
124
+ ip_address: @attributes[:ip_address] || error.ip_address,
125
+ **latest_context,
126
+ **environment_adoption(error)
82
127
  }
83
128
  attrs[:reopened_at] = Time.current if ErrorLog.column_names.include?("reopened_at")
84
129
  error.update!(attrs)
@@ -90,27 +135,29 @@ module RailsErrorDashboard
90
135
  ErrorLog.create!(@attributes.reverse_merge(resolved: false))
91
136
  rescue ActiveRecord::RecordNotUnique
92
137
  # Race condition: another process created the same error
93
- retry_existing = ErrorLog.unresolved
94
- .where(error_hash: @error_hash)
95
- .where(application_id: @attributes[:application_id])
96
- .where("occurred_at >= ?", 24.hours.ago)
97
- .lock
98
- .first
138
+ retry_existing = with_environment(
139
+ ErrorLog.unresolved
140
+ .where(error_hash: @error_hash)
141
+ .where(application_id: @attributes[:application_id])
142
+ .where("occurred_at >= ?", 24.hours.ago)
143
+ ).lock.first
99
144
 
100
145
  if retry_existing
101
146
  retry_existing.update!(
102
147
  occurrence_count: retry_existing.occurrence_count + 1,
103
- last_seen_at: Time.current
148
+ last_seen_at: Time.current,
149
+ **latest_context,
150
+ **environment_adoption(retry_existing)
104
151
  )
105
152
  retry_existing
106
153
  else
107
154
  # Also check resolved in race condition path
108
- retry_resolved = ErrorLog
109
- .where(error_hash: @error_hash)
110
- .where(application_id: @attributes[:application_id])
111
- .where(status: %w[resolved wont_fix])
112
- .lock
113
- .first
155
+ retry_resolved = with_environment(
156
+ ErrorLog
157
+ .where(error_hash: @error_hash)
158
+ .where(application_id: @attributes[:application_id])
159
+ .where(status: %w[resolved wont_fix])
160
+ ).lock.first
114
161
 
115
162
  if retry_resolved
116
163
  reopen_existing(retry_resolved)