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.
- checksums.yaml +4 -4
- data/README.md +34 -1
- data/app/controllers/rails_error_dashboard/errors_controller.rb +31 -0
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +12 -0
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +40 -0
- data/app/mailers/rails_error_dashboard/digest_mailer.rb +23 -0
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +2 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +5 -0
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +172 -0
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +49 -0
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -10
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -7
- data/app/views/rails_error_dashboard/errors/settings.html.erb +7 -3
- data/app/views/rails_error_dashboard/errors/show.html.erb +21 -0
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +172 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +173 -160
- data/lib/generators/rails_error_dashboard/install/templates/README +9 -18
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +15 -10
- data/lib/rails_error_dashboard/configuration.rb +83 -26
- data/lib/rails_error_dashboard/engine.rb +10 -0
- data/lib/rails_error_dashboard/error_reporter.rb +26 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +7 -0
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +16 -12
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -1
- data/lib/rails_error_dashboard/queries/user_impact_summary.rb +93 -0
- data/lib/rails_error_dashboard/services/coverage_tracker.rb +139 -0
- data/lib/rails_error_dashboard/services/digest_builder.rb +158 -0
- data/lib/rails_error_dashboard/services/notification_helpers.rb +2 -1
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +2 -1
- data/lib/rails_error_dashboard/value_objects/error_context.rb +5 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- data/lib/tasks/error_dashboard.rake +23 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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.
|
|
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:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|