rails_error_dashboard 0.14.3 → 0.15.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 +45 -17
- data/app/controllers/rails_error_dashboard/application_controller.rb +28 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +68 -2
- data/app/controllers/rails_error_dashboard/health_controller.rb +38 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +10 -2
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +56 -2
- 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 +26 -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 +43 -12
- data/app/models/rails_error_dashboard/missing_translation.rb +43 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +24 -1
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +3 -1
- 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/settings/_value_badge.html.erb +7 -4
- data/app/views/rails_error_dashboard/errors/settings.html.erb +19 -5
- data/config/locales/de.yml +72 -5
- data/config/locales/en.yml +83 -4
- data/config/locales/es.yml +72 -5
- data/config/locales/fr.yml +72 -5
- data/config/locales/it.yml +72 -5
- data/config/locales/ja.yml +72 -4
- data/config/locales/pl.yml +72 -5
- data/config/locales/pt-BR.yml +72 -5
- data/config/locales/ru.yml +72 -5
- data/config/locales/uk.yml +72 -5
- data/config/locales/zh-CN.yml +72 -4
- data/config/routes.rb +6 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +10 -2
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +9 -0
- data/db/migrate/20261010000001_create_missing_translations.rb +50 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +40 -38
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +31 -3
- data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +11 -2
- data/lib/rails_error_dashboard/commands/assign_error.rb +24 -1
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +9 -1
- data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +103 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +6 -3
- 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 +75 -2
- data/lib/rails_error_dashboard/engine.rb +52 -5
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +21 -43
- data/lib/rails_error_dashboard/plugin.rb +49 -3
- data/lib/rails_error_dashboard/plugin_registry.rb +61 -12
- data/lib/rails_error_dashboard/queries/boot_errors.rb +76 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +3 -11
- data/lib/rails_error_dashboard/queries/errors_list.rb +11 -17
- 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/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 +15 -0
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +30 -0
- data/lib/rails_error_dashboard/services/crash_capture.rb +85 -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/exception_filter.rb +4 -1
- data/lib/rails_error_dashboard/services/github_link_generator.rb +25 -9
- 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/resolution_callbacks.rb +44 -0
- data/lib/rails_error_dashboard/services/severity_classifier.rb +52 -0
- data/lib/rails_error_dashboard/services/solid_queue_config_check.rb +47 -0
- data/lib/rails_error_dashboard/services/source_code_reader.rb +23 -3
- 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/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +12 -0
- data/lib/tasks/error_dashboard.rake +43 -2
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4e3190d52a36d5c36abb07855ac5090194c0e7d3e426f38565724d60c4d54c2
|
|
4
|
+
data.tar.gz: 22c63cf4f49ee2f97b6898b3ab19e81bdf45f5c382a29c6c771c5d462d760dec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd736e679c40f293b58ad44b47f3215d001e45cd13c322d18f4033d178c8bc91242c018ccff8b676797d3282e1aa321e2ab15cc96050eabdd8bb3df42f6b7f63
|
|
7
|
+
data.tar.gz: '0696afc78d045942b741eaa166054455bd36b2962fb097cd67b7db8817c749243e3c70de3737c2e04aedd4b7cb52cd18ffb4d360a0b1d4805ff69e1ca4059716'
|
data/README.md
CHANGED
|
@@ -19,11 +19,11 @@ rails generate rails_error_dashboard:install
|
|
|
19
19
|
rails db:migrate
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Open `/red
|
|
22
|
+
Open `/red`, then send a test error from its Settings page. No monitoring account or ingestion service is required.
|
|
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
|
|
|
@@ -91,7 +91,7 @@ That makes RED a self-hosted Sentry alternative for teams that want Rails-specif
|
|
|
91
91
|
|
|
92
92
|
- Store data in the application's existing PostgreSQL, MySQL/Trilogy or SQLite database.
|
|
93
93
|
- Isolate monitoring writes in a separate error database.
|
|
94
|
-
- Use synchronous writes, or async logging through
|
|
94
|
+
- Use synchronous writes, or async logging through your app's own Active Job backend (Sidekiq, Solid Queue, GoodJob or Rails' in-process `:async`).
|
|
95
95
|
- Track several Rails applications through a shared database.
|
|
96
96
|
|
|
97
97
|
No RED licence or event-ingestion fee, and no plan limits — your database is the only cap, and storm protection deliberately sheds context during floods.
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -619,6 +626,11 @@ Span names follow the `rails_error_dashboard.<operation>` convention, e.g. `rail
|
|
|
619
626
|
gem 'rails_error_dashboard'
|
|
620
627
|
```
|
|
621
628
|
|
|
629
|
+
RED requires Rails 7.0 or newer, Ruby 3.2 or newer, `pagy ~> 43` and `groupdate ~> 6`. An app that pins an older Pagy or groupdate must upgrade it first, or `bundle install` fails.
|
|
630
|
+
|
|
631
|
+
- **Rails before 8.1.4 or 7.2.4** (every 8.0, 7.1 and 7.0 release): also add `gem "json", "< 3"`. json 3 breaks those Rails versions, and the dashboard returns 500. See [the json pin](docs/UPGRADING.md#rails-before-814-or-724-pin-json-below-3).
|
|
632
|
+
- **MySQL**: load the time-zone tables (`mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql`), or the charts fail.
|
|
633
|
+
|
|
622
634
|
### 2. Install with Interactive Setup
|
|
623
635
|
|
|
624
636
|
```bash
|
|
@@ -627,7 +639,9 @@ rails generate rails_error_dashboard:install
|
|
|
627
639
|
rails db:migrate
|
|
628
640
|
```
|
|
629
641
|
|
|
630
|
-
The installer
|
|
642
|
+
The installer asks about notifications and advanced analytics, then where to store errors: in your app's database (the default), a separate one, or one shared by several apps. Without a terminal (CI, Docker) it asks nothing: async logging is on, and every other optional feature is off.
|
|
643
|
+
|
|
644
|
+
**Chose a separate or shared database?** Add the `config/database.yml` entry the installer prints, then run `bin/rails db:create` before `db:migrate`. See [Database Options](docs/guides/DATABASE_OPTIONS.md).
|
|
631
645
|
|
|
632
646
|
### 3. Visit your dashboard
|
|
633
647
|
|
|
@@ -637,17 +651,30 @@ http://localhost:3000/red
|
|
|
637
651
|
|
|
638
652
|
Default credentials: `gandalf` / `youshallnotpass`, for development and test only.
|
|
639
653
|
|
|
640
|
-
**Before you deploy anywhere else**, set `ERROR_DASHBOARD_USER` and `ERROR_DASHBOARD_PASSWORD` in that environment. Outside development and test, the app refuses to boot on the defaults. See [Dashboard Credentials](docs/guides/CONFIGURATION.md#dashboard-credentials).
|
|
654
|
+
**Before you deploy anywhere else**, set `ERROR_DASHBOARD_USER` and `ERROR_DASHBOARD_PASSWORD` in that environment. Outside development and test, the app refuses to boot on the defaults. See [Dashboard Credentials](docs/guides/CONFIGURATION.md#dashboard-credentials). For the rest of what production needs (a worker, scheduled jobs, migrations on deploy), see [Running in Production](docs/PRODUCTION.md).
|
|
641
655
|
|
|
642
656
|
### 4. Test it out
|
|
643
657
|
|
|
658
|
+
Open `/red/settings` and click **Send Test Error**, or raise an exception in any controller action. Errors raised at the Rails console prompt are not captured. To report one from the console, use:
|
|
659
|
+
|
|
644
660
|
```ruby
|
|
645
|
-
|
|
646
|
-
raise "Test error from Rails Error Dashboard!"
|
|
661
|
+
Rails.error.report(RuntimeError.new("Test error from Rails Error Dashboard"), handled: false)
|
|
647
662
|
```
|
|
648
663
|
|
|
664
|
+
With async logging on (the default), leave the console open for a second or two so the background job can write it.
|
|
665
|
+
|
|
649
666
|
[Full installation guide →](docs/QUICKSTART.md)
|
|
650
667
|
|
|
668
|
+
### Upgrading
|
|
669
|
+
|
|
670
|
+
```bash
|
|
671
|
+
bundle update rails_error_dashboard
|
|
672
|
+
bin/rails generate rails_error_dashboard:install --no-interactive
|
|
673
|
+
bin/rails db:migrate
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
The installer keeps your initializer and copies only the new migrations. Some releases need a step of their own, and two of them can stop an app booting: [read the upgrade guide](docs/UPGRADING.md) before you deploy.
|
|
677
|
+
|
|
651
678
|
---
|
|
652
679
|
|
|
653
680
|
## Configuration
|
|
@@ -661,8 +688,7 @@ RailsErrorDashboard.configure do |config|
|
|
|
661
688
|
# Optional features — enable as needed
|
|
662
689
|
config.enable_slack_notifications = true
|
|
663
690
|
config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL']
|
|
664
|
-
config.async_logging = true
|
|
665
|
-
config.async_adapter = :sidekiq # or :solid_queue, :async
|
|
691
|
+
config.async_logging = true # jobs run on your app's Active Job adapter; production needs a worker
|
|
666
692
|
end
|
|
667
693
|
```
|
|
668
694
|
|
|
@@ -670,7 +696,7 @@ end
|
|
|
670
696
|
|
|
671
697
|
**Multi-App Support** — Track errors from multiple Rails apps in a single shared database. Auto-detects app name, supports per-app filtering. [Multi-App guide →](docs/MULTI_APP_PERFORMANCE.md)
|
|
672
698
|
|
|
673
|
-
**OpenTelemetry Export** — Emit error-capture operations as OTel spans to Datadog, Honeycomb, or Jaeger. Add `gem "opentelemetry-api"` and set `config.enable_otel_export = true`. See
|
|
699
|
+
**OpenTelemetry Export** — Emit error-capture operations as OTel spans to Datadog, Honeycomb, or Jaeger. Add `gem "opentelemetry-api"` and set `config.enable_otel_export = true`. See **OpenTelemetry Export** under [Features](#features) above for the full options.
|
|
674
700
|
|
|
675
701
|
---
|
|
676
702
|
|
|
@@ -702,7 +728,7 @@ Users can also switch language per-session from the picker in the dashboard navb
|
|
|
702
728
|
|
|
703
729
|
**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.
|
|
704
730
|
|
|
705
|
-
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.
|
|
731
|
+
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.
|
|
706
732
|
|
|
707
733
|
[Translations guide →](docs/guides/TRANSLATIONS.md) — how the system works, how to correct a string, and how to add a locale.
|
|
708
734
|
|
|
@@ -711,7 +737,7 @@ RED translates through its own private I18n backend, so it never reads, writes o
|
|
|
711
737
|
## FAQ
|
|
712
738
|
|
|
713
739
|
**Does Rails Error Dashboard support a separate database for errors?**
|
|
714
|
-
Yes. You can store errors in your app's existing database
|
|
740
|
+
Yes. You can store errors in your app's existing database **or** in a dedicated one. Choose it at the installer's database prompt (or pass `--separate-database`), then add the `error_dashboard` entry it prints to `config/database.yml` for every environment. The engine routes all of its tables through `connects_to`, keeping error data isolated from your app data. Both modes are first-class and covered by the [Database Options guide](docs/guides/DATABASE_OPTIONS.md).
|
|
715
741
|
|
|
716
742
|
**Which databases does it work with?**
|
|
717
743
|
SQLite, PostgreSQL, and MySQL/Trilogy — in either shared or separate-database mode.
|
|
@@ -726,13 +752,13 @@ Yes — local **and** instance variables at the moment the exception is raised,
|
|
|
726
752
|
No. Storm protection (a circuit breaker with adaptive sampling, **ON by default**) makes the gem degrade itself first during error floods — occurrence counts stay exact while it sheds the expensive work, and a Storm History page shows exactly what was shed. There is no I/O on the hot path — the check is a digest and an atomic increment.
|
|
727
753
|
|
|
728
754
|
**Does it work with my background jobs?**
|
|
729
|
-
Yes — errors raised in jobs are captured, and it can log errors asynchronously through Sidekiq
|
|
755
|
+
Yes — errors raised in jobs are captured, and it can log errors asynchronously through your app's Active Job adapter (Sidekiq, Solid Queue, GoodJob or the in-process `:async`). In production, run a worker for the `default` and `error_notifications` queues. Sidekiq, Solid Queue and GoodJob are all auto-detected for the job-queue stats stored on each error.
|
|
730
756
|
|
|
731
757
|
**Does it work with my authentication?**
|
|
732
758
|
Yes — HTTP Basic Auth out of the box, or a custom `authenticate_with` lambda that integrates with Devise, Warden, or session-based auth.
|
|
733
759
|
|
|
734
760
|
**Can it track more than one app?**
|
|
735
|
-
Yes —
|
|
761
|
+
Yes — apps that point at the same error database share one dashboard, with per-app filtering.
|
|
736
762
|
|
|
737
763
|
**What Rails and Ruby versions are supported?**
|
|
738
764
|
Rails 7.0–8.1 and Ruby 3.2–4.0.
|
|
@@ -744,6 +770,8 @@ Rails 7.0–8.1 and Ruby 3.2–4.0.
|
|
|
744
770
|
### Getting Started
|
|
745
771
|
- **[Quickstart Guide](docs/QUICKSTART.md)** — 5-minute setup
|
|
746
772
|
- **[Configuration](docs/guides/CONFIGURATION.md)** — All configuration options
|
|
773
|
+
- **[Upgrading](docs/UPGRADING.md)** — The upgrade, and the releases that need a step
|
|
774
|
+
- **[Running in Production](docs/PRODUCTION.md)** — A worker, scheduled jobs and the other things RED needs before the first deploy
|
|
747
775
|
- **[Uninstalling](docs/UNINSTALL.md)** — Clean removal
|
|
748
776
|
|
|
749
777
|
### Features
|
|
@@ -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
|
|
@@ -583,6 +583,52 @@ module RailsErrorDashboard
|
|
|
583
583
|
@pagy, @events = pagy(:offset, all_events, limit: per_page_param)
|
|
584
584
|
end
|
|
585
585
|
|
|
586
|
+
def missing_translations
|
|
587
|
+
unless RailsErrorDashboard.configuration.enable_missing_translation_tracking
|
|
588
|
+
flash[:alert] = feature_disabled_message("missing_translations", options: "enable_missing_translation_tracking = true", set: true)
|
|
589
|
+
redirect_to errors_path(**app_context_params)
|
|
590
|
+
return
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
days = days_param(default: 30)
|
|
594
|
+
@days = days
|
|
595
|
+
result = Queries::MissingTranslationSummary.call(days, application_id: @current_application_id)
|
|
596
|
+
all_entries = result[:entries]
|
|
597
|
+
|
|
598
|
+
# Summary stats (computed before pagination). The list is one row per
|
|
599
|
+
# (locale, key), so the badge and pagination count rows; the tile
|
|
600
|
+
# counts distinct keys, which is what "how many keys are missing" means.
|
|
601
|
+
@entry_count = all_entries.size
|
|
602
|
+
@unique_keys = all_entries.map { |e| e[:key] }.uniq.size
|
|
603
|
+
@total_misses = all_entries.sum { |e| e[:count] }
|
|
604
|
+
@locale_count = result[:locales].size
|
|
605
|
+
# Misses the tracker's bounded buffer dropped; shown so the total above
|
|
606
|
+
# is never silently understated.
|
|
607
|
+
@overflow_count = result[:overflow_count].to_i
|
|
608
|
+
|
|
609
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: per_page_param)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def boot_errors
|
|
613
|
+
unless RailsErrorDashboard.configuration.enable_crash_capture
|
|
614
|
+
flash[:alert] = feature_disabled_message("boot_errors", options: "enable_crash_capture = true", set: true)
|
|
615
|
+
redirect_to errors_path(**app_context_params)
|
|
616
|
+
return
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
days = days_param(default: 30)
|
|
620
|
+
@days = days
|
|
621
|
+
result = Queries::BootErrors.call(days, application_id: @current_application_id)
|
|
622
|
+
all_entries = result[:entries]
|
|
623
|
+
|
|
624
|
+
# Summary stats (computed before pagination)
|
|
625
|
+
@entry_count = all_entries.size
|
|
626
|
+
@unresolved_count = result[:unresolved_count]
|
|
627
|
+
@total_occurrences = all_entries.sum { |e| e[:count] }
|
|
628
|
+
|
|
629
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: per_page_param)
|
|
630
|
+
end
|
|
631
|
+
|
|
586
632
|
def actioncable_health_summary
|
|
587
633
|
unless RailsErrorDashboard.configuration.enable_actioncable_tracking &&
|
|
588
634
|
RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
@@ -634,6 +680,9 @@ module RailsErrorDashboard
|
|
|
634
680
|
|
|
635
681
|
days = days_param(default: 30)
|
|
636
682
|
@days = days
|
|
683
|
+
# Live reachability of the configured service, shown above the
|
|
684
|
+
# historical breadcrumb stats. One existence check per page load.
|
|
685
|
+
@storage_probe = Services::ActiveStorageProbe.call
|
|
637
686
|
result = Queries::ActiveStorageSummary.call(days, application_id: @current_application_id)
|
|
638
687
|
all_services = result[:services]
|
|
639
688
|
|
|
@@ -719,7 +768,17 @@ module RailsErrorDashboard
|
|
|
719
768
|
)
|
|
720
769
|
exception.set_backtrace(caller)
|
|
721
770
|
|
|
722
|
-
|
|
771
|
+
started_at = Time.current
|
|
772
|
+
error_log = Commands::LogError.call(exception, { request: request, source: "dashboard.test_error" })
|
|
773
|
+
|
|
774
|
+
# The capture path notifies on a first occurrence and then on the
|
|
775
|
+
# 10th, 50th... A second click within 24 hours is occurrence two of the
|
|
776
|
+
# same row, so it used to notify nobody, which reads as "notifications
|
|
777
|
+
# are broken". This button exists to prove delivery: if the capture
|
|
778
|
+
# path did not claim a notification in this request, send one now.
|
|
779
|
+
if error_log && !notified_since?(error_log, started_at)
|
|
780
|
+
Services::ErrorNotificationDispatcher.call(error_log)
|
|
781
|
+
end
|
|
723
782
|
|
|
724
783
|
flash[:notice] = red_t("red.flash.test_error.logged")
|
|
725
784
|
redirect_to errors_path(**app_context_params)
|
|
@@ -785,7 +844,14 @@ module RailsErrorDashboard
|
|
|
785
844
|
end
|
|
786
845
|
|
|
787
846
|
def filter_params
|
|
788
|
-
|
|
847
|
+
scalar_params(FILTERABLE_PARAMS)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# True when NotificationThrottler claimed a notification for this error
|
|
851
|
+
# at or after +since+. The claim is an update_all, so the row is re-read.
|
|
852
|
+
def notified_since?(error_log, since)
|
|
853
|
+
stamp = error_log.class.where(id: error_log.id).pick(:last_notified_at)
|
|
854
|
+
stamp.present? && stamp >= since
|
|
789
855
|
end
|
|
790
856
|
|
|
791
857
|
# Coerce params[:days] into a sane integer in [1, 365]. Without clamping,
|
|
@@ -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
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
module RailsErrorDashboard
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
# Problems in config/queue.yml that stop RED's jobs in this environment:
|
|
4
|
+
# empty when the config is fine, or when Solid Queue doesn't run RED's jobs.
|
|
5
|
+
def red_solid_queue_problems
|
|
6
|
+
Services::SolidQueueConfigCheck.current_problems
|
|
7
|
+
rescue StandardError
|
|
8
|
+
[]
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
# Returns the host app's CSP nonce (if any) so inline <script> tags pass strict CSP.
|
|
4
12
|
# Falls back to nil when the host has no CSP configured — in that case the script tag
|
|
5
13
|
# works without a nonce attribute. Strict CSPs (script-src 'self' 'nonce-...') require
|
|
@@ -155,8 +163,8 @@ module RailsErrorDashboard
|
|
|
155
163
|
# @return [Hash] Whitelisted params for building URLs
|
|
156
164
|
def permitted_filter_params(extra_keys: [])
|
|
157
165
|
base_keys = RailsErrorDashboard::ErrorsController::FILTERABLE_PARAMS + %i[page per_page days]
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
# scalar_params, not params.permit: see ApplicationController#scalar_params.
|
|
167
|
+
scalar_params(base_keys + Array(extra_keys))
|
|
160
168
|
end
|
|
161
169
|
|
|
162
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
|
|
@@ -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
|
|
@@ -11,9 +11,10 @@ module RailsErrorDashboard
|
|
|
11
11
|
module PlainChannelMessage
|
|
12
12
|
private
|
|
13
13
|
|
|
14
|
-
# Slack takes {text:}, Discord {content:},
|
|
15
|
-
# machine-readable event. Email and PagerDuty are not
|
|
16
|
-
# a sensible shape for a message that is not an
|
|
14
|
+
# Slack takes {text:}, Discord {content:}, Telegram a plain sendMessage,
|
|
15
|
+
# custom webhooks a machine-readable event. Email and PagerDuty are not
|
|
16
|
+
# used: neither has a sensible shape for a message that is not an
|
|
17
|
+
# incident.
|
|
17
18
|
def deliver_plain_message(message, webhook_payload)
|
|
18
19
|
config = RailsErrorDashboard.configuration
|
|
19
20
|
delivered = false
|
|
@@ -28,15 +29,20 @@ module RailsErrorDashboard
|
|
|
28
29
|
delivered = true
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
if Services::TelegramDelivery.configured?(config)
|
|
33
|
+
post_telegram(message)
|
|
34
|
+
delivered = true
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
if config.enable_webhook_notifications && config.webhook_urls.present?
|
|
32
|
-
config.webhook_urls.each { |url|
|
|
38
|
+
config.webhook_urls.each { |url| post_webhook(url, webhook_payload) }
|
|
33
39
|
delivered = true
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
# An email-only or PagerDuty-only deployment has no channel for this.
|
|
37
43
|
# Say so where an operator can find it rather than dropping it silently.
|
|
38
44
|
unless delivered
|
|
39
|
-
Rails.logger.warn("[RailsErrorDashboard] #{self.class.name.demodulize}: no Slack, Discord or webhook channel is enabled; message not sent: #{message}")
|
|
45
|
+
Rails.logger.warn("[RailsErrorDashboard] #{self.class.name.demodulize}: no Slack, Discord, Telegram or webhook channel is enabled; message not sent: #{message}")
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
|
|
@@ -49,6 +55,27 @@ module RailsErrorDashboard
|
|
|
49
55
|
(defined?(Rails) && Rails.application.class.module_parent_name) || "Rails Application"
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
# Custom webhooks go through WebhookDelivery so they are signed when
|
|
59
|
+
# webhook_signing_secret is set. Slack and Discord stay on post_json:
|
|
60
|
+
# their payloads are not ours to sign. Same per-URL rescue as post_json,
|
|
61
|
+
# so one unreachable receiver does not stop the next.
|
|
62
|
+
def post_webhook(url, payload)
|
|
63
|
+
Services::WebhookDelivery.post(url, payload)
|
|
64
|
+
rescue => e
|
|
65
|
+
Rails.logger.error("[RailsErrorDashboard] #{self.class.name.demodulize} post failed: #{e.message}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# No parse_mode: the sentence is sent verbatim, so an app name with a
|
|
69
|
+
# "<" in it cannot break Telegram's HTML parser. The token sits in the
|
|
70
|
+
# request URL, hence the redaction before logging.
|
|
71
|
+
def post_telegram(message)
|
|
72
|
+
Services::TelegramDelivery.send_message(message)
|
|
73
|
+
rescue => e
|
|
74
|
+
Rails.logger.error(
|
|
75
|
+
"[RailsErrorDashboard] #{self.class.name.demodulize} Telegram post failed: #{Services::TelegramDelivery.redact(e.message)}"
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
52
79
|
def post_json(url, payload)
|
|
53
80
|
if defined?(HTTParty)
|
|
54
81
|
HTTParty.post(url, body: payload.to_json,
|
|
@@ -59,7 +86,7 @@ module RailsErrorDashboard
|
|
|
59
86
|
http.use_ssl = uri.scheme == "https"
|
|
60
87
|
http.open_timeout = 5
|
|
61
88
|
http.read_timeout = 10
|
|
62
|
-
request = Net::HTTP::Post.new(uri.
|
|
89
|
+
request = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" })
|
|
63
90
|
request.body = payload.to_json
|
|
64
91
|
http.request(request)
|
|
65
92
|
end
|
|
@@ -32,7 +32,7 @@ module RailsErrorDashboard
|
|
|
32
32
|
http.use_ssl = uri.scheme == "https"
|
|
33
33
|
http.open_timeout = 5
|
|
34
34
|
http.read_timeout = 10
|
|
35
|
-
request = Net::HTTP::Post.new(uri.
|
|
35
|
+
request = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" })
|
|
36
36
|
request.body = payload.to_json
|
|
37
37
|
http.request(request)
|
|
38
38
|
end
|