rails_error_dashboard 0.1.38 → 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.
- checksums.yaml +4 -4
- data/README.md +20 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +1 -0
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +10 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +19 -15
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +19 -9
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +37 -11
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +44 -0
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +38 -16
- data/app/models/rails_error_dashboard/error_log.rb +10 -0
- data/app/models/rails_error_dashboard/error_logs_record.rb +11 -6
- data/app/views/layouts/rails_error_dashboard.html.erb +16 -0
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +3 -0
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +12 -4
- data/app/views/rails_error_dashboard/errors/index.html.erb +7 -5
- data/app/views/rails_error_dashboard/errors/show.html.erb +138 -7
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +36 -0
- data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +1 -1
- data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +1 -1
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +1 -1
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +1 -1
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +1 -1
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +1 -1
- data/db/migrate/20251225100236_create_error_occurrences.rb +1 -1
- data/db/migrate/20251225101920_create_cascade_patterns.rb +1 -1
- data/db/migrate/20251225102500_create_error_baselines.rb +1 -1
- data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +1 -1
- data/db/migrate/20251226020100_create_error_comments.rb +1 -1
- data/db/migrate/20251230075315_cleanup_orphaned_migrations.rb +1 -1
- data/db/migrate/20260220000001_add_exception_cause_to_error_logs.rb +9 -0
- data/db/migrate/20260220000002_add_enriched_context_to_error_logs.rb +12 -0
- data/db/migrate/20260220000003_add_time_series_indexes_to_error_logs.rb +67 -0
- data/db/migrate/20260221000001_add_environment_info_to_error_logs.rb +9 -0
- data/db/migrate/20260221000002_add_reopened_at_to_error_logs.rb +9 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +145 -24
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +12 -8
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +58 -10
- data/lib/rails_error_dashboard/commands/log_error.rb +109 -10
- data/lib/rails_error_dashboard/configuration.rb +52 -0
- data/lib/rails_error_dashboard/manual_error_reporter.rb +12 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +3 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +8 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +8 -0
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +31 -0
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +31 -1
- data/lib/rails_error_dashboard/services/cause_chain_extractor.rb +62 -0
- data/lib/rails_error_dashboard/services/environment_snapshot.rb +85 -0
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +50 -2
- data/lib/rails_error_dashboard/services/notification_throttler.rb +109 -0
- data/lib/rails_error_dashboard/services/platform_detector.rb +36 -11
- data/lib/rails_error_dashboard/services/sensitive_data_filter.rb +176 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +81 -4
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +11 -5
- data/lib/tasks/error_dashboard.rake +158 -2
- metadata +12 -58
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ded53b39ba42ddedec2dea52df11dc8e69c77376bb8204891e6c4f9d3fc165b
|
|
4
|
+
data.tar.gz: c0d80a90c1a2f9dff08f82d4bd40dcc2e1f0336ffe443cc5c96420d059b42a64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fcd8b1b91f2d6a9563ff137b8ceeb50fd82cbda3738792680a3d08b418a204102be98b3527dce1da6e1495a6a0c0647cc85e0d3a073de53e0657fdb8a4d4b490
|
|
7
|
+
data.tar.gz: 0f374fc4577b29cc3165ed5ad756243298d1d5c691a9f0e865c974411a9eec3430b544f8abf176b47d8a1e816d7f26b36eff819a9fe483bbf13798aef801f9ae
|
data/README.md
CHANGED
|
@@ -24,12 +24,12 @@ gem 'rails_error_dashboard'
|
|
|
24
24
|
|
|
25
25
|
Username: `gandalf` · Password: `youshallnotpass`
|
|
26
26
|
|
|
27
|
-
Experience the full dashboard with
|
|
27
|
+
Experience the full dashboard with 480+ realistic Rails errors, LOTR-themed demo data, cause chains, enriched context, auto-reopened errors, and all features enabled.
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
31
|
### ⚠️ BETA SOFTWARE
|
|
32
|
-
This Rails Engine is in beta and under active development. While functional and tested (1,
|
|
32
|
+
This Rails Engine is in beta and under active development. While functional and tested (1,800+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
|
|
33
33
|
|
|
34
34
|
**Supports**: Rails 7.0 - 8.1 | Ruby 3.2 - 4.0
|
|
35
35
|
|
|
@@ -169,6 +169,22 @@ config.git_repository_url = "https://github.com/user/repo"
|
|
|
169
169
|
|
|
170
170
|
**📖 [Complete documentation →](docs/SOURCE_CODE_INTEGRATION.md)**
|
|
171
171
|
|
|
172
|
+
#### 🆕 v0.2 Quick Wins (NEW!)
|
|
173
|
+
|
|
174
|
+
**11 features that make error tracking smarter, safer, and more actionable:**
|
|
175
|
+
|
|
176
|
+
- **Exception Cause Chains** — Automatically captures the full `cause` chain (e.g., `SocketError` → `RuntimeError`) so you see root causes, not just wrappers
|
|
177
|
+
- **Enriched Context** — Every HTTP error captures `http_method`, `hostname`, `content_type`, and `request_duration_ms` automatically
|
|
178
|
+
- **Custom Fingerprint** — Override error grouping with a lambda: group `RecordNotFound` by controller, or any custom logic
|
|
179
|
+
- **CurrentAttributes Integration** — Zero-config capture of `Current.user`, `Current.account`, `Current.request_id`
|
|
180
|
+
- **Environment Info** — Ruby version, Rails version, gem versions, server, and database adapter captured at error time
|
|
181
|
+
- **Sensitive Data Filtering** — Passwords, tokens, secrets, and API keys auto-filtered from error context before storage
|
|
182
|
+
- **Auto-Reopen** — Resolved errors automatically reopen when they recur, with a "Reopened" badge in the UI
|
|
183
|
+
- **Notification Throttling** — Severity filters, per-error cooldown, and milestone threshold alerts prevent alert fatigue
|
|
184
|
+
- **BRIN Indexes** — PostgreSQL BRIN index on `occurred_at` for dramatically faster time-range queries (72KB vs 676MB)
|
|
185
|
+
- **Structured Backtrace** — Uses `backtrace_locations` for richer backtrace data with proper path/line/method fields
|
|
186
|
+
- **Reduced Dependencies** — Core gem now requires only `rails` + `pagy`; `browser`, `chartkick`, `httparty`, `turbo-rails` are optional
|
|
187
|
+
|
|
172
188
|
#### 🔌 Plugin System
|
|
173
189
|
Extensible architecture with event hooks (`on_error_logged`, `on_error_resolved`, `on_threshold_exceeded`). Built-in examples for Jira integration, metrics tracking, audit logging. Easy to create custom plugins - just drop a file in `config/initializers/error_dashboard_plugins/`.
|
|
174
190
|
|
|
@@ -640,7 +656,7 @@ Clean, maintainable, testable architecture you can understand and modify.
|
|
|
640
656
|
|
|
641
657
|
## 🧪 Testing
|
|
642
658
|
|
|
643
|
-
1,
|
|
659
|
+
1,800+ tests covering unit, integration, and browser-based system tests.
|
|
644
660
|
|
|
645
661
|
### Running Tests
|
|
646
662
|
|
|
@@ -733,7 +749,7 @@ Rails Error Dashboard is available as open source under the terms of the [MIT Li
|
|
|
733
749
|
<details>
|
|
734
750
|
<summary><strong>Is this production-ready?</strong></summary>
|
|
735
751
|
|
|
736
|
-
This is currently in **beta** but actively tested with
|
|
752
|
+
This is currently in **beta** but actively tested with 1,800+ passing tests across Rails 7.0-8.1 and Ruby 3.2-4.0. Many users are running it in production. See [production requirements](docs/FEATURES.md#production-readiness).
|
|
737
753
|
</details>
|
|
738
754
|
|
|
739
755
|
<details>
|
|
@@ -10,9 +10,19 @@ module RailsErrorDashboard
|
|
|
10
10
|
# @param exception_data [Hash] Serialized exception data
|
|
11
11
|
# @param context [Hash] Error context (request, user, etc.)
|
|
12
12
|
def perform(exception_data, context)
|
|
13
|
+
# Normalize string keys (ActiveJob may deserialize with string keys)
|
|
14
|
+
exception_data = exception_data.symbolize_keys if exception_data.respond_to?(:symbolize_keys)
|
|
15
|
+
context = context.symbolize_keys if context.respond_to?(:symbolize_keys)
|
|
16
|
+
|
|
13
17
|
# Reconstruct the exception from serialized data
|
|
14
18
|
exception = reconstruct_exception(exception_data)
|
|
15
19
|
|
|
20
|
+
# Pass pre-extracted cause chain via context so LogError can use it
|
|
21
|
+
# (reconstructed exceptions don't have Ruby's built-in cause set)
|
|
22
|
+
if exception_data[:cause_chain]
|
|
23
|
+
context[:_serialized_cause_chain] = exception_data[:cause_chain]
|
|
24
|
+
end
|
|
25
|
+
|
|
16
26
|
# Log the error synchronously in the background job
|
|
17
27
|
# Call .new().call to bypass async check (we're already async)
|
|
18
28
|
Commands::LogError.new(exception, context).call
|
|
@@ -73,11 +73,7 @@ module RailsErrorDashboard
|
|
|
73
73
|
def send_slack_notification(error_log, anomaly_data, config)
|
|
74
74
|
payload = Services::BaselineAlertPayloadBuilder.slack_payload(error_log, anomaly_data)
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
config.slack_webhook_url,
|
|
78
|
-
body: payload.to_json,
|
|
79
|
-
headers: { "Content-Type" => "application/json" }
|
|
80
|
-
)
|
|
76
|
+
post_json(config.slack_webhook_url, payload)
|
|
81
77
|
rescue => e
|
|
82
78
|
Rails.logger.error("Failed to send baseline alert to Slack: #{e.message}")
|
|
83
79
|
end
|
|
@@ -93,11 +89,7 @@ module RailsErrorDashboard
|
|
|
93
89
|
def send_discord_notification(error_log, anomaly_data, config)
|
|
94
90
|
payload = Services::BaselineAlertPayloadBuilder.discord_payload(error_log, anomaly_data)
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
config.discord_webhook_url,
|
|
98
|
-
body: payload.to_json,
|
|
99
|
-
headers: { "Content-Type" => "application/json" }
|
|
100
|
-
)
|
|
92
|
+
post_json(config.discord_webhook_url, payload)
|
|
101
93
|
rescue => e
|
|
102
94
|
Rails.logger.error("Failed to send baseline alert to Discord: #{e.message}")
|
|
103
95
|
end
|
|
@@ -106,16 +98,28 @@ module RailsErrorDashboard
|
|
|
106
98
|
payload = Services::BaselineAlertPayloadBuilder.webhook_payload(error_log, anomaly_data)
|
|
107
99
|
|
|
108
100
|
config.webhook_urls.each do |url|
|
|
109
|
-
|
|
110
|
-
url,
|
|
111
|
-
body: payload.to_json,
|
|
112
|
-
headers: { "Content-Type" => "application/json" }
|
|
113
|
-
)
|
|
101
|
+
post_json(url, payload)
|
|
114
102
|
end
|
|
115
103
|
rescue => e
|
|
116
104
|
Rails.logger.error("Failed to send baseline alert to webhook: #{e.message}")
|
|
117
105
|
end
|
|
118
106
|
|
|
107
|
+
def post_json(url, payload)
|
|
108
|
+
if defined?(HTTParty)
|
|
109
|
+
HTTParty.post(url, body: payload.to_json,
|
|
110
|
+
headers: { "Content-Type" => "application/json" }, timeout: 10)
|
|
111
|
+
else
|
|
112
|
+
uri = URI(url)
|
|
113
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
114
|
+
http.use_ssl = uri.scheme == "https"
|
|
115
|
+
http.open_timeout = 5
|
|
116
|
+
http.read_timeout = 10
|
|
117
|
+
request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
|
|
118
|
+
request.body = payload.to_json
|
|
119
|
+
http.request(request)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
119
123
|
def send_pagerduty_notification(error_log, _anomaly_data, _config)
|
|
120
124
|
Rails.logger.info(
|
|
121
125
|
"Baseline alert PagerDuty notification for #{error_log.error_type}"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "httparty"
|
|
4
|
-
|
|
5
3
|
module RailsErrorDashboard
|
|
6
4
|
# Job to send error notifications to Discord via webhook
|
|
7
5
|
class DiscordErrorNotificationJob < ApplicationJob
|
|
@@ -14,16 +12,28 @@ module RailsErrorDashboard
|
|
|
14
12
|
return unless webhook_url.present?
|
|
15
13
|
|
|
16
14
|
payload = Services::DiscordPayloadBuilder.call(error_log)
|
|
17
|
-
|
|
18
|
-
HTTParty.post(
|
|
19
|
-
webhook_url,
|
|
20
|
-
body: payload.to_json,
|
|
21
|
-
headers: { "Content-Type" => "application/json" },
|
|
22
|
-
timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
|
|
23
|
-
)
|
|
15
|
+
post_json(webhook_url, payload)
|
|
24
16
|
rescue StandardError => e
|
|
25
17
|
Rails.logger.error("[RailsErrorDashboard] Failed to send Discord notification: #{e.message}")
|
|
26
18
|
Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
|
|
27
19
|
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def post_json(url, payload)
|
|
24
|
+
if defined?(HTTParty)
|
|
25
|
+
HTTParty.post(url, body: payload.to_json,
|
|
26
|
+
headers: { "Content-Type" => "application/json" }, timeout: 10)
|
|
27
|
+
else
|
|
28
|
+
uri = URI(url)
|
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
+
http.use_ssl = uri.scheme == "https"
|
|
31
|
+
http.open_timeout = 5
|
|
32
|
+
http.read_timeout = 10
|
|
33
|
+
request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
|
|
34
|
+
request.body = payload.to_json
|
|
35
|
+
http.request(request)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
28
38
|
end
|
|
29
39
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "httparty"
|
|
4
|
-
|
|
5
3
|
module RailsErrorDashboard
|
|
6
4
|
# Job to send critical error notifications to PagerDuty
|
|
7
5
|
# Only triggers for critical severity errors
|
|
@@ -20,20 +18,48 @@ module RailsErrorDashboard
|
|
|
20
18
|
return unless routing_key.present?
|
|
21
19
|
|
|
22
20
|
payload = Services::PagerdutyPayloadBuilder.call(error_log, routing_key: routing_key)
|
|
21
|
+
response = post_json(PAGERDUTY_EVENTS_API, payload)
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
body: payload.to_json,
|
|
27
|
-
headers: { "Content-Type" => "application/json" },
|
|
28
|
-
timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
unless response.success?
|
|
32
|
-
Rails.logger.error("[RailsErrorDashboard] PagerDuty API error: #{response.code} - #{response.body}")
|
|
23
|
+
unless response_success?(response)
|
|
24
|
+
Rails.logger.error("[RailsErrorDashboard] PagerDuty API error: #{response_code(response)} - #{response_body(response)}")
|
|
33
25
|
end
|
|
34
26
|
rescue StandardError => e
|
|
35
27
|
Rails.logger.error("[RailsErrorDashboard] Failed to send PagerDuty notification: #{e.message}")
|
|
36
28
|
Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
|
|
37
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def post_json(url, payload)
|
|
34
|
+
if defined?(HTTParty)
|
|
35
|
+
HTTParty.post(url, body: payload.to_json,
|
|
36
|
+
headers: { "Content-Type" => "application/json" }, timeout: 10)
|
|
37
|
+
else
|
|
38
|
+
uri = URI(url)
|
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
40
|
+
http.use_ssl = uri.scheme == "https"
|
|
41
|
+
http.open_timeout = 5
|
|
42
|
+
http.read_timeout = 10
|
|
43
|
+
request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
|
|
44
|
+
request.body = payload.to_json
|
|
45
|
+
http.request(request)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def response_success?(response)
|
|
50
|
+
if response.respond_to?(:success?)
|
|
51
|
+
response.success?
|
|
52
|
+
else
|
|
53
|
+
response.is_a?(Net::HTTPSuccess)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def response_code(response)
|
|
58
|
+
response.respond_to?(:code) ? response.code : response&.code
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def response_body(response)
|
|
62
|
+
response.respond_to?(:body) ? response.body : response&.body
|
|
63
|
+
end
|
|
38
64
|
end
|
|
39
65
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Background job to enforce the retention_days configuration.
|
|
5
|
+
# Deletes error logs (and their associated records) older than the configured threshold.
|
|
6
|
+
# Uses find_each + destroy to respect dependent: :destroy on associations.
|
|
7
|
+
#
|
|
8
|
+
# Schedule this job daily via your preferred scheduler (SolidQueue, Sidekiq, cron).
|
|
9
|
+
#
|
|
10
|
+
# @example Schedule in initializer
|
|
11
|
+
# RailsErrorDashboard.configure do |config|
|
|
12
|
+
# config.retention_days = 90
|
|
13
|
+
# end
|
|
14
|
+
class RetentionCleanupJob < ApplicationJob
|
|
15
|
+
queue_as :default
|
|
16
|
+
|
|
17
|
+
# @return [Integer] number of errors deleted
|
|
18
|
+
def perform
|
|
19
|
+
retention_days = RailsErrorDashboard.configuration.retention_days
|
|
20
|
+
return 0 if retention_days.blank?
|
|
21
|
+
|
|
22
|
+
cutoff = retention_days.days.ago
|
|
23
|
+
deleted_count = 0
|
|
24
|
+
|
|
25
|
+
# Use find_each to process in batches (default 1000)
|
|
26
|
+
# destroy triggers dependent: :destroy on associations (occurrences, comments, cascades)
|
|
27
|
+
ErrorLog.where("occurred_at < ?", cutoff).find_each do |error_log|
|
|
28
|
+
error_log.destroy
|
|
29
|
+
deleted_count += 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if deleted_count > 0
|
|
33
|
+
RailsErrorDashboard::Logger.info(
|
|
34
|
+
"[RailsErrorDashboard] Retention cleanup: deleted #{deleted_count} errors older than #{retention_days} days"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
deleted_count
|
|
39
|
+
rescue => e
|
|
40
|
+
RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Retention cleanup failed: #{e.class} - #{e.message}")
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "httparty"
|
|
4
|
-
|
|
5
3
|
module RailsErrorDashboard
|
|
6
4
|
# Job to send error notifications to custom webhook URLs
|
|
7
5
|
# Supports multiple webhooks for different integrations
|
|
@@ -30,23 +28,47 @@ module RailsErrorDashboard
|
|
|
30
28
|
private
|
|
31
29
|
|
|
32
30
|
def send_webhook(url, payload, error_log)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
unless response.success?
|
|
46
|
-
Rails.logger.warn("[RailsErrorDashboard] Webhook failed for #{url}: #{response.code}")
|
|
31
|
+
headers = {
|
|
32
|
+
"Content-Type" => "application/json",
|
|
33
|
+
"User-Agent" => "RailsErrorDashboard/1.0",
|
|
34
|
+
"X-Error-Dashboard-Event" => "error.created",
|
|
35
|
+
"X-Error-Dashboard-ID" => error_log.id.to_s
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
response = post_json(url, payload, headers)
|
|
39
|
+
|
|
40
|
+
unless response_success?(response)
|
|
41
|
+
Rails.logger.warn("[RailsErrorDashboard] Webhook failed for #{url}: #{response_code(response)}")
|
|
47
42
|
end
|
|
48
43
|
rescue StandardError => e
|
|
49
44
|
Rails.logger.error("[RailsErrorDashboard] Webhook error for #{url}: #{e.message}")
|
|
50
45
|
end
|
|
46
|
+
|
|
47
|
+
def post_json(url, payload, headers)
|
|
48
|
+
if defined?(HTTParty)
|
|
49
|
+
HTTParty.post(url, body: payload.to_json, headers: headers, timeout: 10)
|
|
50
|
+
else
|
|
51
|
+
uri = URI(url)
|
|
52
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
53
|
+
http.use_ssl = uri.scheme == "https"
|
|
54
|
+
http.open_timeout = 5
|
|
55
|
+
http.read_timeout = 10
|
|
56
|
+
request = Net::HTTP::Post.new(uri.path, headers)
|
|
57
|
+
request.body = payload.to_json
|
|
58
|
+
http.request(request)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def response_success?(response)
|
|
63
|
+
if response.respond_to?(:success?)
|
|
64
|
+
response.success?
|
|
65
|
+
else
|
|
66
|
+
response.is_a?(Net::HTTPSuccess)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def response_code(response)
|
|
71
|
+
response.respond_to?(:code) ? response.code : response&.code
|
|
72
|
+
end
|
|
51
73
|
end
|
|
52
74
|
end
|
|
@@ -4,6 +4,16 @@ module RailsErrorDashboard
|
|
|
4
4
|
class ErrorLog < ErrorLogsRecord
|
|
5
5
|
self.table_name = "rails_error_dashboard_error_logs"
|
|
6
6
|
|
|
7
|
+
# Transient flag: set to true when a resolved/wont_fix error is reopened by FindOrIncrementError.
|
|
8
|
+
# Not persisted — used by LogError to decide notification behavior.
|
|
9
|
+
attr_accessor :just_reopened
|
|
10
|
+
|
|
11
|
+
# Was this error previously resolved and then reopened due to recurrence?
|
|
12
|
+
# Uses the persisted `reopened_at` column (set by FindOrIncrementError).
|
|
13
|
+
def reopened?
|
|
14
|
+
respond_to?(:reopened_at) && reopened_at.present?
|
|
15
|
+
end
|
|
16
|
+
|
|
7
17
|
# Priority level constants
|
|
8
18
|
# Using industry standard: P0 = Critical (highest), P3 = Low (lowest)
|
|
9
19
|
PRIORITY_LEVELS = {
|
|
@@ -5,17 +5,22 @@
|
|
|
5
5
|
# By default, this connects to the same database as the main application.
|
|
6
6
|
#
|
|
7
7
|
# To enable a separate error dashboard database:
|
|
8
|
-
# 1. Set use_separate_database
|
|
9
|
-
# 2. Set database
|
|
10
|
-
# 3.
|
|
11
|
-
# 4. Run: rails db:create
|
|
12
|
-
# 5. Run: rails db:migrate
|
|
8
|
+
# 1. Set config.use_separate_database = true in the gem configuration
|
|
9
|
+
# 2. Set config.database = :error_dashboard in the gem configuration
|
|
10
|
+
# 3. Add an "error_dashboard:" entry in config/database.yml
|
|
11
|
+
# 4. Run: rails db:create:error_dashboard
|
|
12
|
+
# 5. Run: rails db:migrate:error_dashboard
|
|
13
|
+
#
|
|
14
|
+
# For multi-app setups, point all apps' database.yml "error_dashboard:" entry
|
|
15
|
+
# to the same physical database. Each app is identified by config.application_name.
|
|
16
|
+
#
|
|
17
|
+
# Run "rails error_dashboard:verify" to check your setup.
|
|
13
18
|
#
|
|
14
19
|
# Benefits of separate database:
|
|
15
20
|
# - Performance isolation (error logging doesn't slow down user requests)
|
|
16
21
|
# - Independent scaling (can put error DB on separate server)
|
|
22
|
+
# - Multi-app support (centralized error tracking across multiple Rails apps)
|
|
17
23
|
# - Different retention policies (archive old errors without affecting main data)
|
|
18
|
-
# - Security isolation (different access controls for error logs)
|
|
19
24
|
#
|
|
20
25
|
# Trade-offs:
|
|
21
26
|
# - No foreign keys between error_logs and users tables
|
|
@@ -1148,6 +1148,22 @@ body.dark-mode .source-file-path {
|
|
|
1148
1148
|
color: var(--ctp-text) !important; /* Bright text for dark mode */
|
|
1149
1149
|
}
|
|
1150
1150
|
|
|
1151
|
+
/* Backtrace frame number styling */
|
|
1152
|
+
.backtrace-frame-number {
|
|
1153
|
+
display: inline-block;
|
|
1154
|
+
min-width: 2em;
|
|
1155
|
+
text-align: right;
|
|
1156
|
+
margin-right: 0.5em;
|
|
1157
|
+
color: #9ca3af;
|
|
1158
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
1159
|
+
font-size: 0.85em;
|
|
1160
|
+
user-select: none;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
body.dark-mode .backtrace-frame-number {
|
|
1164
|
+
color: var(--ctp-overlay1);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1151
1167
|
/* Backtrace method name styling - override Bootstrap text-info */
|
|
1152
1168
|
span.backtrace-method-name {
|
|
1153
1169
|
color: #0066cc !important; /* Darker blue for better readability in light mode */
|
|
@@ -87,6 +87,9 @@
|
|
|
87
87
|
<% else %>
|
|
88
88
|
<i class="bi bi-exclamation-circle-fill text-danger" data-bs-toggle="tooltip" title="Unresolved"></i>
|
|
89
89
|
<% end %>
|
|
90
|
+
<% if error.reopened? %>
|
|
91
|
+
<i class="bi bi-arrow-counterclockwise text-warning ms-1" data-bs-toggle="tooltip" title="Reopened"></i>
|
|
92
|
+
<% end %>
|
|
90
93
|
</td>
|
|
91
94
|
<td onclick="event.stopPropagation();">
|
|
92
95
|
<%= link_to error_path(error), class: "btn btn-sm btn-outline-primary" do %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="row g-4">
|
|
2
|
-
<div class="col
|
|
2
|
+
<div class="col">
|
|
3
3
|
<div class="card stat-card">
|
|
4
4
|
<div class="card-body">
|
|
5
5
|
<div class="stat-label mb-2">Today</div>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
9
9
|
</div>
|
|
10
|
-
<div class="col
|
|
10
|
+
<div class="col">
|
|
11
11
|
<div class="card stat-card">
|
|
12
12
|
<div class="card-body">
|
|
13
13
|
<div class="stat-label mb-2">This Week</div>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
17
|
</div>
|
|
18
|
-
<div class="col
|
|
18
|
+
<div class="col">
|
|
19
19
|
<div class="card stat-card">
|
|
20
20
|
<div class="card-body">
|
|
21
21
|
<div class="stat-label mb-2">Unresolved</div>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
|
-
<div class="col
|
|
26
|
+
<div class="col">
|
|
27
27
|
<div class="card stat-card">
|
|
28
28
|
<div class="card-body">
|
|
29
29
|
<div class="stat-label mb-2">Resolved</div>
|
|
@@ -31,4 +31,12 @@
|
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
|
+
<div class="col">
|
|
35
|
+
<div class="card stat-card">
|
|
36
|
+
<div class="card-body">
|
|
37
|
+
<div class="stat-label mb-2">Reopened</div>
|
|
38
|
+
<div class="stat-value text-warning"><%= stats[:reopened] %></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
34
42
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<% content_for :page_title, "Errors" %>
|
|
2
2
|
|
|
3
|
-
<!-- Subscribe to Turbo Stream updates (only if
|
|
4
|
-
<% if defined?(ActionCable) %>
|
|
3
|
+
<!-- Subscribe to Turbo Stream updates (only if Turbo Streams is available) -->
|
|
4
|
+
<% if defined?(Turbo::StreamsHelper) && defined?(ActionCable) && respond_to?(:turbo_stream_from) %>
|
|
5
5
|
<%= turbo_stream_from "error_list" %>
|
|
6
6
|
<% end %>
|
|
7
7
|
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
</div>
|
|
75
75
|
<% end %>
|
|
76
76
|
|
|
77
|
-
<!-- 7-Day Error Trend -->
|
|
78
|
-
<% if @stats[:errors_trend_7d]&.any? %>
|
|
77
|
+
<!-- 7-Day Error Trend (requires chartkick gem) -->
|
|
78
|
+
<% if @stats[:errors_trend_7d]&.any? && respond_to?(:line_chart) %>
|
|
79
79
|
<div class="row g-4 mb-4">
|
|
80
80
|
<div class="col-md-8">
|
|
81
81
|
<div class="card">
|
|
@@ -186,9 +186,11 @@
|
|
|
186
186
|
<!-- Quick Filter Buttons -->
|
|
187
187
|
<div class="d-flex gap-2 mb-3">
|
|
188
188
|
<%= link_to "All Errors", errors_path,
|
|
189
|
-
class: "btn btn-sm #{params[:assigned_to].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
189
|
+
class: "btn btn-sm #{params[:assigned_to].blank? && params[:reopened].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
190
190
|
<%= link_to "Unassigned", errors_path(assigned_to: '__unassigned__'),
|
|
191
191
|
class: "btn btn-sm #{params[:assigned_to] == '__unassigned__' ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
192
|
+
<%= link_to "Reopened", errors_path(reopened: 'true'),
|
|
193
|
+
class: "btn btn-sm #{params[:reopened] == 'true' ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
192
194
|
</div>
|
|
193
195
|
|
|
194
196
|
<%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
|