diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index d91e078f79ae35..cf2f33540feb0e 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -986,7 +986,6 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.tasks.web_vitals_issue_detection", "sentry.tasks.weekly_escalating_forecast", "sentry.tempest.tasks", - "sentry.uptime.autodetect.notifications", "sentry.uptime.autodetect.tasks", "sentry.uptime.consumers.tasks", "sentry.uptime.rdap.tasks", diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 83c6cedf3fe8c0..953d6297c0d39d 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -412,8 +412,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:view-hierarchy-scrubbing", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable AI-powered assertion suggestions for uptime monitors manager.add("organizations:uptime-ai-assertion-suggestions", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable email notifications when auto-detected uptime monitors graduate from onboarding - manager.add("organizations:uptime-auto-detected-monitor-emails", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable task-based retry for out-of-order uptime results manager.add("organizations:uptime-backlog-retry", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable storing HTTP response captures for uptime monitor failures diff --git a/src/sentry/templates/sentry/debug/mail/preview.html b/src/sentry/templates/sentry/debug/mail/preview.html index e055481beb8ba8..605471bc6507fc 100644 --- a/src/sentry/templates/sentry/debug/mail/preview.html +++ b/src/sentry/templates/sentry/debug/mail/preview.html @@ -42,9 +42,6 @@ Broken Cron Monitor Muted Cron Monitor - - Auto-Detected Uptime Monitor - Metric Alert Charts Discover Charts diff --git a/src/sentry/templates/sentry/emails/uptime/auto-detected-monitors.html b/src/sentry/templates/sentry/emails/uptime/auto-detected-monitors.html deleted file mode 100644 index b22d00089ba486..00000000000000 --- a/src/sentry/templates/sentry/emails/uptime/auto-detected-monitors.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "sentry/emails/base.html" %} - -{% load i18n %} -{% load sentry_helpers %} -{% load sentry_assets %} - -{% block head %} - {{ block.super }} - -{% endblock %} - -{% block header %} - - {{ block.super }} - - View on Sentry - - -{% endblock %} - -{% block main %} - We've Created a Free Uptime Monitor for Your Project - - We noticed that {{ monitor_url_display }} appears frequently in your project's errors, so we created an uptime monitor to help keep an eye on it. - - - {{ monitor_url_display }} | {{ project_slug }} - Created on {{ date_created|date:"N j, Y, g:i:s a e" }} - - - This monitor will check your URL every minute and alert you if it goes down. It's completely free, and you can customize it to adjust the check frequency, HTTP method, headers, or request body. - - - Not interested? You can remove this monitor anytime, and we won't create it again. - -{% endblock %} - -{% block footer %}{% endblock %} diff --git a/src/sentry/templates/sentry/emails/uptime/auto-detected-monitors.txt b/src/sentry/templates/sentry/emails/uptime/auto-detected-monitors.txt deleted file mode 100644 index e43886105f79f0..00000000000000 --- a/src/sentry/templates/sentry/emails/uptime/auto-detected-monitors.txt +++ /dev/null @@ -1,14 +0,0 @@ -We've Created a Free Uptime Monitor for Your Project - -We noticed that {{ monitor_url_display }} appears frequently in your project's errors, so we created an uptime monitor to help keep an eye on it. - -{{ monitor_url_display }} | {{ project_slug }} -Created on {{ date_created|date:"N j, Y, g:i:s a e" }} - -This monitor will check your URL every minute and alert you if it goes down. It's completely free, and you can customize it to adjust the check frequency, HTTP method, headers, or request body. - -{{ monitor_detail_url }} - -Not interested? You can remove this monitor anytime, and we won't create it again. - -{{ monitor_detail_url }} diff --git a/src/sentry/uptime/autodetect/notifications.py b/src/sentry/uptime/autodetect/notifications.py deleted file mode 100644 index 219e00dca54501..00000000000000 --- a/src/sentry/uptime/autodetect/notifications.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import annotations - -import logging - -from sentry import features -from sentry.models.organization import Organization -from sentry.models.project import Project -from sentry.tasks.base import instrumented_task -from sentry.taskworker.namespaces import uptime_tasks -from sentry.uptime.models import get_uptime_subscription -from sentry.uptime.types import UptimeMonitorMode -from sentry.utils.email import MessageBuilder -from sentry.utils.email.manager import get_email_addresses -from sentry.utils.http import absolute_uri -from sentry.workflow_engine.models.detector import Detector - -logger = logging.getLogger(__name__) - - -def get_user_ids_to_notify_from_project(project: Project): - """ - Get all user IDs who should be notified about auto-detected monitors in a project. - This returns all members of teams that are part of the project. - """ - return project.member_set.values_list("user_id", flat=True) - - -def get_user_emails_from_project(project: Project): - """ - Get verified email addresses for users who should be notified about auto-detected monitors. - """ - user_ids = get_user_ids_to_notify_from_project(project) - return get_email_addresses(user_ids, project, only_verified=True).values() - - -def generate_uptime_monitor_overview_url(organization: Organization): - """Generate URL to the uptime monitoring overview page.""" - return absolute_uri(f"/organizations/{organization.slug}/insights/uptime/") - - -def generate_uptime_monitor_detail_url( - organization: Organization, project_slug: str, detector_id: int -): - """Generate URL to a specific uptime monitor's detail page.""" - return absolute_uri( - f"/organizations/{organization.slug}/issues/alerts/rules/uptime/{project_slug}/{detector_id}/details/" - ) - - -@instrumented_task( - name="sentry.uptime.tasks.send_auto_detected_notifications", - namespace=uptime_tasks, - processing_deadline_duration=5 * 60, -) -def send_auto_detected_notifications(detector_id: int) -> None: - """ - Send an email notification to project members when an uptime monitor graduates - from onboarding to active monitoring. - - Args: - detector_id: The ID of the Detector instance that has graduated to active monitoring - """ - try: - detector = Detector.objects.get(id=detector_id) - except Detector.DoesNotExist: - return - - mode = detector.config.get("mode") - if mode != UptimeMonitorMode.AUTO_DETECTED_ACTIVE: - return - - project = Project.objects.get_from_cache(id=detector.project_id) - organization = project.organization - - if not features.has("organizations:uptime-auto-detected-monitor-emails", organization): - return - - uptime_subscription = get_uptime_subscription(detector) - user_emails = list(get_user_emails_from_project(project)) - - if not user_emails: - logger.info( - "uptime.autodetect.no_emails_to_notify", - extra={ - "detector_id": detector.id, - "project_id": project.id, - "organization_id": organization.id, - }, - ) - return - - monitor_detail_url = generate_uptime_monitor_detail_url(organization, project.slug, detector.id) - view_monitors_link = generate_uptime_monitor_overview_url(organization) - - context = { - "monitor_url_display": uptime_subscription.url, - "monitor_detail_url": monitor_detail_url, - "project_slug": project.slug, - "date_created": detector.date_added, - "view_monitors_link": view_monitors_link, - } - - message = MessageBuilder( - subject="We've Created a Free Uptime Monitor for Your Project", - template="sentry/emails/uptime/auto-detected-monitors.txt", - html_template="sentry/emails/uptime/auto-detected-monitors.html", - type="uptime.auto_detected_monitors", - context=context, - ) - message.send_async(user_emails) - - logger.info( - "uptime.autodetect.email_sent", - extra={ - "detector_id": detector.id, - "project_id": project.id, - "organization_id": organization.id, - "num_recipients": len(user_emails), - }, - ) diff --git a/src/sentry/uptime/autodetect/result_handler.py b/src/sentry/uptime/autodetect/result_handler.py index 736af143927beb..dd69f4ec58f6c8 100644 --- a/src/sentry/uptime/autodetect/result_handler.py +++ b/src/sentry/uptime/autodetect/result_handler.py @@ -10,7 +10,6 @@ ) from sentry import audit_log -from sentry.uptime.autodetect.notifications import send_auto_detected_notifications from sentry.uptime.autodetect.tasks import set_failed_url from sentry.uptime.models import UptimeSubscription, get_audit_log_data from sentry.uptime.subscriptions.subscriptions import ( @@ -122,8 +121,6 @@ def handle_onboarding_result( data=get_audit_log_data(detector), ) - send_auto_detected_notifications.delay(detector.id) - metrics.incr( "uptime.result_processor.autodetection.graduated_onboarding", sample_rate=1.0, diff --git a/src/sentry/web/debug_urls.py b/src/sentry/web/debug_urls.py index 4bea9662f5c7dc..ab6a85628948b5 100644 --- a/src/sentry/web/debug_urls.py +++ b/src/sentry/web/debug_urls.py @@ -69,9 +69,6 @@ DebugUnableToDeleteRepository, ) from sentry.web.frontend.debug.debug_unassigned_email import DebugUnassignedEmailView -from sentry.web.frontend.debug.debug_uptime_auto_detected_monitor_email import ( - DebugUptimeAutoDetectedMonitorEmailView, -) from sentry.web.frontend.debug.debug_weekly_report import DebugWeeklyReportView urlpatterns = [ @@ -159,8 +156,4 @@ re_path(r"^debug/charts/metric-alert-charts/$", DebugMetricAlertChartRendererView.as_view()), re_path(r"^debug/mail/cron-broken-monitor-email/$", DebugCronBrokenMonitorEmailView.as_view()), re_path(r"^debug/mail/cron-muted-monitor-email/$", DebugCronMutedMonitorEmailView.as_view()), - re_path( - r"^debug/mail/uptime-auto-detected-monitor-email/$", - DebugUptimeAutoDetectedMonitorEmailView.as_view(), - ), ] diff --git a/src/sentry/web/frontend/debug/debug_uptime_auto_detected_monitor_email.py b/src/sentry/web/frontend/debug/debug_uptime_auto_detected_monitor_email.py deleted file mode 100644 index 4da51fcee0457e..00000000000000 --- a/src/sentry/web/frontend/debug/debug_uptime_auto_detected_monitor_email.py +++ /dev/null @@ -1,31 +0,0 @@ -import datetime - -from django.http import HttpRequest, HttpResponse -from django.views.generic import View - -from sentry.web.frontend.base import internal_cell_silo_view - -from .mail import MailPreview - - -def get_context(): - date = datetime.datetime(2025, 1, 16, 10, 30, tzinfo=datetime.UTC) - return { - "monitor_url_display": "https://api.example.com", - "monitor_detail_url": "#", - "project_slug": "my-project", - "date_created": date, - "view_monitors_link": "#", - } - - -@internal_cell_silo_view -class DebugUptimeAutoDetectedMonitorEmailView(View): - def get(self, request: HttpRequest) -> HttpResponse: - context = get_context() - - return MailPreview( - text_template="sentry/emails/uptime/auto-detected-monitors.txt", - html_template="sentry/emails/uptime/auto-detected-monitors.html", - context=context, - ).render(request) diff --git a/tests/sentry/uptime/autodetect/test_notifications.py b/tests/sentry/uptime/autodetect/test_notifications.py deleted file mode 100644 index 78254a76724b3b..00000000000000 --- a/tests/sentry/uptime/autodetect/test_notifications.py +++ /dev/null @@ -1,198 +0,0 @@ -from unittest.mock import Mock, patch - -from sentry.silo.base import SiloMode -from sentry.testutils.cases import TestCase -from sentry.testutils.silo import assume_test_silo_mode -from sentry.uptime.autodetect.notifications import ( - generate_uptime_monitor_detail_url, - generate_uptime_monitor_overview_url, - get_user_emails_from_project, - send_auto_detected_notifications, -) -from sentry.uptime.models import UptimeSubscription -from sentry.uptime.types import UptimeMonitorMode -from sentry.users.models.user_option import UserOption -from sentry.users.models.useremail import UserEmail -from sentry.workflow_engine.models.detector import Detector - - -class UptimeAutoDetectedNotificationsTest(TestCase): - def setUp(self) -> None: - super().setUp() - self.organization = self.create_organization() - self.project = self.create_project(organization=self.organization) - self.user1 = self.create_user("user1@example.com") - self.user2 = self.create_user("user2@example.com") - self.create_member(user=self.user1, organization=self.organization) - self.create_member(user=self.user2, organization=self.organization) - self.team = self.create_team( - organization=self.organization, members=[self.user1, self.user2] - ) - self.project.add_team(self.team) - self.features = {"organizations:uptime-auto-detected-monitor-emails": True} - - def create_test_detector( - self, url: str = "https://example.com" - ) -> tuple[Detector, UptimeSubscription]: - uptime_subscription = self.create_uptime_subscription(url=url) - detector = self.create_uptime_detector( - project=self.project, - mode=UptimeMonitorMode.AUTO_DETECTED_ACTIVE, - uptime_subscription=uptime_subscription, - ) - return detector, uptime_subscription - - def test_get_user_emails_from_project(self) -> None: - """Test that we get all project member emails.""" - emails = set(get_user_emails_from_project(self.project)) - assert emails == {"user1@example.com", "user2@example.com"} - - def test_get_user_emails_with_alternate_email(self) -> None: - """Test that alternate project-specific emails are respected.""" - with assume_test_silo_mode(SiloMode.CONTROL): - UserEmail.objects.create( - user=self.user1, email="alternate1@example.com", is_verified=True - ) - UserOption.objects.create( - user=self.user1, - key="mail:email", - project_id=self.project.id, - value="alternate1@example.com", - ) - - emails = set(get_user_emails_from_project(self.project)) - assert emails == {"alternate1@example.com", "user2@example.com"} - - def test_get_user_emails_skips_unverified(self) -> None: - """Test that unverified alternate emails are not used.""" - with assume_test_silo_mode(SiloMode.CONTROL): - UserEmail.objects.create( - user=self.user1, email="unverified@example.com", is_verified=False - ) - UserOption.objects.create( - user=self.user1, - key="mail:email", - project_id=self.project.id, - value="unverified@example.com", - ) - - emails = set(get_user_emails_from_project(self.project)) - assert emails == {"user2@example.com"} - assert "unverified@example.com" not in emails - - def test_generate_uptime_monitor_overview_url(self) -> None: - """Test URL generation for uptime monitoring overview.""" - url = generate_uptime_monitor_overview_url(self.organization) - assert f"/organizations/{self.organization.slug}/insights/uptime/" in url - - def test_generate_uptime_monitor_detail_url(self) -> None: - """Test URL generation for specific detector details.""" - detector, _ = self.create_test_detector() - url = generate_uptime_monitor_detail_url(self.organization, self.project.slug, detector.id) - assert f"/organizations/{self.organization.slug}/issues/alerts/rules/uptime/" in url - assert f"/{self.project.slug}/{detector.id}/details/" in url - - @patch("sentry.uptime.autodetect.notifications.MessageBuilder") - def test_send_auto_detected_notifications(self, mock_builder: Mock) -> None: - """Test that email is sent to all project members on graduation.""" - mock_builder.return_value.send_async = Mock() - detector, uptime_subscription = self.create_test_detector(url="https://api.example.com") - - with self.feature(self.features): - send_auto_detected_notifications(detector.id) - - assert mock_builder.call_count == 1 - call_kwargs = mock_builder.call_args[1] - assert call_kwargs["subject"] == "We've Created a Free Uptime Monitor for Your Project" - assert call_kwargs["template"] == "sentry/emails/uptime/auto-detected-monitors.txt" - assert call_kwargs["html_template"] == "sentry/emails/uptime/auto-detected-monitors.html" - assert call_kwargs["type"] == "uptime.auto_detected_monitors" - - context = call_kwargs["context"] - assert "monitor_url_display" in context - assert "monitor_detail_url" in context - assert "project_slug" in context - assert "date_created" in context - assert "view_monitors_link" in context - - assert context["monitor_url_display"] == uptime_subscription.url - assert context["project_slug"] == self.project.slug - assert ( - f"/organizations/{self.organization.slug}/issues/alerts/rules/uptime/" - in context["monitor_detail_url"] - ) - assert context["date_created"] == detector.date_added - - mock_builder.return_value.send_async.assert_called_once() - email_recipients = mock_builder.return_value.send_async.call_args[0][0] - assert set(email_recipients) == {"user1@example.com", "user2@example.com"} - - @patch("sentry.uptime.autodetect.notifications.MessageBuilder") - def test_send_auto_detected_notifications_no_members(self, mock_builder: Mock) -> None: - """Test that no email is sent if project has no members.""" - empty_org = self.create_organization() - empty_project = self.create_project(organization=empty_org) - detector = self.create_uptime_detector( - project=empty_project, - mode=UptimeMonitorMode.AUTO_DETECTED_ACTIVE, - ) - - with self.feature({"organizations:uptime-auto-detected-monitor-emails": True}): - send_auto_detected_notifications(detector.id) - - mock_builder.return_value.send_async.assert_not_called() - - @patch("sentry.uptime.autodetect.notifications.MessageBuilder") - def test_send_auto_detected_notifications_multiple_teams(self, mock_builder: Mock) -> None: - """Test email sent to all members across multiple teams.""" - mock_builder.return_value.send_async = Mock() - - user3 = self.create_user("user3@example.com") - user4 = self.create_user("user4@example.com") - self.create_member(user=user3, organization=self.organization) - self.create_member(user=user4, organization=self.organization) - team2 = self.create_team(organization=self.organization, members=[user3, user4]) - self.project.add_team(team2) - - detector, _ = self.create_test_detector() - with self.feature(self.features): - send_auto_detected_notifications(detector.id) - - email_recipients = mock_builder.return_value.send_async.call_args[0][0] - assert set(email_recipients) == { - "user1@example.com", - "user2@example.com", - "user3@example.com", - "user4@example.com", - } - - @patch("sentry.uptime.autodetect.notifications.MessageBuilder") - def test_send_auto_detected_notifications_excludes_non_team_members( - self, mock_builder: Mock - ) -> None: - """Test that users not on project teams don't receive emails.""" - mock_builder.return_value.send_async = Mock() - - user_not_in_team = self.create_user("outsider@example.com") - self.create_member(user=user_not_in_team, organization=self.organization) - - detector, _ = self.create_test_detector() - with self.feature(self.features): - send_auto_detected_notifications(detector.id) - - email_recipients = mock_builder.return_value.send_async.call_args[0][0] - assert set(email_recipients) == {"user1@example.com", "user2@example.com"} - assert "outsider@example.com" not in email_recipients - - @patch("sentry.uptime.autodetect.notifications.MessageBuilder") - def test_send_auto_detected_notifications_feature_flag_disabled( - self, mock_builder: Mock - ) -> None: - """Test that no email is sent when feature flag is disabled.""" - mock_builder.return_value.send_async = Mock() - detector, _ = self.create_test_detector(url="https://api.example.com") - - with self.feature({"organizations:uptime-auto-detected-monitor-emails": False}): - send_auto_detected_notifications(detector.id) - - mock_builder.return_value.send_async.assert_not_called() diff --git a/tests/sentry/uptime/consumers/test_results_consumer.py b/tests/sentry/uptime/consumers/test_results_consumer.py index 7a17f7bd8931d8..353d793034d2cb 100644 --- a/tests/sentry/uptime/consumers/test_results_consumer.py +++ b/tests/sentry/uptime/consumers/test_results_consumer.py @@ -806,9 +806,6 @@ def test_onboarding_success_graduate(self) -> None: with ( mock.patch("sentry.uptime.consumers.results_consumer.metrics") as consumer_metrics, mock.patch("sentry.uptime.autodetect.result_handler.metrics") as onboarding_metrics, - mock.patch( - "sentry.uptime.autodetect.result_handler.send_auto_detected_notifications" - ) as mock_email_task, self.tasks(), self.feature(features), ): @@ -841,7 +838,6 @@ def test_onboarding_success_graduate(self) -> None: ), ] ) - mock_email_task.delay.assert_called_once_with(self.detector.id) assert not redis.exists(key) fingerprint = build_detector_fingerprint_component(self.detector).encode("utf-8")
- We noticed that {{ monitor_url_display }} appears frequently in your project's errors, so we created an uptime monitor to help keep an eye on it. -
- {{ monitor_url_display }} | {{ project_slug }} - Created on {{ date_created|date:"N j, Y, g:i:s a e" }} -
- This monitor will check your URL every minute and alert you if it goes down. It's completely free, and you can customize it to adjust the check frequency, HTTP method, headers, or request body. -
- Not interested? You can remove this monitor anytime, and we won't create it again. -