From 60a5d914d204ba8b2cd6b89d978fa1141952acb8 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 16:30:10 -0400 Subject: [PATCH 1/2] feat: support multiple webhook targets via alert_webhook_url array alert_webhook_url now accepts a string or an array of strings. All configured URLs receive the same payload when the failure threshold is exceeded. A failure posting to one URL is logged and skipped without blocking the remaining targets. Co-Authored-By: Claude Sonnet 4.6 --- app/services/solid_queue_web/alert_webhook.rb | 9 ++++-- .../solid_queue_web/alert_webhook_spec.rb | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/services/solid_queue_web/alert_webhook.rb b/app/services/solid_queue_web/alert_webhook.rb index 5e1e759..0d4fa81 100644 --- a/app/services/solid_queue_web/alert_webhook.rb +++ b/app/services/solid_queue_web/alert_webhook.rb @@ -12,7 +12,8 @@ def call(failure_count:) return if failure_count < SolidQueueWeb.alert_failure_threshold return unless should_fire? - Thread.new { post(SolidQueueWeb.alert_webhook_url, failure_count) } + urls = webhook_urls + Thread.new { urls.each { |url| post(url, failure_count) } } end def reset! @@ -22,7 +23,11 @@ def reset! private def configured? - SolidQueueWeb.alert_webhook_url.present? && SolidQueueWeb.alert_failure_threshold.present? + webhook_urls.any? && SolidQueueWeb.alert_failure_threshold.present? + end + + def webhook_urls + Array(SolidQueueWeb.alert_webhook_url).flatten.compact.select(&:present?) end def should_fire? diff --git a/spec/services/solid_queue_web/alert_webhook_spec.rb b/spec/services/solid_queue_web/alert_webhook_spec.rb index 115448c..3f01237 100644 --- a/spec/services/solid_queue_web/alert_webhook_spec.rb +++ b/spec/services/solid_queue_web/alert_webhook_spec.rb @@ -97,5 +97,34 @@ expect(Rails.logger).to receive(:error).with(/Alert webhook failed/) expect { described_class.call(failure_count: 5) }.not_to raise_error end + + context "when alert_webhook_url is an array" do + let(:second_url) { "http://example.com/second-webhook" } + + before { SolidQueueWeb.alert_webhook_url = [webhook_url, second_url] } + + it "posts to all configured URLs" do + expect(Net::HTTP).to receive(:new).twice.and_call_original + described_class.call(failure_count: 5) + end + + it "continues posting to remaining URLs when one fails" do + call_count = 0 + allow_any_instance_of(Net::HTTP).to receive(:request) do + call_count += 1 + raise RuntimeError, "connection refused" if call_count == 1 + Net::HTTPSuccess.new("1.1", "200", "OK") + end + allow(Rails.logger).to receive(:error) + expect { described_class.call(failure_count: 5) }.not_to raise_error + expect(call_count).to eq(2) + end + + it "is not configured when the array is empty" do + SolidQueueWeb.alert_webhook_url = [] + expect_any_instance_of(Net::HTTP).not_to receive(:request) + described_class.call(failure_count: 10) + end + end end end From 74c0c5906a5e11735c7b329cca99dc0afd81252a Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Thu, 21 May 2026 16:30:17 -0400 Subject: [PATCH 2/2] docs: update README, ROADMAP, and CHANGELOG for multiple webhook targets Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + README.md | 11 +++++++++++ ROADMAP.md | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e61eafb..32e6473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Multiple webhook targets — `alert_webhook_url` now accepts an array of URL strings; all configured endpoints receive the same payload when the failure threshold is exceeded; a failure posting to one URL is logged and skipped without blocking the remaining targets - Retry failed job with modified arguments — the Arguments card on the job detail page becomes an editable textarea for failed jobs; editing the JSON and clicking "Retry with these arguments" updates the job record and retries in one step; invalid JSON redirects back with an error message without touching the failed execution ## [1.0.0] - 2026-05-21 diff --git a/README.md b/README.md index 5cf32ca..2bf94ad 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,17 @@ SolidQueueWeb.configure do |config| end ``` +To fan out to multiple endpoints (e.g. Slack and PagerDuty simultaneously), pass an array: + +```ruby +config.alert_webhook_url = [ + "https://hooks.slack.com/services/...", + "https://events.pagerduty.com/..." +] +``` + +All configured URLs receive the same payload. A failure posting to one URL is logged and skipped without blocking the remaining targets. + The request body is JSON: ```json diff --git a/ROADMAP.md b/ROADMAP.md index 54b3c6a..40c3cee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,7 +13,7 @@ Pull requests for any of these are welcome. See [Contributing](README.md#contrib | Feature | Notes | |---|---| | ~~**Retry failed job with modified arguments**~~ | ✓ Shipped — editable textarea on the job detail page; submitting updates the job record and retries in one step. | -| **Multiple webhook targets** | Change `alert_webhook_url` to accept an array. Fan out to Slack + PagerDuty simultaneously. | +| ~~**Multiple webhook targets**~~ | ✓ Shipped — `alert_webhook_url` accepts a string or an array; all URLs receive the same payload; one failure doesn't block the rest. | | **Queue depth alert** | Fire a webhook when a queue's ready count exceeds a per-queue threshold (e.g. `alert_queue_thresholds: { critical: 50 }`). | ---