From 2808dd6595e68bcc529810a2b6087ef7b3f08b71 Mon Sep 17 00:00:00 2001 From: Ashen <310210685+ashen0x@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:00:16 +0530 Subject: [PATCH] fix: skip non-public analytics relays instead of disabling delivery --- routstr/nostr/analytics_v2_delivery.py | 10 ++++++++-- tests/unit/test_analytics_v2_delivery.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/routstr/nostr/analytics_v2_delivery.py b/routstr/nostr/analytics_v2_delivery.py index 6528c1ac..4c1d46a0 100644 --- a/routstr/nostr/analytics_v2_delivery.py +++ b/routstr/nostr/analytics_v2_delivery.py @@ -1015,10 +1015,16 @@ class AnalyticsV2Delivery: if retry_seconds < 0 or timeout_seconds <= 0: raise AnalyticsV2DeliveryError("Delivery timing is invalid") self._session_factory = session_factory - relays = sorted({_normalize_public_wss_url(url) for url in operator_relays}) + relays: set[str] = set() + for url in operator_relays: + try: + relays.add(_normalize_public_wss_url(url)) + except AnalyticsV2DeliveryError as error: + # The URL itself may carry credentials, so only the reason is logged. + logger.warning("Skipping analytics relay", extra={"reason": str(error)}) if not relays: raise AnalyticsV2DeliveryError("At least one analytics relay is required") - self._targets = tuple(RelayTarget(url) for url in relays) + self._targets = tuple(RelayTarget(url) for url in sorted(relays)) self._quorum = min(ANALYTICS_RELAY_QUORUM, len(self._targets)) self._retry_ms = int(retry_seconds * 1000) self._timeout_seconds = timeout_seconds diff --git a/tests/unit/test_analytics_v2_delivery.py b/tests/unit/test_analytics_v2_delivery.py index 27ebfecb..9cc378c5 100644 --- a/tests/unit/test_analytics_v2_delivery.py +++ b/tests/unit/test_analytics_v2_delivery.py @@ -1079,6 +1079,22 @@ async def test_single_configured_relay_can_deliver_without_a_manifest( ) == delivery_module.DeliveryPassResult(1, 1) +@pytest.mark.asyncio +async def test_non_public_relays_are_skipped_instead_of_disabling_delivery( + session_factory: async_sessionmaker[AsyncSession], +) -> None: + delivery = AnalyticsV2Delivery( + session_factory, + operator_relays=(RELAYS[0], "ws://umbrel.local:4848", "wss://relay.lan"), + ) + assert delivery._targets == (RelayTarget(RELAYS[0]),) + + with pytest.raises(AnalyticsV2DeliveryError, match="At least one"): + AnalyticsV2Delivery( + session_factory, operator_relays=("ws://umbrel.local:4848",) + ) + + @pytest.mark.asyncio async def test_frame_limit_uses_the_required_relay_quorum_and_caches_each_pass( session_factory: async_sessionmaker[AsyncSession],