rails_error_dashboard 0.5.10 → 0.5.12

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -1
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +31 -0
  4. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +12 -0
  5. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +40 -0
  6. data/app/mailers/rails_error_dashboard/digest_mailer.rb +23 -0
  7. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +2 -1
  8. data/app/views/layouts/rails_error_dashboard.html.erb +5 -0
  9. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +172 -0
  10. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +49 -0
  11. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -10
  12. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -7
  13. data/app/views/rails_error_dashboard/errors/settings.html.erb +7 -3
  14. data/app/views/rails_error_dashboard/errors/show.html.erb +21 -0
  15. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +172 -0
  16. data/config/routes.rb +3 -0
  17. data/lib/generators/rails_error_dashboard/install/install_generator.rb +173 -160
  18. data/lib/generators/rails_error_dashboard/install/templates/README +9 -18
  19. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +15 -10
  20. data/lib/rails_error_dashboard/configuration.rb +83 -26
  21. data/lib/rails_error_dashboard/engine.rb +10 -0
  22. data/lib/rails_error_dashboard/error_reporter.rb +26 -0
  23. data/lib/rails_error_dashboard/middleware/error_catcher.rb +7 -0
  24. data/lib/rails_error_dashboard/middleware/rate_limiter.rb +16 -12
  25. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -1
  26. data/lib/rails_error_dashboard/queries/user_impact_summary.rb +93 -0
  27. data/lib/rails_error_dashboard/services/coverage_tracker.rb +139 -0
  28. data/lib/rails_error_dashboard/services/digest_builder.rb +158 -0
  29. data/lib/rails_error_dashboard/services/notification_helpers.rb +2 -1
  30. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +2 -1
  31. data/lib/rails_error_dashboard/value_objects/error_context.rb +5 -0
  32. data/lib/rails_error_dashboard/version.rb +1 -1
  33. data/lib/rails_error_dashboard.rb +3 -0
  34. data/lib/tasks/error_dashboard.rake +23 -0
  35. metadata +33 -9
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ # Pure algorithm: Build a digest summary of error activity for a time period.
6
+ # Aggregates stats from existing queries into a single hash suitable for email templates.
7
+ #
8
+ # @example
9
+ # DigestBuilder.call(period: :daily)
10
+ # # => { period: :daily, stats: { new_errors: 12, ... }, top_errors: [...], ... }
11
+ class DigestBuilder
12
+ PERIODS = {
13
+ daily: { days: 1, label: "Last 24 hours" },
14
+ weekly: { days: 7, label: "Last 7 days" }
15
+ }.freeze
16
+
17
+ def self.call(period: :daily, application_id: nil)
18
+ new(period: period, application_id: application_id).call
19
+ end
20
+
21
+ def initialize(period: :daily, application_id: nil)
22
+ @period = PERIODS.key?(period) ? period : :daily
23
+ @days = PERIODS[@period][:days]
24
+ @application_id = application_id
25
+ @start_date = @days.days.ago
26
+ end
27
+
28
+ def call
29
+ {
30
+ period: @period,
31
+ period_label: PERIODS[@period][:label],
32
+ generated_at: Time.current,
33
+ stats: build_stats,
34
+ top_errors: top_errors,
35
+ critical_unresolved: critical_unresolved,
36
+ comparison: build_comparison
37
+ }
38
+ rescue => e
39
+ Rails.logger.error("[RailsErrorDashboard] DigestBuilder failed: #{e.class}: #{e.message}")
40
+ empty_result
41
+ end
42
+
43
+ private
44
+
45
+ def base_scope
46
+ scope = ErrorLog.where("occurred_at >= ?", @start_date)
47
+ scope = scope.where(application_id: @application_id) if @application_id.present?
48
+ scope
49
+ end
50
+
51
+ def build_stats
52
+ scope = base_scope
53
+
54
+ new_errors = scope.where("occurrence_count <= 1").count
55
+ total_occurrences = scope.sum(:occurrence_count)
56
+ resolved = scope.where(resolved: true).count
57
+ unresolved = scope.where(resolved: false).count
58
+ # Severity is computed from error_type via SeverityClassifier (not a DB column).
59
+ # Count critical+high by matching known error type patterns via SQL WHERE IN.
60
+ critical_types = Services::SeverityClassifier::CRITICAL_ERROR_TYPES +
61
+ Services::SeverityClassifier::HIGH_SEVERITY_ERROR_TYPES
62
+ critical_high = scope.where(error_type: critical_types).count
63
+
64
+ total = resolved + unresolved
65
+ resolution_rate = total > 0 ? (resolved.to_f / total * 100).round(1) : 0
66
+
67
+ {
68
+ new_errors: new_errors,
69
+ total_occurrences: total_occurrences,
70
+ resolved: resolved,
71
+ unresolved: unresolved,
72
+ critical_high: critical_high,
73
+ resolution_rate: resolution_rate
74
+ }
75
+ rescue => e
76
+ Rails.logger.error("[RailsErrorDashboard] DigestBuilder.build_stats failed: #{e.class}: #{e.message}")
77
+ { new_errors: 0, total_occurrences: 0, resolved: 0, unresolved: 0, critical_high: 0, resolution_rate: 0 }
78
+ end
79
+
80
+ def top_errors
81
+ base_scope
82
+ .where(resolved: false)
83
+ .group(:error_type)
84
+ .order("count_all DESC")
85
+ .limit(5)
86
+ .count
87
+ .map do |error_type, count|
88
+ sample = base_scope.where(error_type: error_type).order(occurred_at: :desc).first
89
+ {
90
+ error_type: error_type,
91
+ message: sample&.message.to_s.truncate(100),
92
+ count: count,
93
+ id: sample&.id
94
+ }
95
+ end
96
+ rescue => e
97
+ Rails.logger.error("[RailsErrorDashboard] DigestBuilder.top_errors failed: #{e.class}: #{e.message}")
98
+ []
99
+ end
100
+
101
+ def critical_unresolved
102
+ critical_types = Services::SeverityClassifier::CRITICAL_ERROR_TYPES +
103
+ Services::SeverityClassifier::HIGH_SEVERITY_ERROR_TYPES
104
+ base_scope
105
+ .where(resolved: false)
106
+ .where(error_type: critical_types)
107
+ .order(occurred_at: :desc)
108
+ .limit(5)
109
+ .map do |error|
110
+ {
111
+ error_type: error.error_type,
112
+ message: error.message.to_s.truncate(100),
113
+ severity: error.severity,
114
+ id: error.id
115
+ }
116
+ end
117
+ rescue => e
118
+ Rails.logger.error("[RailsErrorDashboard] DigestBuilder.critical_unresolved failed: #{e.class}: #{e.message}")
119
+ []
120
+ end
121
+
122
+ def build_comparison
123
+ previous_start = (@days * 2).days.ago
124
+ previous_end = @start_date
125
+
126
+ current_count = base_scope.count
127
+ previous_scope = ErrorLog.where("occurred_at >= ? AND occurred_at < ?", previous_start, previous_end)
128
+ previous_scope = previous_scope.where(application_id: @application_id) if @application_id.present?
129
+ previous_count = previous_scope.count
130
+
131
+ delta = current_count - previous_count
132
+ percentage = previous_count > 0 ? ((delta.to_f / previous_count) * 100).round(1) : nil
133
+
134
+ {
135
+ current_count: current_count,
136
+ previous_count: previous_count,
137
+ error_delta: delta,
138
+ error_delta_percentage: percentage
139
+ }
140
+ rescue => e
141
+ Rails.logger.error("[RailsErrorDashboard] DigestBuilder.build_comparison failed: #{e.class}: #{e.message}")
142
+ { current_count: 0, previous_count: 0, error_delta: 0, error_delta_percentage: nil }
143
+ end
144
+
145
+ def empty_result
146
+ {
147
+ period: @period,
148
+ period_label: PERIODS.dig(@period, :label) || "Unknown",
149
+ generated_at: Time.current,
150
+ stats: { new_errors: 0, total_occurrences: 0, resolved: 0, unresolved: 0, critical_high: 0, resolution_rate: 0 },
151
+ top_errors: [],
152
+ critical_unresolved: [],
153
+ comparison: { current_count: 0, previous_count: 0, error_delta: 0, error_delta_percentage: nil }
154
+ }
155
+ end
156
+ end
157
+ end
158
+ end
@@ -14,7 +14,8 @@ module RailsErrorDashboard
14
14
  # @return [String] Full URL to the error detail page
15
15
  def dashboard_url(error_log)
16
16
  base_url = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
17
- "#{base_url}/error_dashboard/errors/#{error_log.id}"
17
+ mount_path = RailsErrorDashboard.configuration.engine_mount_path
18
+ "#{base_url}#{mount_path}/errors/#{error_log.id}"
18
19
  end
19
20
 
20
21
  # Truncate a message to a maximum length
@@ -13,7 +13,8 @@ module RailsErrorDashboard
13
13
  # Called when a new error is first logged
14
14
  def on_error_logged(error_log)
15
15
  return unless should_auto_create?(error_log)
16
- CreateIssueJob.perform_later(error_log.id)
16
+ dashboard_url = Services::NotificationHelpers.dashboard_url(error_log)
17
+ CreateIssueJob.perform_later(error_log.id, dashboard_url: dashboard_url)
17
18
  rescue => e
18
19
  nil
19
20
  end
@@ -110,6 +110,11 @@ module RailsErrorDashboard
110
110
  # Additional context (from mobile apps, etc.)
111
111
  params.merge!(@context[:additional_context]) if @context[:additional_context]
112
112
 
113
+ # Pre-serialized params (from async logging or double-ErrorContext path).
114
+ # LogError creates a second ErrorContext from error_context.to_h which
115
+ # has :request_params as a JSON string but no :request object.
116
+ return @context[:request_params] if params.empty? && @context[:request_params].present?
117
+
113
118
  params.to_json
114
119
  end
115
120
 
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.5.10"
2
+ VERSION = "0.5.12"
3
3
  end
@@ -67,6 +67,9 @@ require "rails_error_dashboard/services/local_variable_capturer"
67
67
  require "rails_error_dashboard/services/swallowed_exception_tracker"
68
68
  require "rails_error_dashboard/services/crash_capture"
69
69
  require "rails_error_dashboard/services/diagnostic_dump_generator"
70
+ require "rails_error_dashboard/services/coverage_tracker"
71
+ require "rails_error_dashboard/services/digest_builder"
72
+ require "rails_error_dashboard/queries/user_impact_summary"
70
73
  require "rails_error_dashboard/subscribers/breadcrumb_subscriber"
71
74
  require "rails_error_dashboard/subscribers/rack_attack_subscriber"
72
75
  require "rails_error_dashboard/subscribers/action_cable_subscriber"
@@ -525,4 +525,27 @@ namespace :error_dashboard do
525
525
 
526
526
  puts "\n" + "=" * 80 + "\n"
527
527
  end
528
+
529
+ desc "Send error digest email (PERIOD=daily|weekly, APP_ID=optional)"
530
+ task send_digest: :environment do
531
+ period = ENV.fetch("PERIOD", "daily")
532
+ app_id = ENV["APP_ID"]
533
+
534
+ unless RailsErrorDashboard.configuration.enable_scheduled_digests
535
+ puts "Scheduled digests are not enabled. Set config.enable_scheduled_digests = true"
536
+ next
537
+ end
538
+
539
+ recipients = RailsErrorDashboard.configuration.digest_recipients ||
540
+ RailsErrorDashboard.configuration.notification_email_recipients
541
+
542
+ if recipients.blank?
543
+ puts "No digest recipients configured. Set config.digest_recipients or config.notification_email_recipients"
544
+ next
545
+ end
546
+
547
+ puts "Sending #{period} error digest to #{recipients.join(', ')}..."
548
+ RailsErrorDashboard::ScheduledDigestJob.perform_later(period: period, application_id: app_id)
549
+ puts "Digest job enqueued."
550
+ end
528
551
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.10
4
+ version: 0.5.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -260,10 +260,12 @@ files:
260
260
  - app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb
261
261
  - app/jobs/rails_error_dashboard/reopen_linked_issue_job.rb
262
262
  - app/jobs/rails_error_dashboard/retention_cleanup_job.rb
263
+ - app/jobs/rails_error_dashboard/scheduled_digest_job.rb
263
264
  - app/jobs/rails_error_dashboard/slack_error_notification_job.rb
264
265
  - app/jobs/rails_error_dashboard/swallowed_exception_flush_job.rb
265
266
  - app/jobs/rails_error_dashboard/webhook_error_notification_job.rb
266
267
  - app/mailers/rails_error_dashboard/application_mailer.rb
268
+ - app/mailers/rails_error_dashboard/digest_mailer.rb
267
269
  - app/mailers/rails_error_dashboard/error_notification_mailer.rb
268
270
  - app/models/rails_error_dashboard/application.rb
269
271
  - app/models/rails_error_dashboard/cascade_pattern.rb
@@ -275,6 +277,8 @@ files:
275
277
  - app/models/rails_error_dashboard/error_occurrence.rb
276
278
  - app/models/rails_error_dashboard/swallowed_exception.rb
277
279
  - app/views/layouts/rails_error_dashboard.html.erb
280
+ - app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb
281
+ - app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb
278
282
  - app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb
279
283
  - app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb
280
284
  - app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb
@@ -315,6 +319,7 @@ files:
315
319
  - app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb
316
320
  - app/views/rails_error_dashboard/errors/show.html.erb
317
321
  - app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb
322
+ - app/views/rails_error_dashboard/errors/user_impact.html.erb
318
323
  - config/routes.rb
319
324
  - db/development.sqlite3
320
325
  - db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb
@@ -418,6 +423,7 @@ files:
418
423
  - lib/rails_error_dashboard/queries/release_timeline.rb
419
424
  - lib/rails_error_dashboard/queries/similar_errors.rb
420
425
  - lib/rails_error_dashboard/queries/swallowed_exception_summary.rb
426
+ - lib/rails_error_dashboard/queries/user_impact_summary.rb
421
427
  - lib/rails_error_dashboard/services/analytics_cache_manager.rb
422
428
  - lib/rails_error_dashboard/services/backtrace_parser.rb
423
429
  - lib/rails_error_dashboard/services/backtrace_processor.rb
@@ -429,10 +435,12 @@ files:
429
435
  - lib/rails_error_dashboard/services/cascade_detector.rb
430
436
  - lib/rails_error_dashboard/services/cause_chain_extractor.rb
431
437
  - lib/rails_error_dashboard/services/codeberg_issue_client.rb
438
+ - lib/rails_error_dashboard/services/coverage_tracker.rb
432
439
  - lib/rails_error_dashboard/services/crash_capture.rb
433
440
  - lib/rails_error_dashboard/services/curl_generator.rb
434
441
  - lib/rails_error_dashboard/services/database_health_inspector.rb
435
442
  - lib/rails_error_dashboard/services/diagnostic_dump_generator.rb
443
+ - lib/rails_error_dashboard/services/digest_builder.rb
436
444
  - lib/rails_error_dashboard/services/discord_payload_builder.rb
437
445
  - lib/rails_error_dashboard/services/environment_snapshot.rb
438
446
  - lib/rails_error_dashboard/services/error_broadcaster.rb
@@ -486,14 +494,30 @@ metadata:
486
494
  documentation_uri: https://AnjanJ.github.io/rails_error_dashboard
487
495
  bug_tracker_uri: https://github.com/AnjanJ/rails_error_dashboard/issues
488
496
  funding_uri: https://github.com/sponsors/AnjanJ
489
- post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
490
- \ Rails Error Dashboard v0.5.10\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
491
- First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
492
- db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
493
- => '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n
494
- \ \U0001F4DD Changelog: https://github.com/AnjanJ/rails_error_dashboard/blob/main/CHANGELOG.md\n\n\U0001F3AE
495
- Live demo: https://rails-error-dashboard.anjan.dev\n (gandalf / youshallnotpass)\n\n\U0001F4D6
496
- Full docs: https://github.com/AnjanJ/rails_error_dashboard\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
497
+ post_install_message: |
498
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
499
+ RED (Rails Error Dashboard) v0.5.12
500
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
501
+
502
+ First install:
503
+ rails generate rails_error_dashboard:install
504
+ rails db:migrate
505
+ # Route and config are set up automatically by the generator.
506
+
507
+ Upgrading from a previous version:
508
+ rails generate rails_error_dashboard:install
509
+ rails db:migrate
510
+ # The generator detects your existing config and only adds new migrations.
511
+
512
+ Separate database users:
513
+ rails generate rails_error_dashboard:install
514
+ rails db:migrate:error_dashboard
515
+ # See docs for full separate-DB setup.
516
+
517
+ Live demo: https://rails-error-dashboard.anjan.dev
518
+ Full docs: https://github.com/AnjanJ/rails_error_dashboard
519
+ Changelog: https://github.com/AnjanJ/rails_error_dashboard/blob/main/CHANGELOG.md
520
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
497
521
  rdoc_options: []
498
522
  require_paths:
499
523
  - lib