From 5d5feb724b29da580e9996ca959ba265af7d1d6d Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 13 Sep 2024 14:11:32 -0400 Subject: [PATCH] feat(uptime): Add option to restrict issue creation via host id (#77435) This will allow us to disable issue creation for specific hosting providers using the `host_provider_id` field of the UptimeSubscription. Resolves https://github.com/getsentry/sentry/issues/76875 --- src/sentry/options/defaults.py | 13 +++++++ src/sentry/testutils/factories.py | 2 + src/sentry/testutils/fixtures.py | 5 +++ .../uptime/consumers/results_consumer.py | 23 ++++++++++-- .../consumers/test_results_consumers.py | 37 +++++++++++++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 30ed16de322a8..483fecac40f48 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -2713,3 +2713,16 @@ default=False, flags=FLAG_AUTOMATOR_MODIFIABLE, ) + +# Restrict uptime issue creation for specific host provider identifiers. Items +# in this list map to the `host_provider_id` column in the UptimeSubscription +# table. +# +# This may be used to stop issue creation in the event that a network / hosting +# provider blocks the uptime checker causing false positives. +register( + "uptime.restrict-issue-creation-by-hosting-provider-id", + type=Sequence, + default=[], + flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE, +) diff --git a/src/sentry/testutils/factories.py b/src/sentry/testutils/factories.py index c2466c68a58dd..c41e6c77ba8b1 100644 --- a/src/sentry/testutils/factories.py +++ b/src/sentry/testutils/factories.py @@ -1938,6 +1938,7 @@ def create_uptime_subscription( subscription_id: str | None, status: UptimeSubscription.Status, url: str, + host_provider_id: str, interval_seconds: int, timeout_ms: int, date_updated: datetime, @@ -1947,6 +1948,7 @@ def create_uptime_subscription( subscription_id=subscription_id, status=status.value, url=url, + host_provider_id=host_provider_id, interval_seconds=interval_seconds, timeout_ms=timeout_ms, date_updated=date_updated, diff --git a/src/sentry/testutils/fixtures.py b/src/sentry/testutils/fixtures.py index c9143826375ee..00eb2fc555742 100644 --- a/src/sentry/testutils/fixtures.py +++ b/src/sentry/testutils/fixtures.py @@ -651,17 +651,22 @@ def create_uptime_subscription( subscription_id: str | None = None, status: UptimeSubscription.Status = UptimeSubscription.Status.ACTIVE, url="http://sentry.io/", + host_provider_id: str | None = None, interval_seconds=60, timeout_ms=100, date_updated: None | datetime = None, ) -> UptimeSubscription: if date_updated is None: date_updated = timezone.now() + if host_provider_id is None: + host_provider_id = "TEST" + return Factories.create_uptime_subscription( type=type, subscription_id=subscription_id, status=status, url=url, + host_provider_id=host_provider_id, interval_seconds=interval_seconds, timeout_ms=timeout_ms, date_updated=date_updated, diff --git a/src/sentry/uptime/consumers/results_consumer.py b/src/sentry/uptime/consumers/results_consumer.py index 689ad5154ef2d..bcc4c6905a539 100644 --- a/src/sentry/uptime/consumers/results_consumer.py +++ b/src/sentry/uptime/consumers/results_consumer.py @@ -10,7 +10,7 @@ CheckResult, ) -from sentry import features +from sentry import features, options from sentry.conf.types.kafka_definition import Topic from sentry.remote_subscriptions.consumers.result_consumer import ( ResultProcessor, @@ -258,9 +258,24 @@ def handle_result_for_project_active_mode( if not self.has_reached_status_threshold(project_subscription, result["status"]): return - if features.has( - "organizations:uptime-create-issues", project_subscription.project.organization - ): + issue_creation_flag_enabled = features.has( + "organizations:uptime-create-issues", + project_subscription.project.organization, + ) + + # Do not create uptime issue occurences for + restricted_host_provider_ids = options.get( + "uptime.restrict-issue-creation-by-hosting-provider-id" + ) + issue_creation_restricted_by_provider = ( + project_subscription.uptime_subscription.host_provider_id + in restricted_host_provider_ids + ) + + if issue_creation_restricted_by_provider: + metrics.incr("uptime.result_processor.restricted_by_provider", sample_rate=1.0) + + if issue_creation_flag_enabled and not issue_creation_restricted_by_provider: create_issue_platform_occurrence(result, project_subscription) metrics.incr("uptime.result_processor.active.sent_occurrence", sample_rate=1.0) logger.info( diff --git a/tests/sentry/uptime/consumers/test_results_consumers.py b/tests/sentry/uptime/consumers/test_results_consumers.py index d716f88bb59ff..1ec189a2937f4 100644 --- a/tests/sentry/uptime/consumers/test_results_consumers.py +++ b/tests/sentry/uptime/consumers/test_results_consumers.py @@ -20,6 +20,7 @@ from sentry.issues.grouptype import UptimeDomainCheckFailure from sentry.models.group import Group, GroupStatus from sentry.testutils.cases import UptimeTestCase +from sentry.testutils.helpers.options import override_options from sentry.uptime.consumers.results_consumer import ( AUTO_DETECTED_ACTIVE_SUBSCRIPTION_INTERVAL, ONBOARDING_MONITOR_PERIOD, @@ -125,6 +126,42 @@ def test(self): self.project_subscription.refresh_from_db() assert self.project_subscription.uptime_status == UptimeStatus.FAILED + def test_restricted_host_provider_id(self): + """ + Test that we do NOT create an issue when the host provider identifier + has been restricted using the + `restrict-issue-creation-by-hosting-provider-id` option. + """ + result = self.create_uptime_result( + self.subscription.subscription_id, + scheduled_check_time=datetime.now() - timedelta(minutes=5), + ) + with ( + mock.patch("sentry.uptime.consumers.results_consumer.metrics") as metrics, + self.feature("organizations:uptime-create-issues"), + mock.patch( + "sentry.uptime.consumers.results_consumer.ACTIVE_FAILURE_THRESHOLD", + new=1, + ), + override_options({"uptime.restrict-issue-creation-by-hosting-provider-id": ["TEST"]}), + ): + self.send_result(result) + metrics.incr.assert_has_calls( + [ + call("uptime.result_processor.restricted_by_provider", sample_rate=1.0), + ], + any_order=True, + ) + + # Issue is not created + hashed_fingerprint = md5(str(self.project_subscription.id).encode("utf-8")).hexdigest() + with pytest.raises(Group.DoesNotExist): + Group.objects.get(grouphash__hash=hashed_fingerprint) + + # subscription status is still updated + self.project_subscription.refresh_from_db() + assert self.project_subscription.uptime_status == UptimeStatus.FAILED + def test_reset_fail_count(self): with ( mock.patch("sentry.uptime.consumers.results_consumer.metrics") as metrics,