rails_error_dashboard 0.1.37 → 0.2.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -4
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +2 -5
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +2 -3
  5. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +10 -0
  6. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +19 -15
  7. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +19 -9
  8. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +37 -11
  9. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +44 -0
  10. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +38 -16
  11. data/app/models/rails_error_dashboard/error_log.rb +10 -0
  12. data/app/models/rails_error_dashboard/error_logs_record.rb +11 -6
  13. data/app/views/layouts/rails_error_dashboard.html.erb +16 -0
  14. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +3 -0
  15. data/app/views/rails_error_dashboard/errors/_stats.html.erb +12 -4
  16. data/app/views/rails_error_dashboard/errors/index.html.erb +9 -7
  17. data/app/views/rails_error_dashboard/errors/show.html.erb +138 -7
  18. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +36 -0
  19. data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +1 -1
  20. data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +1 -1
  21. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +1 -1
  22. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +1 -1
  23. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +1 -1
  24. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +1 -1
  25. data/db/migrate/20251225100236_create_error_occurrences.rb +1 -1
  26. data/db/migrate/20251225101920_create_cascade_patterns.rb +1 -1
  27. data/db/migrate/20251225102500_create_error_baselines.rb +1 -1
  28. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +1 -1
  29. data/db/migrate/20251226020100_create_error_comments.rb +1 -1
  30. data/db/migrate/20251230075315_cleanup_orphaned_migrations.rb +1 -1
  31. data/db/migrate/20260220000001_add_exception_cause_to_error_logs.rb +9 -0
  32. data/db/migrate/20260220000002_add_enriched_context_to_error_logs.rb +12 -0
  33. data/db/migrate/20260220000003_add_time_series_indexes_to_error_logs.rb +67 -0
  34. data/db/migrate/20260221000001_add_environment_info_to_error_logs.rb +9 -0
  35. data/db/migrate/20260221000002_add_reopened_at_to_error_logs.rb +9 -0
  36. data/lib/generators/rails_error_dashboard/install/install_generator.rb +145 -24
  37. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +12 -8
  38. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +58 -10
  39. data/lib/rails_error_dashboard/commands/log_error.rb +109 -10
  40. data/lib/rails_error_dashboard/configuration.rb +52 -0
  41. data/lib/rails_error_dashboard/manual_error_reporter.rb +12 -0
  42. data/lib/rails_error_dashboard/middleware/error_catcher.rb +3 -0
  43. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +8 -0
  44. data/lib/rails_error_dashboard/queries/errors_list.rb +8 -0
  45. data/lib/rails_error_dashboard/services/backtrace_parser.rb +31 -0
  46. data/lib/rails_error_dashboard/services/backtrace_processor.rb +31 -1
  47. data/lib/rails_error_dashboard/services/cause_chain_extractor.rb +62 -0
  48. data/lib/rails_error_dashboard/services/environment_snapshot.rb +85 -0
  49. data/lib/rails_error_dashboard/services/error_hash_generator.rb +50 -2
  50. data/lib/rails_error_dashboard/services/notification_throttler.rb +109 -0
  51. data/lib/rails_error_dashboard/services/platform_detector.rb +36 -11
  52. data/lib/rails_error_dashboard/services/sensitive_data_filter.rb +176 -0
  53. data/lib/rails_error_dashboard/value_objects/error_context.rb +81 -4
  54. data/lib/rails_error_dashboard/version.rb +1 -1
  55. data/lib/rails_error_dashboard.rb +11 -6
  56. data/lib/tasks/error_dashboard.rake +158 -2
  57. metadata +14 -60
@@ -18,6 +18,9 @@
18
18
  user_id: <%= raw @error.user_id.to_json %>,
19
19
  severity: <%= raw @error.severity.to_json %>,
20
20
  priority_level: <%= raw (@error.priority_level || 0).to_json %>,
21
+ <% if @error.respond_to?(:exception_cause) && @error.exception_cause.present? %>
22
+ exception_cause: <%= raw @error.exception_cause %>,
23
+ <% end %>
21
24
  <% if @error.respond_to?(:app_version) %>
22
25
  app_version: <%= raw @error.app_version.to_json %>,
23
26
  <% end %>
@@ -108,6 +111,11 @@
108
111
  <% if @error.recent? %>
109
112
  <span class="badge bg-success ms-2" data-bs-toggle="tooltip" title="Error occurred within the last hour">NEW</span>
110
113
  <% end %>
114
+ <% if @error.reopened? %>
115
+ <span class="badge bg-warning text-dark ms-2" data-bs-toggle="tooltip" title="Previously resolved, recurred <%= @error.reopened_at&.strftime('%b %d, %Y %H:%M') %>">
116
+ <i class="bi bi-arrow-counterclockwise"></i> Reopened
117
+ </span>
118
+ <% end %>
111
119
  </h5>
112
120
  <button class="btn btn-sm btn-outline-light" onclick="copyToClipboard('<%= j @error.error_type %>', this)" title="Copy error type">
113
121
  <i class="bi bi-clipboard"></i>
@@ -125,6 +133,57 @@
125
133
  <%= @error.message %>
126
134
  </div>
127
135
 
136
+ <% if @error.respond_to?(:exception_cause) && @error.exception_cause.present? %>
137
+ <% begin %>
138
+ <% cause_chain = JSON.parse(@error.exception_cause) %>
139
+ <% if cause_chain.is_a?(Array) && cause_chain.any? %>
140
+ <div class="mt-4 mb-3">
141
+ <h6 class="text-muted mb-2">
142
+ <i class="bi bi-link-45deg"></i> Exception Cause Chain
143
+ <span class="badge bg-secondary ms-1"><%= cause_chain.length %></span>
144
+ </h6>
145
+ <div class="cause-chain">
146
+ <% cause_chain.each_with_index do |cause, index| %>
147
+ <div class="card mb-2 border-warning">
148
+ <div class="card-header bg-warning bg-opacity-10 py-2">
149
+ <div class="d-flex justify-content-between align-items-center">
150
+ <div>
151
+ <small class="text-muted">Caused by<%= " (##{index + 1})" if cause_chain.length > 1 %>:</small>
152
+ <strong class="ms-1"><code><%= cause["class_name"] %></code></strong>
153
+ </div>
154
+ <% if cause["backtrace"].present? %>
155
+ <button class="btn btn-sm btn-outline-secondary py-0" type="button"
156
+ data-bs-toggle="collapse"
157
+ data-bs-target="#cause-backtrace-<%= index %>"
158
+ aria-expanded="false">
159
+ <small><i class="bi bi-code-slash"></i> Backtrace</small>
160
+ </button>
161
+ <% end %>
162
+ </div>
163
+ </div>
164
+ <div class="card-body py-2">
165
+ <small><%= cause["message"] %></small>
166
+ </div>
167
+ <% if cause["backtrace"].present? %>
168
+ <div class="collapse" id="cause-backtrace-<%= index %>">
169
+ <div class="card-body p-0 border-top">
170
+ <div class="code-block p-2" style="max-height: 200px; overflow-y: auto; overflow-x: auto; font-size: 0.8rem;">
171
+ <pre class="mb-0"><code><% cause["backtrace"].each do |line| %><%= line %>
172
+ <% end %></code></pre>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ <% end %>
177
+ </div>
178
+ <% end %>
179
+ </div>
180
+ </div>
181
+ <% end %>
182
+ <% rescue JSON::ParserError %>
183
+ <%# Silently skip invalid JSON — safety first %>
184
+ <% end %>
185
+ <% end %>
186
+
128
187
  <div class="d-flex justify-content-between align-items-center mb-2 mt-4">
129
188
  <h6 class="text-muted mb-0">
130
189
  Backtrace:
@@ -166,6 +225,7 @@
166
225
  <!-- Frame header (always visible) -->
167
226
  <div class="<%= frame_bg_class(frame[:category]) %> d-flex justify-content-between align-items-center px-2 py-1">
168
227
  <div>
228
+ <span class="backtrace-frame-number"><%= index + 1 %></span>
169
229
  <span class="<%= frame_color_class(frame[:category]) %>">
170
230
  <%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %>
171
231
  </span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span>
@@ -209,7 +269,7 @@
209
269
  <div id="frameworkBacktrace" class="accordion-collapse collapse" data-bs-parent="#frameworkBacktraceAccordion">
210
270
  <div class="accordion-body p-0">
211
271
  <div class="code-block p-3" style="max-height: 400px; overflow-y: auto; overflow-x: auto;">
212
- <pre class="mb-0"><code><% framework_frames.each do |frame| %><span class="<%= frame_bg_class(frame[:category]) %> d-block px-2 py-1"><span class="<%= frame_color_class(frame[:category]) %>"><%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %></span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span></span>
272
+ <pre class="mb-0"><code><% framework_frames.each_with_index do |frame, index| %><span class="<%= frame_bg_class(frame[:category]) %> d-block px-2 py-1"><span class="backtrace-frame-number"><%= index + 1 %></span><span class="<%= frame_color_class(frame[:category]) %>"><%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %></span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span></span>
213
273
  <% end %></code></pre>
214
274
  </div>
215
275
  </div>
@@ -232,7 +292,7 @@
232
292
  <% end %>
233
293
 
234
294
  <!-- Request Context -->
235
- <% cache [@error, 'request_context_v1'] do %>
295
+ <% cache [@error, 'request_context_v2'] do %>
236
296
  <div class="card mb-4">
237
297
  <div class="card-header bg-white">
238
298
  <h5 class="mb-0"><i class="bi bi-globe"></i> Request Context</h5>
@@ -241,8 +301,36 @@
241
301
  <table class="table table-sm">
242
302
  <tr>
243
303
  <th width="200">Request URL:</th>
244
- <td><code><%= @error.request_url || 'N/A' %></code></td>
304
+ <td>
305
+ <% if @error.respond_to?(:http_method) && @error.http_method.present? %>
306
+ <span class="badge bg-primary me-1"><%= @error.http_method %></span>
307
+ <% end %>
308
+ <code><%= @error.request_url || 'N/A' %></code>
309
+ </td>
245
310
  </tr>
311
+ <% if @error.respond_to?(:hostname) && @error.hostname.present? %>
312
+ <tr>
313
+ <th>Hostname:</th>
314
+ <td><code><%= @error.hostname %></code></td>
315
+ </tr>
316
+ <% end %>
317
+ <% if @error.respond_to?(:content_type) && @error.content_type.present? %>
318
+ <tr>
319
+ <th>Content Type:</th>
320
+ <td><code><%= @error.content_type %></code></td>
321
+ </tr>
322
+ <% end %>
323
+ <% if @error.respond_to?(:request_duration_ms) && @error.request_duration_ms.present? %>
324
+ <tr>
325
+ <th>Request Duration:</th>
326
+ <td>
327
+ <% duration = @error.request_duration_ms %>
328
+ <span class="badge bg-<%= duration > 5000 ? 'danger' : duration > 1000 ? 'warning' : 'success' %>">
329
+ <%= duration > 1000 ? "#{(duration / 1000.0).round(1)}s" : "#{duration}ms" %>
330
+ </span>
331
+ </td>
332
+ </tr>
333
+ <% end %>
246
334
  <tr>
247
335
  <th>Request Params:</th>
248
336
  <td>
@@ -789,6 +877,20 @@
789
877
  <% end %>
790
878
  </div>
791
879
 
880
+ <!-- Reopened indicator -->
881
+ <% if @error.reopened? %>
882
+ <div class="mb-3">
883
+ <small class="text-muted d-block mb-1">Reopened</small>
884
+ <span class="badge bg-warning text-dark">
885
+ <i class="bi bi-arrow-counterclockwise"></i> Reopened
886
+ </span>
887
+ <br>
888
+ <small class="text-muted mt-1 d-block">
889
+ <%= local_time(@error.reopened_at, format: :full) %>
890
+ </small>
891
+ </div>
892
+ <% end %>
893
+
792
894
  <!-- Phase 3: Assignment -->
793
895
  <% if @error.respond_to?(:assigned_to) %>
794
896
  <div class="mb-3">
@@ -893,22 +995,51 @@
893
995
  </div>
894
996
  <% end %>
895
997
 
998
+ <% env_info = @error.respond_to?(:environment_info) && @error.environment_info.present? ?
999
+ JSON.parse(@error.environment_info, symbolize_names: true) : nil rescue nil %>
1000
+
896
1001
  <div class="mb-1">
897
1002
  <small class="text-muted">Rails:</small>
898
- <code class="ms-1"><%= Rails.version %></code>
1003
+ <code class="ms-1"><%= env_info&.dig(:rails_version) || Rails.version %></code>
899
1004
  </div>
900
1005
 
901
1006
  <div class="mb-1">
902
1007
  <small class="text-muted">Ruby:</small>
903
- <code class="ms-1"><%= RUBY_VERSION %></code>
1008
+ <code class="ms-1"><%= env_info&.dig(:ruby_version) || RUBY_VERSION %></code>
904
1009
  </div>
905
1010
 
906
1011
  <div class="mb-1">
907
1012
  <small class="text-muted">Environment:</small>
908
- <span class="badge bg-<%= Rails.env.production? ? 'danger' : Rails.env.development? ? 'success' : 'warning' %> ms-1">
909
- <%= Rails.env.titleize %>
1013
+ <% env_name = env_info&.dig(:rails_env) || Rails.env.to_s %>
1014
+ <span class="badge bg-<%= env_name == 'production' ? 'danger' : env_name == 'development' ? 'success' : 'warning' %> ms-1">
1015
+ <%= env_name.titleize %>
910
1016
  </span>
911
1017
  </div>
1018
+
1019
+ <% if env_info&.dig(:server).present? && env_info[:server] != "unknown" %>
1020
+ <div class="mb-1">
1021
+ <small class="text-muted">Server:</small>
1022
+ <code class="ms-1"><%= env_info[:server].capitalize %></code>
1023
+ </div>
1024
+ <% end %>
1025
+
1026
+ <% if env_info&.dig(:database_adapter).present? && env_info[:database_adapter] != "unknown" %>
1027
+ <div class="mb-1">
1028
+ <small class="text-muted">Database:</small>
1029
+ <code class="ms-1"><%= env_info[:database_adapter] %></code>
1030
+ </div>
1031
+ <% end %>
1032
+
1033
+ <% if env_info&.dig(:gem_versions)&.any? %>
1034
+ <div class="mb-1">
1035
+ <small class="text-muted d-block">Key Gems:</small>
1036
+ <div class="ps-2">
1037
+ <% env_info[:gem_versions].each do |name, version| %>
1038
+ <small><code><%= name %> <%= version %></code></small><br>
1039
+ <% end %>
1040
+ </div>
1041
+ </div>
1042
+ <% end %>
912
1043
  </div>
913
1044
 
914
1045
  <div>
@@ -68,6 +68,21 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
68
68
  # Application association (from 20260106094233)
69
69
  t.integer :application_id, null: false
70
70
 
71
+ # Exception cause chain (from 20260220000001)
72
+ t.text :exception_cause
73
+
74
+ # Enriched request context (from 20260220000002)
75
+ t.string :http_method, limit: 10
76
+ t.string :hostname, limit: 255
77
+ t.string :content_type, limit: 100
78
+ t.integer :request_duration_ms
79
+
80
+ # Environment info snapshot (from 20260221000001)
81
+ t.text :environment_info
82
+
83
+ # Auto-reopen tracking (from 20260221000002)
84
+ t.datetime :reopened_at
85
+
71
86
  t.timestamps
72
87
  end
73
88
 
@@ -170,6 +185,27 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
170
185
  add_index :rails_error_dashboard_error_comments, :error_log_id
171
186
  add_index :rails_error_dashboard_error_comments, [ :error_log_id, :created_at ], name: "index_error_comments_on_error_and_time"
172
187
 
188
+ # PostgreSQL-specific indexes (BRIN + functional for time-series optimization)
189
+ if ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
190
+ execute <<-SQL
191
+ CREATE INDEX index_error_logs_on_occurred_at_brin
192
+ ON rails_error_dashboard_error_logs
193
+ USING brin (occurred_at)
194
+ SQL
195
+
196
+ execute <<-SQL
197
+ CREATE INDEX index_error_logs_on_occurred_at_day
198
+ ON rails_error_dashboard_error_logs
199
+ (DATE_TRUNC('day', occurred_at))
200
+ SQL
201
+
202
+ execute <<-SQL
203
+ CREATE INDEX index_error_logs_on_occurred_at_hour
204
+ ON rails_error_dashboard_error_logs
205
+ (DATE_TRUNC('hour', occurred_at))
206
+ SQL
207
+ end
208
+
173
209
  # Add foreign keys
174
210
  add_foreign_key :rails_error_dashboard_error_logs, :rails_error_dashboard_applications, column: :application_id
175
211
  add_foreign_key :rails_error_dashboard_error_occurrences, :rails_error_dashboard_error_logs, column: :error_log_id
@@ -1,4 +1,4 @@
1
- class AddBetterTrackingToErrorLogs < ActiveRecord::Migration[8.1]
1
+ class AddBetterTrackingToErrorLogs < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  # Skip if squashed migration already added these columns
4
4
  return if column_exists?(:rails_error_dashboard_error_logs, :error_hash)
@@ -1,4 +1,4 @@
1
- class AddControllerActionToErrorLogs < ActiveRecord::Migration[8.1]
1
+ class AddControllerActionToErrorLogs < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  # Skip if squashed migration already added these columns
4
4
  return if column_exists?(:rails_error_dashboard_error_logs, :controller_name)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class AddOptimizedIndexesToErrorLogs < ActiveRecord::Migration[8.1]
3
+ class AddOptimizedIndexesToErrorLogs < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  # Skip if squashed migration already added these indexes
6
6
  return if index_exists?(:rails_error_dashboard_error_logs, [ :resolved, :occurred_at ],
@@ -1,4 +1,4 @@
1
- class RemoveEnvironmentFromErrorLogs < ActiveRecord::Migration[8.1]
1
+ class RemoveEnvironmentFromErrorLogs < ActiveRecord::Migration[7.0]
2
2
  def up
3
3
  # Skip if squashed migration ran (column never existed) or already removed
4
4
  return unless column_exists?(:rails_error_dashboard_error_logs, :environment)
@@ -1,4 +1,4 @@
1
- class AddEnhancedMetricsToErrorLogs < ActiveRecord::Migration[8.0]
1
+ class AddEnhancedMetricsToErrorLogs < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  # Skip if squashed migration already added these columns
4
4
  return if column_exists?(:rails_error_dashboard_error_logs, :app_version)
@@ -1,4 +1,4 @@
1
- class AddSimilarityTrackingToErrorLogs < ActiveRecord::Migration[8.0]
1
+ class AddSimilarityTrackingToErrorLogs < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  # Skip if squashed migration already added these columns
4
4
  return if column_exists?(:rails_error_dashboard_error_logs, :similarity_score)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateErrorOccurrences < ActiveRecord::Migration[8.0]
3
+ class CreateErrorOccurrences < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  # Skip if squashed migration already created this table
6
6
  return if table_exists?(:rails_error_dashboard_error_occurrences)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateCascadePatterns < ActiveRecord::Migration[8.0]
3
+ class CreateCascadePatterns < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  # Skip if squashed migration already created this table
6
6
  return if table_exists?(:rails_error_dashboard_cascade_patterns)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateErrorBaselines < ActiveRecord::Migration[8.0]
3
+ class CreateErrorBaselines < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  # Skip if squashed migration already created this table
6
6
  return if table_exists?(:rails_error_dashboard_error_baselines)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class AddWorkflowFieldsToErrorLogs < ActiveRecord::Migration[8.0]
3
+ class AddWorkflowFieldsToErrorLogs < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  # Skip if squashed migration already added these columns
6
6
  return if column_exists?(:rails_error_dashboard_error_logs, :status)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CreateErrorComments < ActiveRecord::Migration[8.0]
3
+ class CreateErrorComments < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  # Skip if squashed migration already created this table
6
6
  return if table_exists?(:rails_error_dashboard_error_comments)
@@ -1,4 +1,4 @@
1
- class CleanupOrphanedMigrations < ActiveRecord::Migration[8.0]
1
+ class CleanupOrphanedMigrations < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  end
4
4
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddExceptionCauseToErrorLogs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ unless column_exists?(:rails_error_dashboard_error_logs, :exception_cause)
6
+ add_column :rails_error_dashboard_error_logs, :exception_cause, :text
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddEnrichedContextToErrorLogs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ unless column_exists?(:rails_error_dashboard_error_logs, :http_method)
6
+ add_column :rails_error_dashboard_error_logs, :http_method, :string, limit: 10
7
+ add_column :rails_error_dashboard_error_logs, :hostname, :string, limit: 255
8
+ add_column :rails_error_dashboard_error_logs, :content_type, :string, limit: 100
9
+ add_column :rails_error_dashboard_error_logs, :request_duration_ms, :integer
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tier 0 time-series optimization: BRIN and functional indexes
4
+ #
5
+ # BRIN (Block Range Index) on occurred_at:
6
+ # - 99.9% smaller than B-tree (72KB vs 676MB on 100M rows)
7
+ # - Nearly identical query performance for time-range scans
8
+ # - Perfect for INSERT-heavy tables with naturally ordered timestamps
9
+ #
10
+ # Functional indexes for Groupdate:
11
+ # - Pre-compute DATE_TRUNC expressions used by group_by_day/group_by_hour
12
+ # - Up to 70x speedup on analytics dashboard queries
13
+ #
14
+ # All PostgreSQL-specific — gracefully skipped on SQLite/MySQL.
15
+ class AddTimeSeriesIndexesToErrorLogs < ActiveRecord::Migration[7.0]
16
+ def up
17
+ return unless postgresql?
18
+
19
+ # BRIN index on occurred_at for time-range scans
20
+ # Replaces expensive B-tree sequential scans on large tables
21
+ unless index_exists?(:rails_error_dashboard_error_logs, :occurred_at, name: "index_error_logs_on_occurred_at_brin")
22
+ execute <<-SQL
23
+ CREATE INDEX CONCURRENTLY index_error_logs_on_occurred_at_brin
24
+ ON rails_error_dashboard_error_logs
25
+ USING brin (occurred_at)
26
+ SQL
27
+ end
28
+
29
+ # Functional index for daily grouping (used by group_by_day)
30
+ unless index_exists_by_name?("index_error_logs_on_occurred_at_day")
31
+ execute <<-SQL
32
+ CREATE INDEX CONCURRENTLY index_error_logs_on_occurred_at_day
33
+ ON rails_error_dashboard_error_logs
34
+ (DATE_TRUNC('day', occurred_at))
35
+ SQL
36
+ end
37
+
38
+ # Functional index for hourly grouping (used by group_by_hour)
39
+ unless index_exists_by_name?("index_error_logs_on_occurred_at_hour")
40
+ execute <<-SQL
41
+ CREATE INDEX CONCURRENTLY index_error_logs_on_occurred_at_hour
42
+ ON rails_error_dashboard_error_logs
43
+ (DATE_TRUNC('hour', occurred_at))
44
+ SQL
45
+ end
46
+ end
47
+
48
+ def down
49
+ return unless postgresql?
50
+
51
+ execute "DROP INDEX IF EXISTS index_error_logs_on_occurred_at_brin"
52
+ execute "DROP INDEX IF EXISTS index_error_logs_on_occurred_at_day"
53
+ execute "DROP INDEX IF EXISTS index_error_logs_on_occurred_at_hour"
54
+ end
55
+
56
+ private
57
+
58
+ def postgresql?
59
+ ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
60
+ end
61
+
62
+ def index_exists_by_name?(name)
63
+ ActiveRecord::Base.connection.execute(
64
+ "SELECT 1 FROM pg_indexes WHERE indexname = '#{name}'"
65
+ ).any?
66
+ end
67
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddEnvironmentInfoToErrorLogs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ unless column_exists?(:rails_error_dashboard_error_logs, :environment_info)
6
+ add_column :rails_error_dashboard_error_logs, :environment_info, :text
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddReopenedAtToErrorLogs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ unless column_exists?(:rails_error_dashboard_error_logs, :reopened_at)
6
+ add_column :rails_error_dashboard_error_logs, :reopened_at, :datetime
7
+ end
8
+ end
9
+ end