fix(integrations): skip unconfigured + dedupe health-check alerts#207
Conversation
There was a problem hiding this comment.
Pull request overview
Hotfix to reduce #newspack-errors noise from hourly integration health checks by (1) skipping checks for enabled-but-not-configured integrations and (2) deduplicating repeated health-check failure alerts over a fixed interval.
Changes:
- Skip
Integrations::run_health_checks()for integrations whereis_set_up()is false. - Add transient-based deduplication in
Alert_Manager::handle_health_check_failed()keyed byintegration_id + sorted error codes. - Add/extend PHPUnit coverage for the skip + dedupe behaviors (including a more configurable
Sample_Integrationtest double).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/newspack-plugin/tests/unit-tests/integrations/class-test-integrations.php | Adds tests asserting health checks are skipped when is_set_up() is false and still fire when set up + failing. |
| plugins/newspack-plugin/tests/unit-tests/integrations/class-sample-integration.php | Extends the test integration double to simulate setup state and configurable can_sync() error codes. |
| plugins/newspack-plugin/tests/unit-tests/alert-manager.php | Adds tests validating deduplication behavior across repeats, new error signatures, and per-integration isolation. |
| plugins/newspack-plugin/includes/reader-activation/class-integrations.php | Implements the is_set_up() gate in run_health_checks() to avoid treating setup-incomplete integrations as incidents. |
| plugins/newspack-plugin/includes/class-alert-manager.php | Implements deduplication for health-check-failure alerts via a transient key derived from integration + error signature. |
There was a problem hiding this comment.
Thanks for the hotfix @miguelpeixe! I ran this through review and Claude came up with some good points which I've added inline. I should also note that while testing, the ESP integration seems to still think I'm setup even after I removed the master list via settings 😦
I think this might require some more thought. Wdyt about submitting a simpler hotfix to just remove the hook temporarily:
| } | ||
|
|
||
| foreach ( $integrations as $integration_id => $integration ) { | ||
| if ( ! $integration->is_set_up() ) { |
There was a problem hiding this comment.
🔴 Silent data loss when the ESP readiness probe transiently fails
is_set_up() for the ESP resolves to Reader_Activation::is_esp_configured() → get_enabled_lists() → Newspack_Newsletters_Subscription::get_lists(), which always fetches lists live from the ESP (subscription class L165). On a timeout / rate-limit / auth blip it returns a WP_Error, which is_esp_configured() treats as "not an array" and returns false.
So for a fully-configured integration during a momentary provider hiccup, this continue skips the push entirely: push_contact_data is never called, newspack_sync_contact_failed never fires, and no retry is scheduled — the contact update is permanently lost, silently. This inverts the purpose of the retry system, which exists to survive exactly these transient failures.
It also narrows the true|WP_Error contract: push_to_integrations can now return true even though an integration was skipped, so callers like the ras-contact-sync WP-CLI report success for a contact that wasn't pushed.
Suggest gating on a stored readiness signal (provider selected + stored list/master-list config) instead of a live get_lists() call, or failing open when the probe returns WP_Error (treat "unknown" as "don't skip").
There was a problem hiding this comment.
Fixed in dae5bae — root cause was ESP::is_set_up() calling the live get_lists() via Reader_Activation::is_esp_configured(). It now reads stored state only (provider option + master list ID option), so a transient provider failure can't make is_set_up() return false. The push gate (and the rest of the sites that consult is_set_up()) is now safe to skip on. 8b0fc58 also centralizes the skip into Integrations::get_active_configured_integrations() so a 6th walk site can't drift.
| return; | ||
| } | ||
|
|
||
| if ( ! $integration->is_set_up() ) { |
There was a problem hiding this comment.
🔴 Aborts an in-flight retry chain on a transient probe failure
Same root cause as the push-path comment above. A retry that was queued because of a real prior failure hits this guard at execution time; if the live is_set_up() ESP call momentarily errors, the chain aborts with no reschedule and no exhaustion record — the queued contact update is dropped. A configured-but-temporarily-unreachable ESP is exactly the case the backoff/retry was built to handle.
There was a problem hiding this comment.
Same root cause as the push-path comment. Fixed in dae5bae — is_set_up() is now a stored-state check, so this retry guard can't abort a chain because of a transient probe failure.
| $errors = []; | ||
|
|
||
| foreach ( $active_integrations as $integration ) { | ||
| if ( ! $integration->is_set_up() ) { |
There was a problem hiding this comment.
🔴 Same transient-failure data loss on the pull side
See the push-path comment in class-contact-sync.php. is_set_up() here also performs a live ESP list fetch, so a transient WP_Error makes it return false and this continue silently drops the pull — incoming fields fail to populate for the user with no retry.
| return; | ||
| } | ||
|
|
||
| if ( ! $integration->is_set_up() ) { |
There was a problem hiding this comment.
🔴 Same retry-chain abort on the pull side
Mirrors the sync retry guard: a transient is_set_up() probe failure permanently aborts the queued pull retry with no reschedule.
There was a problem hiding this comment.
Same root cause. Fixed in dae5bae — the stored-state is_set_up() means a transient probe failure can't abort an in-flight pull retry chain.
| public static function run_health_checks() { | ||
| $active = self::get_active_integrations(); | ||
| foreach ( $active as $integration ) { | ||
| if ( ! $integration->is_set_up() ) { |
There was a problem hiding this comment.
🟠 Live ESP I/O on a hot path + a monitoring blind spot + altitude
Three things converge on this is_set_up() skip:
-
Efficiency —
is_set_up()is now a blocking remote ESP call, run on every active integration every hour, immediately beforehealth_check()(which makes more live calls). Adds latency and provider rate-limit pressure to the cron. -
Monitoring blind spot — when a previously-working ESP loses its last active list (or the probe itself errors),
is_set_up()returnsfalse, sohealth_check()→test_connection()never runs. A genuinely broken connection (revoked key, etc.) is silently reclassified as "setup incomplete" and stops alerting. -
Altitude — this
is_set_up()skip is now copy-pasted into 5 walk sites (health-check, sync push + retry, pull + retry) with no central "configured integrations" filter, so a 6th walk path added later silently re-introduces the flood. Consider aget_configured_integrations()helper (orget_active_integrations( $configured = true )) plus a shared retry-abort helper for the two identicalexecute_integration_retryguards.
There was a problem hiding this comment.
All three points addressed:
- Efficiency — dae5bae:
is_set_up()no longer hits the provider. The check now consults two stored options (newspack_newsletters_service_provider+ the provider's master-list option). - Monitoring blind spot — also dae5bae: a previously-working ESP that loses its last list (or whose probe errors) keeps
is_set_up()=true(master_list_id option still stored).health_check()→test_connection()continues to run, and revoked credentials still alert. - Altitude / DRY — 8b0fc58: added
Integrations::get_active_configured_integrations(); the three foreach walks (health check, sync push, pull) route through it now. The twoexecute_integration_retryguards stayed inline because they differ in logger context — happy to extract a tiny shared helper if you'd prefer.
| ] | ||
| ); | ||
|
|
||
| set_transient( $dedup_key, time(), self::HEALTH_CHECK_DEDUP_INTERVAL ); |
There was a problem hiding this comment.
🟡 Set the dedup transient before dispatch, not after
If any newspack_alert handler throws on a transient error (e.g. the Slack POST connector), execution never reaches this set_transient, so the next hourly cron re-alerts — dedup is defeated exactly when delivery is flaky. Since the dedup's intent is to cap alert volume rather than guarantee delivery, set the transient before do_action( 'newspack_alert', ... ).
There was a problem hiding this comment.
Fixed in 0654b80 — set_transient is now called before do_action( 'newspack_alert' ). Added test_health_check_failed_sets_dedup_before_dispatch which registers a throwing listener and asserts the transient is still set after the throw.
| $error = $payload['error'] ?? null; | ||
| $error = $payload['error'] ?? null; | ||
| $integration_id = $payload['integration_id'] ?? 'unknown'; | ||
| $error_codes = is_wp_error( $error ) ? $error->get_error_codes() : []; |
There was a problem hiding this comment.
🟡 Dedup keys on error codes only, not messages
An escalating failure that keeps the same code set but carries a worse message (e.g. "list missing" → "auth fully revoked") is suppressed for the full 24h window, so the richer signal never reaches Slack. Worth confirming that's acceptable, or fold a short message hash into the key.
There was a problem hiding this comment.
Fixed in 0654b80 — the dedup key now folds WP_Error::get_error_messages() into the md5 alongside the codes. An escalating same-code failure with a worse message now bypasses the bucket and re-alerts. Added test_health_check_failed_alerts_on_new_error_messages. Trade-off noted in the docblock for any future caller passing dynamic content (timestamps/IDs) in the message — for the current Newspack ESP errors, messages are static per code so dedup remains stable.
chickenn00dle
left a comment
There was a problem hiding this comment.
Approving assuming feedback is addressed!
| public static function pull_all( $user_id ) { | ||
| $active_integrations = Integrations::get_active_integrations(); | ||
| $active_integrations = Integrations::get_active_configured_integrations(); | ||
| $errors = []; |
There was a problem hiding this comment.
Good catch — Fixed in a8cea54. Contact_Pull::pull_sync() now defaults to Integrations::get_active_configured_integrations(), and handle_ajax_pull() has a defense-in-depth check that silently succeeds for an unconfigured integration (so a direct AJAX call can't bypass the gate). Added test_pull_sync_skips_unconfigured_integrations which registers a configured and an unconfigured mock and asserts only the configured one receives a loopback. The other remaining get_active_integrations() callers (register_group_labels, register_my_account_endpoints, promoted-fields, Sync::has_one_syncable_integration) are intentionally kept — they're registration/UI/has-any checks, not integration-walking I/O, so the configured filter doesn't apply.
| * Deduplicates by integration + error-code + error-message signature | ||
| * for HEALTH_CHECK_DEDUP_INTERVAL so an hourly cron does not repeat | ||
| * the same Slack alert all day. A new error code OR a changed message | ||
| * on the same integration (e.g. "list missing" escalating to "auth | ||
| * fully revoked") falls outside the key and alerts immediately. | ||
| * | ||
| * @param array $payload Health check failure data. | ||
| */ | ||
| public static function handle_health_check_failed( $payload ) { | ||
| $error = $payload['error'] ?? null; | ||
| $error = $payload['error'] ?? null; | ||
| $integration_id = $payload['integration_id'] ?? 'unknown'; | ||
| $error_codes = is_wp_error( $error ) ? $error->get_error_codes() : []; | ||
| if ( empty( $error_codes ) ) { | ||
| $error_codes = [ 'unknown' ]; | ||
| } | ||
| $error_messages = is_wp_error( $error ) ? $error->get_error_messages() : []; | ||
|
|
||
| $dedup_key = self::get_health_check_dedup_key( $integration_id, $error_codes, $error_messages ); |
There was a problem hiding this comment.
Right — PR description updated. The 'Changes proposed' section now states the dedup key is integration_id + sorted error codes + sorted error messages and explicitly calls out the trade-off (escalating same-code failures re-alert; dynamic content in messages would inflate alert volume — current Newspack ESP messages are static per code so it lands on the right side). The change came out of @chickenn00dle's feedback on the prior codes-only signature; test_health_check_failed_alerts_on_new_error_messages pins the new behavior.
|
Hey @miguelpeixe, good job getting this PR merged! 🎉 Now, the Please check if this PR needs to be included in the "Upcoming Changes" and "Release Notes" doc. If it doesn't, simply remove the label. If it does, please add an entry to our shared document, with screenshots and testing instructions if applicable, then remove the label. Thank you! ❤️ |
## newspack [6.42.4](https://github.com/Automattic/newspack-workspace/compare/newspack@6.42.3...newspack@6.42.4) (2026-06-04) ### Bug Fixes * **integrations:** skip unconfigured + dedupe health-check alerts ([#207](#207)) ([63eebb9](63eebb9))
## [4.26.5-alpha.1](https://github.com/Automattic/newspack-workspace/compare/@automattic/newspack-blocks@4.26.4...@automattic/newspack-blocks@4.26.5-alpha.1) (2026-06-05) ### Bug Fixes * add contrasting colour to the my account mobile menu button ([#164](#164)) ([c06219c](c06219c)) * add contrasting colour to the my account mobile menu button ([#164](#164)) ([94b765c](94b765c)) * **audience:** decode HTML entities in campaign prompt titles ([#4711](https://github.com/Automattic/newspack-workspace/issues/4711)) ([f1d189b](f1d189b)) * **blocks:** restore allow-duplicate toggle for static blocks ([#180](#180)) ([8aaa75e](8aaa75e)) * **ci:** restore workspace:* deps in post-release branch maintenance ([#139](#139)) ([821630b](821630b)) * correct submit button text on change-payment-method page ([#4654](https://github.com/Automattic/newspack-workspace/issues/4654)) ([7aba1ff](7aba1ff)) * **editor:** keep legacy CSS vars in sync with newsletters-prefixed ones ([#2140](https://github.com/Automattic/newspack-workspace/issues/2140)) ([4f3c33a](4f3c33a)) * **group-subscription:** update params; avoid secondary My Account redirect ([#151](#151)) ([c5f6249](c5f6249)) * **integrations:** skip unconfigured + dedupe health-check alerts ([#207](#207)) ([63eebb9](63eebb9)) * **jetpack:** disable Newsletter (subscriptions) module ([#208](#208)) ([7d56e9d](7d56e9d)) * **n:** correct worktree-to-env routing ([#152](#152)) ([f8bf0c2](f8bf0c2)) * **newsletters:** clear false "unsaved changes" prompt after save ([#190](#190)) ([265f0dc](265f0dc)) * **newspack-blocks:** update swiper to patched release ([a33812a](a33812a)) * **newspack-network:** patch critical basic-ftp alert ([a2c8802](a2c8802)) * **newspack-theme:** apply ad background color to Broadstreet ads ([#167](#167)) ([fbf469f](fbf469f)) * **popups:** portal overlays to wp_footer to escape stacking contexts ([797c42c](797c42c)) * **posts-inserter:** show placeholder when block has zero posts ([#166](#166)) ([c269ff2](c269ff2)) * prevent content-gate editor.scss styles from getting chunked into the common.css file ([#4716](https://github.com/Automattic/newspack-workspace/issues/4716)) ([f6e5a56](f6e5a56)) * quote additional site names and derivatives ([#66](#66)) ([9a377db](9a377db)) * **reader-activation:** clear localStorage namespace on logout ([#145](#145)) ([c201ad5](c201ad5)) * **reader-activation:** exclude peeking newsletter from a11y tree ([#4744](https://github.com/Automattic/newspack-workspace/issues/4744)) ([b726bbf](b726bbf)) * render synced patterns inside Group blocks in newsletters ([#2069](https://github.com/Automattic/newspack-workspace/issues/2069)) ([b09da75](b09da75)) * **theme:** square icon-only buttons via theme.json variations ([#452](https://github.com/Automattic/newspack-workspace/issues/452)) ([fb1493d](fb1493d)) * update paragraph markup to fix failing tests ([2fbadaa](2fbadaa)) ### Features * **block-theme:** add search overlay block ([#4729](https://github.com/Automattic/newspack-workspace/issues/4729)) ([0ebac0d](0ebac0d)) * **integrations:** add inactive plugin state ([#4721](https://github.com/Automattic/newspack-workspace/issues/4721)) ([d67ffe0](d67ffe0)) * **integrations:** add oauth and hidden field types ([#4639](https://github.com/Automattic/newspack-workspace/issues/4639)) ([8bd0e7c](8bd0e7c)) * **integrations:** allow filtering integration settings list ([#224](#224)) ([af0a884](af0a884)) * **n:** auto-discover repos/{plugins,themes} checkouts, no registration ([#178](#178)) ([961fe1c](961fe1c)) * **n:** mount standalone plugins from repos/ for local development ([#177](#177)) ([dafcf70](dafcf70)) * **reader-auth:** unify auth + post-reg verification flows ([#135](#135)) ([f67bb65](f67bb65)), closes [#signin_modal](https://github.com/Automattic/newspack-workspace/issues/signin_modal) [#register_modal](https://github.com/Automattic/newspack-workspace/issues/register_modal) * **wc-subscriptions:** recover switch proration when no amount paid ([#4745](https://github.com/Automattic/newspack-workspace/issues/4745)) ([f9db7a7](f9db7a7))
# [3.34.0-alpha.1](https://github.com/Automattic/newspack-workspace/compare/newspack-newsletters@3.33.6...newspack-newsletters@3.34.0-alpha.1) (2026-06-05) ### Bug Fixes * add contrasting colour to the my account mobile menu button ([#164](#164)) ([c06219c](c06219c)) * **audience:** decode HTML entities in campaign prompt titles ([#4711](https://github.com/Automattic/newspack-workspace/issues/4711)) ([f1d189b](f1d189b)) * **blocks:** restore allow-duplicate toggle for static blocks ([#180](#180)) ([8aaa75e](8aaa75e)) * **ci:** restore workspace:* deps in post-release branch maintenance ([#139](#139)) ([821630b](821630b)) * correct submit button text on change-payment-method page ([#4654](https://github.com/Automattic/newspack-workspace/issues/4654)) ([7aba1ff](7aba1ff)) * **editor:** keep legacy CSS vars in sync with newsletters-prefixed ones ([#2140](https://github.com/Automattic/newspack-workspace/issues/2140)) ([4f3c33a](4f3c33a)) * **integrations:** skip unconfigured + dedupe health-check alerts ([#207](#207)) ([63eebb9](63eebb9)) * **jetpack:** disable Newsletter (subscriptions) module ([#208](#208)) ([7d56e9d](7d56e9d)) * **n:** correct worktree-to-env routing ([#152](#152)) ([f8bf0c2](f8bf0c2)) * **newsletters:** clear false "unsaved changes" prompt after save ([#190](#190)) ([265f0dc](265f0dc)) * **newspack-blocks:** update swiper to patched release ([a33812a](a33812a)) * **newspack-network:** patch critical basic-ftp alert ([a2c8802](a2c8802)) * prevent content-gate editor.scss styles from getting chunked into the common.css file ([#4716](https://github.com/Automattic/newspack-workspace/issues/4716)) ([f6e5a56](f6e5a56)) * quote additional site names and derivatives ([#66](#66)) ([9a377db](9a377db)) * **reader-activation:** clear localStorage namespace on logout ([#145](#145)) ([c201ad5](c201ad5)) * **reader-activation:** exclude peeking newsletter from a11y tree ([#4744](https://github.com/Automattic/newspack-workspace/issues/4744)) ([b726bbf](b726bbf)) * render synced patterns inside Group blocks in newsletters ([#2069](https://github.com/Automattic/newspack-workspace/issues/2069)) ([b09da75](b09da75)) * **theme:** square icon-only buttons via theme.json variations ([#452](https://github.com/Automattic/newspack-workspace/issues/452)) ([fb1493d](fb1493d)) * update paragraph markup to fix failing tests ([2fbadaa](2fbadaa)) ### Features * **block-theme:** add search overlay block ([#4729](https://github.com/Automattic/newspack-workspace/issues/4729)) ([0ebac0d](0ebac0d)) * **integrations:** add inactive plugin state ([#4721](https://github.com/Automattic/newspack-workspace/issues/4721)) ([d67ffe0](d67ffe0)) * **integrations:** add oauth and hidden field types ([#4639](https://github.com/Automattic/newspack-workspace/issues/4639)) ([8bd0e7c](8bd0e7c)) * **integrations:** allow filtering integration settings list ([#224](#224)) ([af0a884](af0a884)) * **n:** auto-discover repos/{plugins,themes} checkouts, no registration ([#178](#178)) ([961fe1c](961fe1c)) * **n:** mount standalone plugins from repos/ for local development ([#177](#177)) ([dafcf70](dafcf70)) * **reader-auth:** unify auth + post-reg verification flows ([#135](#135)) ([f67bb65](f67bb65)), closes [#signin_modal](https://github.com/Automattic/newspack-workspace/issues/signin_modal) [#register_modal](https://github.com/Automattic/newspack-workspace/issues/register_modal) * **wc-subscriptions:** recover switch proration when no amount paid ([#4745](https://github.com/Automattic/newspack-workspace/issues/4745)) ([f9db7a7](f9db7a7))
## [1.28.4-alpha.1](https://github.com/Automattic/newspack-workspace/compare/newspack-block-theme@1.28.3...newspack-block-theme@1.28.4-alpha.1) (2026-06-05) ### Bug Fixes * add contrasting colour to the my account mobile menu button ([#164](#164)) ([c06219c](c06219c)) * add contrasting colour to the my account mobile menu button ([#164](#164)) ([94b765c](94b765c)) * **audience:** decode HTML entities in campaign prompt titles ([#4711](https://github.com/Automattic/newspack-workspace/issues/4711)) ([f1d189b](f1d189b)) * **blocks:** restore allow-duplicate toggle for static blocks ([#180](#180)) ([8aaa75e](8aaa75e)) * **ci:** restore workspace:* deps in post-release branch maintenance ([#139](#139)) ([821630b](821630b)) * correct submit button text on change-payment-method page ([#4654](https://github.com/Automattic/newspack-workspace/issues/4654)) ([7aba1ff](7aba1ff)) * **editor:** keep legacy CSS vars in sync with newsletters-prefixed ones ([#2140](https://github.com/Automattic/newspack-workspace/issues/2140)) ([4f3c33a](4f3c33a)) * **group-subscription:** update params; avoid secondary My Account redirect ([#151](#151)) ([c5f6249](c5f6249)) * **integrations:** skip unconfigured + dedupe health-check alerts ([#207](#207)) ([63eebb9](63eebb9)) * **jetpack:** disable Newsletter (subscriptions) module ([#208](#208)) ([7d56e9d](7d56e9d)) * **n:** correct worktree-to-env routing ([#152](#152)) ([f8bf0c2](f8bf0c2)) * **newsletters:** clear false "unsaved changes" prompt after save ([#190](#190)) ([265f0dc](265f0dc)) * **newspack-blocks:** update swiper to patched release ([a33812a](a33812a)) * **newspack-network:** patch critical basic-ftp alert ([a2c8802](a2c8802)) * **newspack-theme:** apply ad background color to Broadstreet ads ([#167](#167)) ([fbf469f](fbf469f)) * **popups:** portal overlays to wp_footer to escape stacking contexts ([797c42c](797c42c)) * **posts-inserter:** show placeholder when block has zero posts ([#166](#166)) ([c269ff2](c269ff2)) * prevent content-gate editor.scss styles from getting chunked into the common.css file ([#4716](https://github.com/Automattic/newspack-workspace/issues/4716)) ([f6e5a56](f6e5a56)) * quote additional site names and derivatives ([#66](#66)) ([9a377db](9a377db)) * **reader-activation:** clear localStorage namespace on logout ([#145](#145)) ([c201ad5](c201ad5)) * **reader-activation:** exclude peeking newsletter from a11y tree ([#4744](https://github.com/Automattic/newspack-workspace/issues/4744)) ([b726bbf](b726bbf)) * render synced patterns inside Group blocks in newsletters ([#2069](https://github.com/Automattic/newspack-workspace/issues/2069)) ([b09da75](b09da75)) * **theme:** square icon-only buttons via theme.json variations ([#452](https://github.com/Automattic/newspack-workspace/issues/452)) ([fb1493d](fb1493d)) * update paragraph markup to fix failing tests ([2fbadaa](2fbadaa)) ### Features * **block-theme:** add search overlay block ([#4729](https://github.com/Automattic/newspack-workspace/issues/4729)) ([0ebac0d](0ebac0d)) * **integrations:** add inactive plugin state ([#4721](https://github.com/Automattic/newspack-workspace/issues/4721)) ([d67ffe0](d67ffe0)) * **integrations:** add oauth and hidden field types ([#4639](https://github.com/Automattic/newspack-workspace/issues/4639)) ([8bd0e7c](8bd0e7c)) * **integrations:** allow filtering integration settings list ([#224](#224)) ([af0a884](af0a884)) * **n:** auto-discover repos/{plugins,themes} checkouts, no registration ([#178](#178)) ([961fe1c](961fe1c)) * **n:** mount standalone plugins from repos/ for local development ([#177](#177)) ([dafcf70](dafcf70)) * **reader-auth:** unify auth + post-reg verification flows ([#135](#135)) ([f67bb65](f67bb65)), closes [#signin_modal](https://github.com/Automattic/newspack-workspace/issues/signin_modal) [#register_modal](https://github.com/Automattic/newspack-workspace/issues/register_modal) * **wc-subscriptions:** recover switch proration when no amount paid ([#4745](https://github.com/Automattic/newspack-workspace/issues/4745)) ([f9db7a7](f9db7a7))
All Submissions:
Changes proposed in this Pull Request:
Hotfix for the alert flood and ActionScheduler retry flood that started after #4723. Shared root cause: an integration that is enabled-but-unconfigured (e.g. ESP enabled by default but no provider/master list selected) is a setup-incomplete state, not a runtime incident — it should surface in the integrations UI, not in the alerts channel or the AS retry queue.
1. Skip unconfigured integrations across all integration-walking paths.
`Integration::is_set_up()` is now consulted before every cron- or loopback-driven walk, via the new `Integrations::get_active_configured_integrations()` helper:
1a. `ESP::is_set_up()` reads STORED state only.
The implementation now consults two stored options — `newspack_newsletters_service_provider` (provider selected) and the provider's master-list option — instead of calling the live `get_lists()` API. This is required because every gate above uses `is_set_up()`: if it dipped into a live provider call, any transient provider failure (timeout, rate-limit, auth blip) would make every gate skip and silently drop data — exactly the failure mode the AS retry system was built to survive. The setup question "did the admin finish configuring this?" must be answered from local state; "is the provider reachable right now?" is `health_check()`'s job.
2. Dedupe `Alert_Manager::handle_health_check_failed`.
The handler stores a transient keyed on `integration_id + sorted error codes + sorted error messages` with a `DAY_IN_SECONDS` TTL (exposed as `Alert_Manager::HEALTH_CHECK_DEDUP_INTERVAL`). Including the messages in the key (and not only the codes) is intentional: an escalating same-code failure with a worse message (e.g. "list missing" → "auth fully revoked") bypasses the bucket and re-alerts. The trade-off is that messages containing dynamic content (timestamps, IDs) would increase alert volume; current Newspack ESP error messages are static per code so the trade-off lands on the right side. Empty `WP_Error` codes collapse to `[ 'unknown' ]` so a bare `new WP_Error()` shares the bucket with the non-`WP_Error` payload path. The transient is set BEFORE `do_action( 'newspack_alert' )` so a handler that throws (e.g. transient Slack POST failure) cannot leave the key unset and defeat dedup on the next hourly cron.
Together these drop staging-clone and legacy enabled-but-unconfigured noise — both Slack alerts and AS retry rows — to zero, while preserving signal for real, configured-but-broken integrations.
How to test the changes in this Pull Request:
The initial-push gate (`Contact_Sync::push_to_integrations`) has no direct unit test — a reflection-based test would also iterate the live ESP whose `push_contact_data` hits `Newspack_Newsletters_Contacts` (not loaded in the unit-test env). The gate is structurally identical to the health-check and pull skips above and is covered by manual test step 5.
Other information:
This is a hotfix targeted at `release`. Once merged, it should be propagated forward to `alpha` and `main` via the standard branch-promotion flow.