rails_error_dashboard 0.14.4 → 0.16.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 +19 -8
- data/app/controllers/rails_error_dashboard/application_controller.rb +28 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +97 -4
- data/app/controllers/rails_error_dashboard/health_controller.rb +38 -0
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +10 -9
- data/app/helpers/rails_error_dashboard/application_helper.rb +2 -2
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +63 -16
- data/app/helpers/rails_error_dashboard/overview_helper.rb +4 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +20 -2
- data/app/jobs/rails_error_dashboard/cascade_detection_job.rb +30 -0
- data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +33 -6
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +1 -1
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +1 -1
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +31 -0
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +6 -1
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +1 -1
- data/app/jobs/rails_error_dashboard/telegram_error_notification_job.rb +25 -0
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +2 -18
- data/app/models/rails_error_dashboard/error_log.rb +54 -20
- data/app/models/rails_error_dashboard/error_occurrence.rb +1 -0
- data/app/models/rails_error_dashboard/error_occurrence_context.rb +14 -0
- data/app/models/rails_error_dashboard/missing_translation.rb +43 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +14 -2
- data/app/views/rails_error_dashboard/errors/_captured_occurrences.html.erb +45 -0
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +12 -3
- data/app/views/rails_error_dashboard/errors/_occurrence_banner.html.erb +12 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +3 -1
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +3 -9
- data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -0
- data/app/views/rails_error_dashboard/errors/boot_errors.html.erb +130 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/missing_translations.html.erb +143 -0
- data/app/views/rails_error_dashboard/errors/overview.html.erb +10 -7
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +16 -11
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +7 -4
- data/app/views/rails_error_dashboard/errors/settings.html.erb +23 -8
- data/app/views/rails_error_dashboard/errors/show.html.erb +7 -4
- data/config/locales/de.yml +87 -5
- data/config/locales/en.yml +99 -4
- data/config/locales/es.yml +88 -5
- data/config/locales/fr.yml +88 -5
- data/config/locales/it.yml +88 -5
- data/config/locales/ja.yml +86 -4
- data/config/locales/pl.yml +89 -5
- data/config/locales/pt-BR.yml +88 -5
- data/config/locales/ru.yml +89 -5
- data/config/locales/uk.yml +89 -5
- data/config/locales/zh-CN.yml +86 -4
- data/config/routes.rb +6 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +10 -2
- data/db/migrate/20260106094256_backfill_application_for_existing_errors.rb +3 -3
- data/db/migrate/20261010000001_create_missing_translations.rb +50 -0
- data/db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb +35 -0
- data/db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb +46 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +9 -4
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +41 -5
- data/lib/rails_error_dashboard/commands/assign_error.rb +24 -1
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +11 -2
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +16 -3
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +7 -2
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +6 -0
- data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +101 -0
- data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +2 -4
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +7 -6
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +2 -4
- data/lib/rails_error_dashboard/commands/log_error.rb +20 -8
- data/lib/rails_error_dashboard/commands/record_occurrence_context.rb +60 -0
- data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -15
- data/lib/rails_error_dashboard/commands/update_error_status.rb +7 -0
- data/lib/rails_error_dashboard/configuration.rb +61 -4
- data/lib/rails_error_dashboard/engine.rb +42 -1
- data/lib/rails_error_dashboard/error_reporter.rb +4 -2
- data/lib/rails_error_dashboard/manual_error_reporter.rb +11 -1
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +18 -4
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +7 -1
- data/lib/rails_error_dashboard/plugin.rb +19 -3
- data/lib/rails_error_dashboard/plugin_registry.rb +61 -12
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +0 -12
- data/lib/rails_error_dashboard/queries/boot_errors.rb +76 -0
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +59 -33
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +3 -11
- data/lib/rails_error_dashboard/queries/errors_list.rb +8 -16
- data/lib/rails_error_dashboard/queries/health_status.rb +191 -0
- data/lib/rails_error_dashboard/queries/missing_translation_summary.rb +82 -0
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +14 -6
- data/lib/rails_error_dashboard/services/active_storage_probe.rb +91 -0
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +13 -7
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +31 -7
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +30 -0
- data/lib/rails_error_dashboard/services/crash_capture.rb +90 -2
- data/lib/rails_error_dashboard/services/digest_builder.rb +9 -6
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +6 -0
- data/lib/rails_error_dashboard/services/github_link_generator.rb +25 -14
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +15 -16
- data/lib/rails_error_dashboard/services/missing_translation_handler.rb +152 -0
- data/lib/rails_error_dashboard/services/missing_translation_tracker.rb +316 -0
- data/lib/rails_error_dashboard/services/repository_url.rb +18 -0
- data/lib/rails_error_dashboard/services/resolution_callbacks.rb +44 -0
- data/lib/rails_error_dashboard/services/severity_classifier.rb +52 -0
- data/lib/rails_error_dashboard/services/similarity_calculator.rb +3 -9
- data/lib/rails_error_dashboard/services/source_code_reader.rb +23 -3
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +6 -3
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +6 -1
- data/lib/rails_error_dashboard/services/telegram_delivery.rb +134 -0
- data/lib/rails_error_dashboard/services/telegram_payload_builder.rb +105 -0
- data/lib/rails_error_dashboard/services/webhook_delivery.rb +63 -0
- data/lib/rails_error_dashboard/services/webhook_signer.rb +80 -0
- data/lib/rails_error_dashboard/value_objects/occurrence_snapshot.rb +23 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +28 -0
- data/lib/tasks/error_dashboard.rake +2 -3
- metadata +29 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84ad49626b8dddc0665ed515f3fc8e3357adadddcb5991679324cffa40d2aa2e
|
|
4
|
+
data.tar.gz: af06a63fb80ded2960e23f6e6c25ace1a741b85e13f014ae4bfbfd4b4b16980a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3154ebfa3b1d46d497a86cfca33dca5a09ed98127a699e7a9eb17ce3d05380f807172b377a2a824fbffff806e6f1e9a6c6ba96e8cb61802368e81ffc56ea93a
|
|
7
|
+
data.tar.gz: 571ca72a5e588371357a427c02a2ebed3bf565cf70f03b8c8e5877245e060bd7d8ddff873ae50235ac1c4103c6944d4f5e852c221766f7292225b4af8406ceca
|
data/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Open `/red`, then send a test error from its Settings page. No monitoring accoun
|
|
|
23
23
|
|
|
24
24
|
[Try the live demo](https://rails-error-dashboard.anjan.dev) (`gandalf` / `youshallnotpass`) · [Read the documentation](https://anjanj.github.io/rails_error_dashboard/) · [View on RubyGems](https://rubygems.org/gems/rails_error_dashboard)
|
|
25
25
|
|
|
26
|
-
> **Beta:** RED is functional and extensively tested, but configuration and APIs may change before 1.0. Supports Rails 7.0–8.1 and Ruby 3.2–4.0 (CI runs Ruby 3.2
|
|
26
|
+
> **Beta:** RED is functional and extensively tested, but configuration and APIs may change before 1.0. Supports Rails 7.0–8.1 and Ruby 3.2–4.0 (CI runs every Ruby from 3.2 to 4.0 against every supported Rails version).
|
|
27
27
|
|
|
28
28
|
## See the Ruby state and Rails runtime health behind every exception
|
|
29
29
|
|
|
@@ -208,6 +208,8 @@ Cross-error N+1 detection grouped by SQL fingerprint, and aggregate deprecation
|
|
|
208
208
|
|
|
209
209
|
Requires breadcrumbs to be enabled. Deprecations are seen only when the host's `ActiveSupport::Deprecation` behaviour includes `:notify` (the production default does not) and only inside requests that later raised.
|
|
210
210
|
|
|
211
|
+
**Missing translations** — opt in with `config.enable_missing_translation_tracking = true` and RED counts every key your app looked up and did not have, by locale, with the first call site, on its own page. No breadcrumbs needed; it wraps `I18n.exception_handler` and delegates to the handler you already had. [Details →](docs/FEATURES.md#missing-translation-tracking)
|
|
212
|
+
|
|
211
213
|
[Complete documentation →](docs/FEATURES.md#n1-query-detection)
|
|
212
214
|
</details>
|
|
213
215
|
|
|
@@ -232,12 +234,14 @@ Requires breadcrumbs to be enabled. Deprecations are seen only when the host's `
|
|
|
232
234
|
config.enable_actioncable_tracking = true # requires enable_breadcrumbs = true
|
|
233
235
|
```
|
|
234
236
|
|
|
235
|
-
**ActiveStorage Health** — Track file uploads, downloads, deletes, and existence checks across storage services (Disk, S3, GCS, Azure — any ActiveStorage backend). Dashboard page at `/errors/activestorage_health_summary` with per-service operation counts, average and slowest durations. Helps identify slow storage operations correlating with errors.
|
|
237
|
+
**ActiveStorage Health** — Track file uploads, downloads, deletes, and existence checks across storage services (Disk, S3, GCS, Azure — any ActiveStorage backend). Dashboard page at `/errors/activestorage_health_summary` with per-service operation counts, average and slowest durations. Helps identify slow storage operations correlating with errors. The page opens with a live reachability card (one `exist?` for a key that is never stored, against the configured service), and `GET /error_dashboard/health` reports the same probe as its `active_storage` check.
|
|
236
238
|
|
|
237
239
|
```ruby
|
|
238
240
|
config.enable_activestorage_tracking = true # requires enable_breadcrumbs = true
|
|
239
241
|
```
|
|
240
242
|
|
|
243
|
+
**Health Check** — `GET /error_dashboard/health` answers in JSON whether the dashboard itself can still capture and show errors: the error database (`SELECT 1`, tables present), last capture time, the Active Job adapter and Solid Queue config problems, the storm breaker state and the gem version. 503 when the error database is down, so an uptime monitor can watch the watchmen. Authenticated like every other route. [Reference →](docs/API_REFERENCE.md#health-check)
|
|
244
|
+
|
|
241
245
|
[Complete documentation →](docs/FEATURES.md#job-health-page)
|
|
242
246
|
</details>
|
|
243
247
|
|
|
@@ -390,7 +394,7 @@ config.git_repository_url = "https://github.com/user/repo" # enables SHA links
|
|
|
390
394
|
<details>
|
|
391
395
|
<summary><strong>Source Code Integration + Git Blame</strong></summary>
|
|
392
396
|
|
|
393
|
-
View actual source code directly in error backtraces
|
|
397
|
+
View actual source code directly in error backtraces, 5 lines before and after the error line by default. Git blame shows who last modified the code, when, and the commit message. Repository links jump to the exact line on GitHub, GitLab, Bitbucket or Codeberg.
|
|
394
398
|
|
|
395
399
|
```ruby
|
|
396
400
|
config.enable_source_code_integration = true
|
|
@@ -430,7 +434,7 @@ config.llm_model = "gpt-5"
|
|
|
430
434
|
</details>
|
|
431
435
|
|
|
432
436
|
<details>
|
|
433
|
-
<summary><strong>Notifications — Slack, Discord, PagerDuty, Email, Webhooks</strong></summary>
|
|
437
|
+
<summary><strong>Notifications — Slack, Discord, Telegram, PagerDuty, Email, Webhooks</strong></summary>
|
|
434
438
|
|
|
435
439
|
Multi-channel alerting with severity filters, per-error cooldown, milestone threshold alerts, and a per-environment allowlist (`config.notification_environments = %w[production]`) so a staging deploy never pages anyone.
|
|
436
440
|
|
|
@@ -439,6 +443,8 @@ config.enable_slack_notifications = true
|
|
|
439
443
|
config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
|
|
440
444
|
```
|
|
441
445
|
|
|
446
|
+
Custom webhooks can carry an HMAC-SHA256 signature (`config.webhook_signing_secret`) so the receiver can verify the sender and reject replays.
|
|
447
|
+
|
|
442
448
|
[Notification setup guide →](docs/guides/NOTIFICATIONS.md)
|
|
443
449
|
</details>
|
|
444
450
|
|
|
@@ -556,7 +562,8 @@ config.enable_rack_attack_tracking = true
|
|
|
556
562
|
Capture unhandled exceptions that crash the Ruby process via an `at_exit` hook — the last line of defense.
|
|
557
563
|
|
|
558
564
|
- Disk-based fallback: writes crash data to disk because the database may be unavailable during shutdown
|
|
559
|
-
- Imported automatically on next boot
|
|
565
|
+
- Imported automatically on next boot; a crash that repeats on every restart is one row with a count
|
|
566
|
+
- Boot crashes too: the hook is registered before Rails eager-loads, so a `Zeitwerk::NameError` or a `SyntaxError` that kills the process before it serves a request lands on the **Boot Errors** page under Diagnostics, with the file and constant it names
|
|
560
567
|
- Captures exception details, backtrace, uptime, GC stats, thread count, and cause chain
|
|
561
568
|
- Honeybadger, Bugsnag and AppSignal have `at_exit` reporters too; RED's writes to disk and imports at next boot because the database may already be gone during shutdown
|
|
562
569
|
|
|
@@ -570,14 +577,18 @@ config.enable_crash_capture = true
|
|
|
570
577
|
<details>
|
|
571
578
|
<summary><strong>Plugin System</strong></summary>
|
|
572
579
|
|
|
573
|
-
Event-driven extensibility with hooks
|
|
580
|
+
Event-driven extensibility with hooks such as `on_error_logged`, `on_error_reopened` and `on_error_resolved` (eleven in all). Built-in examples for Jira integration, metrics tracking, and audit logging.
|
|
574
581
|
|
|
575
582
|
```ruby
|
|
576
583
|
class MyPlugin < RailsErrorDashboard::Plugin
|
|
584
|
+
def name = "My Plugin"
|
|
585
|
+
|
|
577
586
|
def on_error_logged(error_log)
|
|
578
587
|
# Your custom logic
|
|
579
588
|
end
|
|
580
589
|
end
|
|
590
|
+
|
|
591
|
+
RailsErrorDashboard.register_plugin(MyPlugin.new)
|
|
581
592
|
```
|
|
582
593
|
|
|
583
594
|
[Plugin System guide →](docs/PLUGIN_SYSTEM.md)
|
|
@@ -721,7 +732,7 @@ Users can also switch language per-session from the picker in the dashboard navb
|
|
|
721
732
|
|
|
722
733
|
**Corrections are very welcome, and a one-key PR is a perfectly good PR.** If you read one of these languages, [every unreviewed locale has an open issue](https://github.com/AnjanJ/rails_error_dashboard/issues?q=is%3Aissue+is%3Aopen+label%3Atranslation%3Aneeds-review) tracking its review — comment there, or [report a bad translation](https://github.com/AnjanJ/rails_error_dashboard/issues/new?template=translation_report.yml) without touching any code. You do not need to know Ruby, and you are not expected to review a whole file.
|
|
723
734
|
|
|
724
|
-
RED translates through its own private I18n backend, so it never reads, writes or mutates your application's `I18n` configuration — your locale and its `available_locales` are untouched.
|
|
735
|
+
RED translates through its own private I18n backend, so it never reads, writes or mutates your application's `I18n` configuration — your locale and its `available_locales` are untouched. The one exception is opt-in: `enable_missing_translation_tracking` wraps `I18n.exception_handler` to count *your* app's missing keys, and delegates every call to the handler you had.
|
|
725
736
|
|
|
726
737
|
[Translations guide →](docs/guides/TRANSLATIONS.md) — how the system works, how to correct a string, and how to add a locale.
|
|
727
738
|
|
|
@@ -779,7 +790,7 @@ Rails 7.0–8.1 and Ruby 3.2–4.0.
|
|
|
779
790
|
### Advanced
|
|
780
791
|
- **[Multi-App Support](docs/MULTI_APP_PERFORMANCE.md)** — Track multiple applications
|
|
781
792
|
- **[Plugin System](docs/PLUGIN_SYSTEM.md)** — Build custom integrations
|
|
782
|
-
- **[API Reference](docs/API_REFERENCE.md)** —
|
|
793
|
+
- **[API Reference](docs/API_REFERENCE.md)** — The dashboard's HTTP routes and the Ruby API
|
|
783
794
|
- **[Customization](docs/CUSTOMIZATION.md)** — Customize everything
|
|
784
795
|
- **[Database Options](docs/guides/DATABASE_OPTIONS.md)** — Separate database setup
|
|
785
796
|
- **[Database Optimization](docs/guides/DATABASE_OPTIMIZATION.md)** — Performance tuning
|
|
@@ -227,15 +227,39 @@ module RailsErrorDashboard
|
|
|
227
227
|
end
|
|
228
228
|
|
|
229
229
|
# Page size for every paginated action. Only the upper bound is enforced
|
|
230
|
-
# here: a value
|
|
231
|
-
# so that
|
|
230
|
+
# here: a value Pagy would reject (0, a negative number, a Hash) is passed
|
|
231
|
+
# on so that it does and the rescue above redirects, as it always has.
|
|
232
|
+
#
|
|
233
|
+
# Strings are read the way Pagy reads them, with to_i, so the cap sees the
|
|
234
|
+
# same number Pagy would use: Integer() refused "150.5" and "0999" and the
|
|
235
|
+
# raw string went through, where Pagy's to_i made them 150 and 999.
|
|
232
236
|
def per_page_param
|
|
233
237
|
raw = params[:per_page]
|
|
234
238
|
return DEFAULT_PER_PAGE if raw.blank?
|
|
235
239
|
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
value = raw.is_a?(String) ? raw.to_i : raw
|
|
241
|
+
value.is_a?(Integer) && value > MAX_PER_PAGE ? MAX_PER_PAGE : value
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The named query-string keys as a symbol-keyed Hash of their String values.
|
|
245
|
+
#
|
|
246
|
+
# Not params.permit: permit judges every key in the request, so a stray
|
|
247
|
+
# utm_source, or the `page` and `locale` the dashboard itself appends, was
|
|
248
|
+
# an "unpermitted parameter" on every index request. Under the default
|
|
249
|
+
# that is a log line; in a host with action_on_unpermitted_parameters =
|
|
250
|
+
# :raise it was a 500 on the errors page. Picking the keys we want and
|
|
251
|
+
# keeping only scalar values does what permit was meant to do here without
|
|
252
|
+
# passing judgement on the rest of the request.
|
|
253
|
+
#
|
|
254
|
+
# @param keys [Array<Symbol>]
|
|
255
|
+
# @return [Hash{Symbol => String}]
|
|
256
|
+
def scalar_params(keys)
|
|
257
|
+
keys.each_with_object({}) do |key, hash|
|
|
258
|
+
value = params[key]
|
|
259
|
+
hash[key] = value if value.is_a?(String)
|
|
260
|
+
end
|
|
238
261
|
end
|
|
262
|
+
helper_method :scalar_params
|
|
239
263
|
|
|
240
264
|
def render_dashboard_error(icon:, title:, message:, detail: nil, icon_style: nil, status: :internal_server_error)
|
|
241
265
|
# The styled page runs queries and names the host app's applications. A
|
|
@@ -119,6 +119,11 @@ module RailsErrorDashboard
|
|
|
119
119
|
@related_errors = @error.related_errors(limit: 5, application_id: @current_application_id)
|
|
120
120
|
@error_markdown = Services::MarkdownErrorFormatter.call(@error, related_errors: @related_errors)
|
|
121
121
|
|
|
122
|
+
# ?occurrence=ID: the context cards show that occurrence's stored context
|
|
123
|
+
@occurrence_context = find_occurrence_context(@error)
|
|
124
|
+
@snapshot = @occurrence_context ? ValueObjects::OccurrenceSnapshot.build(@error, @occurrence_context) : @error
|
|
125
|
+
@captured_occurrences = captured_occurrences(@error)
|
|
126
|
+
|
|
122
127
|
# Fetch platform issue state and comments if linked
|
|
123
128
|
@platform_issue = fetch_platform_issue(@error)
|
|
124
129
|
@platform_comments = fetch_platform_comments(@error)
|
|
@@ -279,8 +284,6 @@ module RailsErrorDashboard
|
|
|
279
284
|
@errors_by_hour = analytics[:errors_by_hour]
|
|
280
285
|
@top_users = analytics[:top_users]
|
|
281
286
|
@resolution_rate = analytics[:resolution_rate]
|
|
282
|
-
@mobile_errors = analytics[:mobile_errors]
|
|
283
|
-
@api_errors = analytics[:api_errors]
|
|
284
287
|
|
|
285
288
|
# Get recurring issues data (pass application filter)
|
|
286
289
|
recurring = Queries::RecurringIssues.call(days, application_id: @current_application_id)
|
|
@@ -583,6 +586,52 @@ module RailsErrorDashboard
|
|
|
583
586
|
@pagy, @events = pagy(:offset, all_events, limit: per_page_param)
|
|
584
587
|
end
|
|
585
588
|
|
|
589
|
+
def missing_translations
|
|
590
|
+
unless RailsErrorDashboard.configuration.enable_missing_translation_tracking
|
|
591
|
+
flash[:alert] = feature_disabled_message("missing_translations", options: "enable_missing_translation_tracking = true", set: true)
|
|
592
|
+
redirect_to errors_path(**app_context_params)
|
|
593
|
+
return
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
days = days_param(default: 30)
|
|
597
|
+
@days = days
|
|
598
|
+
result = Queries::MissingTranslationSummary.call(days, application_id: @current_application_id)
|
|
599
|
+
all_entries = result[:entries]
|
|
600
|
+
|
|
601
|
+
# Summary stats (computed before pagination). The list is one row per
|
|
602
|
+
# (locale, key), so the badge and pagination count rows; the tile
|
|
603
|
+
# counts distinct keys, which is what "how many keys are missing" means.
|
|
604
|
+
@entry_count = all_entries.size
|
|
605
|
+
@unique_keys = all_entries.map { |e| e[:key] }.uniq.size
|
|
606
|
+
@total_misses = all_entries.sum { |e| e[:count] }
|
|
607
|
+
@locale_count = result[:locales].size
|
|
608
|
+
# Misses the tracker's bounded buffer dropped; shown so the total above
|
|
609
|
+
# is never silently understated.
|
|
610
|
+
@overflow_count = result[:overflow_count].to_i
|
|
611
|
+
|
|
612
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: per_page_param)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def boot_errors
|
|
616
|
+
unless RailsErrorDashboard.configuration.enable_crash_capture
|
|
617
|
+
flash[:alert] = feature_disabled_message("boot_errors", options: "enable_crash_capture = true", set: true)
|
|
618
|
+
redirect_to errors_path(**app_context_params)
|
|
619
|
+
return
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
days = days_param(default: 30)
|
|
623
|
+
@days = days
|
|
624
|
+
result = Queries::BootErrors.call(days, application_id: @current_application_id)
|
|
625
|
+
all_entries = result[:entries]
|
|
626
|
+
|
|
627
|
+
# Summary stats (computed before pagination)
|
|
628
|
+
@entry_count = all_entries.size
|
|
629
|
+
@unresolved_count = result[:unresolved_count]
|
|
630
|
+
@total_occurrences = all_entries.sum { |e| e[:count] }
|
|
631
|
+
|
|
632
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: per_page_param)
|
|
633
|
+
end
|
|
634
|
+
|
|
586
635
|
def actioncable_health_summary
|
|
587
636
|
unless RailsErrorDashboard.configuration.enable_actioncable_tracking &&
|
|
588
637
|
RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
@@ -634,6 +683,9 @@ module RailsErrorDashboard
|
|
|
634
683
|
|
|
635
684
|
days = days_param(default: 30)
|
|
636
685
|
@days = days
|
|
686
|
+
# Live reachability of the configured service, shown above the
|
|
687
|
+
# historical breadcrumb stats. One existence check per page load.
|
|
688
|
+
@storage_probe = Services::ActiveStorageProbe.call
|
|
637
689
|
result = Queries::ActiveStorageSummary.call(days, application_id: @current_application_id)
|
|
638
690
|
all_services = result[:services]
|
|
639
691
|
|
|
@@ -719,7 +771,17 @@ module RailsErrorDashboard
|
|
|
719
771
|
)
|
|
720
772
|
exception.set_backtrace(caller)
|
|
721
773
|
|
|
722
|
-
|
|
774
|
+
started_at = Time.current
|
|
775
|
+
error_log = Commands::LogError.call(exception, { request: request, source: "dashboard.test_error" })
|
|
776
|
+
|
|
777
|
+
# The capture path notifies on a first occurrence and then on the
|
|
778
|
+
# 10th, 50th... A second click within 24 hours is occurrence two of the
|
|
779
|
+
# same row, so it used to notify nobody, which reads as "notifications
|
|
780
|
+
# are broken". This button exists to prove delivery: if the capture
|
|
781
|
+
# path did not claim a notification in this request, send one now.
|
|
782
|
+
if error_log && !notified_since?(error_log, started_at)
|
|
783
|
+
Services::ErrorNotificationDispatcher.call(error_log)
|
|
784
|
+
end
|
|
723
785
|
|
|
724
786
|
flash[:notice] = red_t("red.flash.test_error.logged")
|
|
725
787
|
redirect_to errors_path(**app_context_params)
|
|
@@ -785,7 +847,14 @@ module RailsErrorDashboard
|
|
|
785
847
|
end
|
|
786
848
|
|
|
787
849
|
def filter_params
|
|
788
|
-
|
|
850
|
+
scalar_params(FILTERABLE_PARAMS)
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
# True when NotificationThrottler claimed a notification for this error
|
|
854
|
+
# at or after +since+. The claim is an update_all, so the row is re-read.
|
|
855
|
+
def notified_since?(error_log, since)
|
|
856
|
+
stamp = error_log.class.where(id: error_log.id).pick(:last_notified_at)
|
|
857
|
+
stamp.present? && stamp >= since
|
|
789
858
|
end
|
|
790
859
|
|
|
791
860
|
# Coerce params[:days] into a sane integer in [1, 365]. Without clamping,
|
|
@@ -835,6 +904,30 @@ module RailsErrorDashboard
|
|
|
835
904
|
red_t("red.errors.row.status.#{status}", default: status.to_s.tr("_", " "))
|
|
836
905
|
end
|
|
837
906
|
|
|
907
|
+
# Looked up through the error, so another error's occurrence id finds
|
|
908
|
+
# nothing. nil (the page shows the latest snapshot) when there is no id, it
|
|
909
|
+
# is foreign or unknown, or its context is no longer kept.
|
|
910
|
+
def find_occurrence_context(error)
|
|
911
|
+
return if params[:occurrence].blank?
|
|
912
|
+
# An app that updated the gem but has not migrated yet
|
|
913
|
+
return unless ErrorOccurrenceContext.table_exists?
|
|
914
|
+
|
|
915
|
+
error.occurrence_contexts.find_by(error_occurrence_id: params[:occurrence])
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# This error's occurrences that still have a stored context, newest first.
|
|
919
|
+
# The context table is read for its occurrence ids only, so listing them
|
|
920
|
+
# never loads the stored payloads.
|
|
921
|
+
def captured_occurrences(error)
|
|
922
|
+
return [] unless ErrorOccurrenceContext.table_exists?
|
|
923
|
+
|
|
924
|
+
error.error_occurrences
|
|
925
|
+
.where(id: ErrorOccurrenceContext.where(error_log_id: error.id).select(:error_occurrence_id))
|
|
926
|
+
.order(occurred_at: :desc, id: :desc)
|
|
927
|
+
.select(:id, :occurred_at, :app_version, :git_sha)
|
|
928
|
+
.to_a
|
|
929
|
+
end
|
|
930
|
+
|
|
838
931
|
def fetch_platform_issue(error)
|
|
839
932
|
return nil unless error.external_issue_url.present? && error.external_issue_number.present?
|
|
840
933
|
return nil unless RailsErrorDashboard.configuration.enable_issue_tracking
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# GET /health — can the dashboard itself still capture and show errors?
|
|
5
|
+
#
|
|
6
|
+
# JSON only, for an uptime monitor: 200 when the error database answers,
|
|
7
|
+
# 503 when it does not (status "down"). "degraded" (a storm in progress,
|
|
8
|
+
# a Solid Queue config that will not run RED's jobs) is still a 200: the
|
|
9
|
+
# dashboard works, with a caveat the body spells out. See
|
|
10
|
+
# Queries::HealthStatus for what is probed and why.
|
|
11
|
+
#
|
|
12
|
+
# Authenticated like every other dashboard route (ApplicationController
|
|
13
|
+
# prepends the filter): the body names the database adapter and the queue
|
|
14
|
+
# backend, which is more than an anonymous caller should learn. Basic auth
|
|
15
|
+
# works from any monitor; with authenticate_with, point the monitor at
|
|
16
|
+
# whatever that lambda accepts.
|
|
17
|
+
#
|
|
18
|
+
# Its own controller rather than an ErrorsController action: no
|
|
19
|
+
# application context, no storm banner, no error data is loaded here.
|
|
20
|
+
class HealthController < ApplicationController
|
|
21
|
+
# ApplicationController's handler renders the HTML error page. A health
|
|
22
|
+
# endpoint must answer in JSON whatever happens, so this one, declared
|
|
23
|
+
# later, takes precedence.
|
|
24
|
+
rescue_from StandardError do |exception|
|
|
25
|
+
Rails.logger.error("[RailsErrorDashboard] Health check failed: #{exception.class} - #{exception.message}")
|
|
26
|
+
render json: { status: Queries::HealthStatus::DOWN, version: RailsErrorDashboard::VERSION,
|
|
27
|
+
error: exception.class.name },
|
|
28
|
+
status: :service_unavailable
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def show
|
|
32
|
+
result = Queries::HealthStatus.call
|
|
33
|
+
|
|
34
|
+
response.headers["Cache-Control"] = "no-store"
|
|
35
|
+
render json: result, status: Queries::HealthStatus.http_status_for(result)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -14,7 +14,12 @@ module RailsErrorDashboard
|
|
|
14
14
|
class WebhooksController < ActionController::Base
|
|
15
15
|
skip_before_action :verify_authenticity_token
|
|
16
16
|
|
|
17
|
+
PROVIDERS = %w[github gitlab codeberg linear].freeze
|
|
18
|
+
|
|
17
19
|
before_action :verify_webhook_enabled
|
|
20
|
+
# Before the signature check: an unknown provider is a wrong URL, and a 401
|
|
21
|
+
# would send the sender looking at a secret that is fine
|
|
22
|
+
before_action :verify_provider
|
|
18
23
|
before_action :verify_signature
|
|
19
24
|
|
|
20
25
|
def receive
|
|
@@ -32,9 +37,6 @@ module RailsErrorDashboard
|
|
|
32
37
|
handle_codeberg(payload)
|
|
33
38
|
when "linear"
|
|
34
39
|
handle_linear(payload)
|
|
35
|
-
else
|
|
36
|
-
head :not_found
|
|
37
|
-
return
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
head :ok
|
|
@@ -52,16 +54,15 @@ module RailsErrorDashboard
|
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
|
|
57
|
+
def verify_provider
|
|
58
|
+
head :not_found unless PROVIDERS.include?(params[:provider])
|
|
59
|
+
end
|
|
60
|
+
|
|
55
61
|
def verify_signature
|
|
56
62
|
provider = params[:provider]
|
|
63
|
+
# Present: verify_webhook_enabled answers 404 without it
|
|
57
64
|
secret = RailsErrorDashboard.configuration.issue_webhook_secret
|
|
58
65
|
|
|
59
|
-
unless secret.present?
|
|
60
|
-
Rails.logger.warn("[RailsErrorDashboard] Webhook received but no issue_webhook_secret configured")
|
|
61
|
-
head :unauthorized
|
|
62
|
-
return
|
|
63
|
-
end
|
|
64
|
-
|
|
65
66
|
body = request.body.read
|
|
66
67
|
request.body.rewind
|
|
67
68
|
|
|
@@ -163,8 +163,8 @@ module RailsErrorDashboard
|
|
|
163
163
|
# @return [Hash] Whitelisted params for building URLs
|
|
164
164
|
def permitted_filter_params(extra_keys: [])
|
|
165
165
|
base_keys = RailsErrorDashboard::ErrorsController::FILTERABLE_PARAMS + %i[page per_page days]
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
# scalar_params, not params.permit: see ApplicationController#scalar_params.
|
|
167
|
+
scalar_params(base_keys + Array(extra_keys))
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
# Generates a sortable column header link
|
|
@@ -7,6 +7,59 @@ module RailsErrorDashboard
|
|
|
7
7
|
# one view context.
|
|
8
8
|
include I18nHelper
|
|
9
9
|
|
|
10
|
+
# The brand name for a repository link's tooltip, from the URL's host.
|
|
11
|
+
# A substring test on the whole URL called every non-GitHub, non-GitLab
|
|
12
|
+
# host "Bitbucket", Codeberg included, and a path containing "github"
|
|
13
|
+
# mislabelled any host.
|
|
14
|
+
def repository_host_label(url)
|
|
15
|
+
host = URI.parse(url.to_s).host.to_s.downcase
|
|
16
|
+
case host
|
|
17
|
+
when /github/ then "GitHub"
|
|
18
|
+
when /gitlab/ then "GitLab"
|
|
19
|
+
when /bitbucket/ then "Bitbucket"
|
|
20
|
+
when /codeberg/ then "Codeberg"
|
|
21
|
+
when /gitea/ then "Gitea"
|
|
22
|
+
when /forgejo/ then "Forgejo"
|
|
23
|
+
else host.presence || "Repository"
|
|
24
|
+
end
|
|
25
|
+
rescue URI::InvalidURIError
|
|
26
|
+
"Repository"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The error-info fragment renders the source viewer, blame, repository
|
|
30
|
+
# links and the coverage overlay, none of which come from the error row
|
|
31
|
+
# the fragment is keyed on. These three helpers put the rest of the key's
|
|
32
|
+
# inputs in: the configuration those parts depend on, the source cache
|
|
33
|
+
# TTL as the fragment's lifetime, and no caching at all while coverage is
|
|
34
|
+
# active, because that overlay is live data.
|
|
35
|
+
def source_view_cache_key
|
|
36
|
+
config = RailsErrorDashboard.configuration
|
|
37
|
+
# The gem version too, for the same reason as read_source_code's key
|
|
38
|
+
parts = [
|
|
39
|
+
RailsErrorDashboard::VERSION,
|
|
40
|
+
config.enable_source_code_integration, config.enable_git_blame, config.only_show_app_code_source,
|
|
41
|
+
config.git_repository_url, config.git_branch_strategy
|
|
42
|
+
]
|
|
43
|
+
Digest::SHA1.hexdigest(parts.map(&:to_s).join("|"))[0, 12]
|
|
44
|
+
rescue StandardError
|
|
45
|
+
"nocfg"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def source_view_cacheable?
|
|
49
|
+
!(defined?(Services::CoverageTracker) && Services::CoverageTracker.active?)
|
|
50
|
+
rescue StandardError
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def source_view_cache_options
|
|
55
|
+
config = RailsErrorDashboard.configuration
|
|
56
|
+
return {} unless config.enable_source_code_integration
|
|
57
|
+
|
|
58
|
+
{ expires_in: (config.source_code_cache_ttl || 3600).to_i.seconds }
|
|
59
|
+
rescue StandardError
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
10
63
|
# Language mapping for syntax highlighting
|
|
11
64
|
LANGUAGE_MAP = {
|
|
12
65
|
".rb" => "ruby",
|
|
@@ -152,8 +205,9 @@ module RailsErrorDashboard
|
|
|
152
205
|
return nil unless RailsErrorDashboard.configuration.enable_source_code_integration
|
|
153
206
|
return nil unless frame[:file_path] && frame[:line_number]
|
|
154
207
|
|
|
155
|
-
#
|
|
156
|
-
|
|
208
|
+
# The gem version is in the key: a cached read must not outlive a change
|
|
209
|
+
# to the reader's rules for which files it may read.
|
|
210
|
+
cache_key = "source_code/#{RailsErrorDashboard::VERSION}/#{frame[:file_path]}/#{frame[:line_number]}"
|
|
157
211
|
cache_ttl = RailsErrorDashboard.configuration.source_code_cache_ttl || 3600
|
|
158
212
|
|
|
159
213
|
Rails.cache.fetch(cache_key, expires_in: cache_ttl) do
|
|
@@ -238,15 +292,19 @@ module RailsErrorDashboard
|
|
|
238
292
|
|
|
239
293
|
# Determine which commit SHA to use based on strategy
|
|
240
294
|
def determine_commit_sha(error_log)
|
|
241
|
-
|
|
295
|
+
# A String (from ENV, say) means the same as the Symbol
|
|
296
|
+
strategy = (RailsErrorDashboard.configuration.git_branch_strategy || :commit_sha).to_s.to_sym
|
|
242
297
|
|
|
243
298
|
case strategy
|
|
244
299
|
when :commit_sha
|
|
245
300
|
# Use the SHA from when the error occurred (most accurate)
|
|
246
301
|
error_log.respond_to?(:git_sha) ? error_log.git_sha : nil
|
|
247
302
|
when :current_branch
|
|
248
|
-
#
|
|
249
|
-
|
|
303
|
+
# The commit this process is running: config.git_sha, the deploy ENV
|
|
304
|
+
# variables, then the .git HEAD read once at boot. Never `git rev-parse`
|
|
305
|
+
# here: this renders on a host request thread, and a subprocess with
|
|
306
|
+
# no timeout is the pattern the capture path already removed.
|
|
307
|
+
Commands::LogError.capture_git_sha
|
|
250
308
|
when :main
|
|
251
309
|
# Use main/master branch
|
|
252
310
|
nil # Will default to "main" in GithubLinkGenerator
|
|
@@ -254,16 +312,5 @@ module RailsErrorDashboard
|
|
|
254
312
|
nil
|
|
255
313
|
end
|
|
256
314
|
end
|
|
257
|
-
|
|
258
|
-
# Get current git SHA from repository
|
|
259
|
-
def get_current_git_sha
|
|
260
|
-
return @current_git_sha if defined?(@current_git_sha)
|
|
261
|
-
|
|
262
|
-
@current_git_sha = begin
|
|
263
|
-
`git rev-parse HEAD 2>/dev/null`.strip.presence
|
|
264
|
-
rescue StandardError
|
|
265
|
-
nil
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
315
|
end
|
|
269
316
|
end
|
|
@@ -67,6 +67,8 @@ module RailsErrorDashboard
|
|
|
67
67
|
"success"
|
|
68
68
|
when :warning
|
|
69
69
|
"warning"
|
|
70
|
+
when :not_rated
|
|
71
|
+
"secondary"
|
|
70
72
|
else
|
|
71
73
|
"danger"
|
|
72
74
|
end
|
|
@@ -82,6 +84,8 @@ module RailsErrorDashboard
|
|
|
82
84
|
red_t("red.common.health_status.healthy")
|
|
83
85
|
when :warning
|
|
84
86
|
red_t("red.common.health_status.warning")
|
|
87
|
+
when :not_rated
|
|
88
|
+
red_t("red.common.health_status.not_rated")
|
|
85
89
|
else
|
|
86
90
|
red_t("red.common.health_status.critical")
|
|
87
91
|
end
|
|
@@ -59,6 +59,11 @@ module RailsErrorDashboard
|
|
|
59
59
|
send_discord_notification(error_log, anomaly_data, config, locale)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Telegram notification
|
|
63
|
+
if Services::TelegramDelivery.configured?(config)
|
|
64
|
+
send_telegram_notification(error_log, anomaly_data, config, locale)
|
|
65
|
+
end
|
|
66
|
+
|
|
62
67
|
# Webhook notification
|
|
63
68
|
if config.enable_webhook_notifications && config.webhook_urls.any?
|
|
64
69
|
send_webhook_notification(error_log, anomaly_data, config, locale)
|
|
@@ -96,11 +101,24 @@ module RailsErrorDashboard
|
|
|
96
101
|
Rails.logger.error("Failed to send baseline alert to Discord: #{e.message}")
|
|
97
102
|
end
|
|
98
103
|
|
|
104
|
+
def send_telegram_notification(error_log, anomaly_data, _config, locale)
|
|
105
|
+
payload = Services::BaselineAlertPayloadBuilder.telegram_payload(error_log, anomaly_data, locale: locale)
|
|
106
|
+
|
|
107
|
+
Services::TelegramDelivery.post(payload)
|
|
108
|
+
rescue => e
|
|
109
|
+
Rails.logger.error("Failed to send baseline alert to Telegram: #{Services::TelegramDelivery.redact(e.message)}")
|
|
110
|
+
end
|
|
111
|
+
|
|
99
112
|
def send_webhook_notification(error_log, anomaly_data, config, locale)
|
|
100
113
|
payload = Services::BaselineAlertPayloadBuilder.webhook_payload(error_log, anomaly_data, locale: locale)
|
|
101
114
|
|
|
115
|
+
# Signed when webhook_signing_secret is set; Slack and Discord above
|
|
116
|
+
# keep post_json because their payloads are not ours to sign. Rescued
|
|
117
|
+
# per URL so one unreachable receiver does not stop the next.
|
|
102
118
|
config.webhook_urls.each do |url|
|
|
103
|
-
|
|
119
|
+
Services::WebhookDelivery.post(url, payload)
|
|
120
|
+
rescue => e
|
|
121
|
+
Rails.logger.error("Failed to send baseline alert to webhook: #{e.message}")
|
|
104
122
|
end
|
|
105
123
|
rescue => e
|
|
106
124
|
Rails.logger.error("Failed to send baseline alert to webhook: #{e.message}")
|
|
@@ -116,7 +134,7 @@ module RailsErrorDashboard
|
|
|
116
134
|
http.use_ssl = uri.scheme == "https"
|
|
117
135
|
http.open_timeout = 5
|
|
118
136
|
http.read_timeout = 10
|
|
119
|
-
request = Net::HTTP::Post.new(uri.
|
|
137
|
+
request = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" })
|
|
120
138
|
request.body = payload.to_json
|
|
121
139
|
http.request(request)
|
|
122
140
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Finds error cascades (A reliably followed by B) in recent occurrences and
|
|
5
|
+
# upserts them as CascadePattern rows, which the error page's Cascades card
|
|
6
|
+
# reads.
|
|
7
|
+
#
|
|
8
|
+
# Nothing in the gem schedules this: run it from the host's scheduler like
|
|
9
|
+
# the other periodic jobs (docs/PRODUCTION.md). Before this job existed the
|
|
10
|
+
# docs said cascades were detected "hourly" by a background job that no
|
|
11
|
+
# code path ever ran, so the card stayed empty on every install.
|
|
12
|
+
#
|
|
13
|
+
# Use a lookback equal to the interval it runs at: an hourly run with a
|
|
14
|
+
# 24-hour lookback re-counts the same pairs 24 times a day.
|
|
15
|
+
class CascadeDetectionJob < ApplicationJob
|
|
16
|
+
queue_as :default
|
|
17
|
+
|
|
18
|
+
# @param lookback_hours [Integer] the window of occurrences to scan
|
|
19
|
+
def perform(lookback_hours: 1)
|
|
20
|
+
return { detected: 0, updated: 0, skipped: "enable_error_cascades is off" } unless RailsErrorDashboard.configuration.enable_error_cascades
|
|
21
|
+
|
|
22
|
+
result = Services::CascadeDetector.call(lookback_hours: lookback_hours.to_i.clamp(1, 24 * 30))
|
|
23
|
+
Rails.logger.info("[RailsErrorDashboard] Cascade detection: #{result[:detected]} detected, #{result[:updated]} updated")
|
|
24
|
+
result
|
|
25
|
+
rescue => e
|
|
26
|
+
Rails.logger.error("[RailsErrorDashboard] CascadeDetectionJob failed: #{e.class}: #{e.message}")
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|