Browse Source

feat(slack): Add toggle flags for threads (#71188)

Add flags to toggle features on and off for Slack threads



https://github.com/getsentry/sentry/assets/5581484/e0df749f-d0c9-48d2-a225-12b341552a92
Yash Kamothi 9 months ago
parent
commit
016f426340

+ 16 - 0
src/sentry/api/endpoints/organization_details.py

@@ -37,8 +37,10 @@ from sentry.constants import (
     DEBUG_FILES_ROLE_DEFAULT,
     EVENTS_MEMBER_ADMIN_DEFAULT,
     GITHUB_COMMENT_BOT_DEFAULT,
+    ISSUE_ALERTS_THREAD_DEFAULT,
     JOIN_REQUESTS_DEFAULT,
     LEGACY_RATE_LIMIT_OPTIONS,
+    METRIC_ALERTS_THREAD_DEFAULT,
     PROJECT_RATE_LIMIT_DEFAULT,
     REQUIRE_SCRUB_DATA_DEFAULT,
     REQUIRE_SCRUB_DEFAULTS_DEFAULT,
@@ -176,6 +178,18 @@ ORG_OPTIONS = (
     ),
     ("aggregatedDataConsent", "sentry:aggregated_data_consent", bool, DATA_CONSENT_DEFAULT),
     ("genAIConsent", "sentry:gen_ai_consent", bool, DATA_CONSENT_DEFAULT),
+    (
+        "issueAlertsThreadFlag",
+        "sentry:issue_alerts_thread_flag",
+        bool,
+        ISSUE_ALERTS_THREAD_DEFAULT,
+    ),
+    (
+        "metricAlertsThreadFlag",
+        "sentry:metric_alerts_thread_flag",
+        bool,
+        METRIC_ALERTS_THREAD_DEFAULT,
+    ),
 )
 
 DELETION_STATUSES = frozenset(
@@ -222,6 +236,8 @@ class OrganizationSerializer(BaseOrganizationSerializer):
     githubOpenPRBot = serializers.BooleanField(required=False)
     githubNudgeInvite = serializers.BooleanField(required=False)
     githubPRBot = serializers.BooleanField(required=False)
+    issueAlertsThreadFlag = serializers.BooleanField(required=False)
+    metricAlertsThreadFlag = serializers.BooleanField(required=False)
     aggregatedDataConsent = serializers.BooleanField(required=False)
     genAIConsent = serializers.BooleanField(required=False)
     require2FA = serializers.BooleanField(required=False)

+ 10 - 0
src/sentry/api/serializers/models/organization.py

@@ -33,7 +33,9 @@ from sentry.constants import (
     DEBUG_FILES_ROLE_DEFAULT,
     EVENTS_MEMBER_ADMIN_DEFAULT,
     GITHUB_COMMENT_BOT_DEFAULT,
+    ISSUE_ALERTS_THREAD_DEFAULT,
     JOIN_REQUESTS_DEFAULT,
+    METRIC_ALERTS_THREAD_DEFAULT,
     PROJECT_RATE_LIMIT_DEFAULT,
     REQUIRE_SCRUB_DATA_DEFAULT,
     REQUIRE_SCRUB_DEFAULTS_DEFAULT,
@@ -429,6 +431,8 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp
     aggregatedDataConsent: bool
     genAIConsent: bool
     isDynamicallySampled: bool
+    issueAlertsThreadFlag: bool
+    metricAlertsThreadFlag: bool
 
 
 class DetailedOrganizationSerializer(OrganizationSerializer):
@@ -547,6 +551,12 @@ class DetailedOrganizationSerializer(OrganizationSerializer):
                 "aggregatedDataConsent": bool(
                     obj.get_option("sentry:aggregated_data_consent", DATA_CONSENT_DEFAULT)
                 ),
+                "issueAlertsThreadFlag": bool(
+                    obj.get_option("sentry:issue_alerts_thread_flag", ISSUE_ALERTS_THREAD_DEFAULT)
+                ),
+                "metricAlertsThreadFlag": bool(
+                    obj.get_option("sentry:metric_alerts_thread_flag", METRIC_ALERTS_THREAD_DEFAULT)
+                ),
             }
         )
 

+ 2 - 0
src/sentry/constants.py

@@ -643,6 +643,8 @@ TRUSTED_RELAYS_DEFAULT = None
 JOIN_REQUESTS_DEFAULT = True
 AI_SUGGESTED_SOLUTION = True
 GITHUB_COMMENT_BOT_DEFAULT = True
+ISSUE_ALERTS_THREAD_DEFAULT = True
+METRIC_ALERTS_THREAD_DEFAULT = True
 DATA_CONSENT_DEFAULT = False
 
 # `sentry:events_member_admin` - controls whether the 'member' role gets the event:admin scope

+ 20 - 2
src/sentry/integrations/slack/service.py

@@ -4,6 +4,7 @@ from logging import Logger, getLogger
 
 import orjson
 
+from sentry.constants import ISSUE_ALERTS_THREAD_DEFAULT
 from sentry.integrations.repository import get_default_issue_alert_repository
 from sentry.integrations.repository.issue_alert import (
     IssueAlertNotificationMessage,
@@ -16,6 +17,7 @@ from sentry.integrations.slack.threads.activity_notifications import (
 )
 from sentry.integrations.utils.common import get_active_integration_for_organization
 from sentry.models.activity import Activity
+from sentry.models.options.organization_option import OrganizationOption
 from sentry.models.rule import Rule
 from sentry.notifications.notifications.activity.archive import ArchiveActivityNotification
 from sentry.notifications.notifications.activity.base import ActivityNotification
@@ -114,6 +116,22 @@ class SlackService:
             )
             return None
 
+        organization_id = activity.group.organization.id
+        # If the feature is turned off for the organization, exit early as there's nothing to do
+        if not OrganizationOption.objects.get_value(
+            organization=activity.group.organization,
+            key="sentry:issue_alerts_thread_flag",
+            default=ISSUE_ALERTS_THREAD_DEFAULT,
+        ):
+            self._logger.info(
+                "feature is turned off for this organization",
+                extra={
+                    "activity_id": activity.id,
+                    "organization_id": organization_id,
+                    "project_id": activity.project.id,
+                },
+            )
+            return None
         # The same message is sent to all the threads, so this needs to only happen once
         notification_to_send = self._get_notification_message_to_send(activity=activity)
         if not notification_to_send:
@@ -126,7 +144,7 @@ class SlackService:
             return None
 
         integration = get_active_integration_for_organization(
-            organization_id=activity.group.organization.id,
+            organization_id=organization_id,
             provider=ExternalProviderEnum.SLACK,
         )
         if integration is None:
@@ -134,7 +152,7 @@ class SlackService:
                 "no integration found for activity",
                 extra={
                     "activity_id": activity.id,
-                    "organization_id": activity.project.organization_id,
+                    "organization_id": organization_id,
                     "project_id": activity.project.id,
                 },
             )

+ 18 - 10
src/sentry/integrations/slack/utils/notifications.py

@@ -7,7 +7,7 @@ import orjson
 import sentry_sdk
 
 from sentry import features
-from sentry.constants import ObjectStatus
+from sentry.constants import METRIC_ALERTS_THREAD_DEFAULT, ObjectStatus
 from sentry.incidents.charts import build_metric_alert_chart
 from sentry.incidents.models.alert_rule import AlertRuleTriggerAction
 from sentry.incidents.models.incident import Incident, IncidentStatus
@@ -19,6 +19,7 @@ from sentry.integrations.repository.metric_alert import (
 from sentry.integrations.slack.client import SlackClient
 from sentry.integrations.slack.message_builder.incidents import SlackIncidentsMessageBuilder
 from sentry.models.integrations.integration import Integration
+from sentry.models.options.organization_option import OrganizationOption
 from sentry.services.hybrid_cloud.integration import integration_service
 from sentry.shared_integrations.exceptions import ApiError
 from sentry.shared_integrations.response import BaseApiResponse, MappingApiResponse
@@ -72,15 +73,22 @@ def send_incident_alert_notification(
 
     repository: MetricAlertNotificationMessageRepository = get_default_metric_alert_repository()
     parent_notification_message = None
-    try:
-        parent_notification_message = repository.get_parent_notification_message(
-            alert_rule_id=incident.alert_rule_id,
-            incident_id=incident.id,
-            trigger_action_id=action.id,
-        )
-    except Exception:
-        # if there's an error trying to grab a parent notification, don't let that error block this flow
-        pass
+    # Only grab the parent notification message for thread use if the feature is on
+    # Otherwise, leave it empty, and it will not create a thread
+    if OrganizationOption.objects.get_value(
+        organization=organization,
+        key="sentry:metric_alerts_thread_flag",
+        default=METRIC_ALERTS_THREAD_DEFAULT,
+    ):
+        try:
+            parent_notification_message = repository.get_parent_notification_message(
+                alert_rule_id=incident.alert_rule_id,
+                incident_id=incident.id,
+                trigger_action_id=action.id,
+            )
+        except Exception:
+            # if there's an error trying to grab a parent notification, don't let that error block this flow
+            pass
 
     new_notification_message_object = NewMetricAlertNotificationMessage(
         incident_id=incident.id,

+ 4 - 0
tests/sentry/api/endpoints/test_organization_details.py

@@ -426,6 +426,8 @@ class OrganizationUpdateTest(OrganizationDetailsTestBase):
             "allowJoinRequests": False,
             "aggregatedDataConsent": True,
             "genAIConsent": True,
+            "issueAlertsThreadFlag": False,
+            "metricAlertsThreadFlag": False,
         }
 
         # needed to set require2FA
@@ -489,6 +491,8 @@ class OrganizationUpdateTest(OrganizationDetailsTestBase):
         assert "to {}".format(data["githubNudgeInvite"]) in log.data["githubNudgeInvite"]
         assert "to {}".format(data["aggregatedDataConsent"]) in log.data["aggregatedDataConsent"]
         assert "to {}".format(data["genAIConsent"]) in log.data["genAIConsent"]
+        assert "to {}".format(data["issueAlertsThreadFlag"]) in log.data["issueAlertsThreadFlag"]
+        assert "to {}".format(data["metricAlertsThreadFlag"]) in log.data["metricAlertsThreadFlag"]
 
     @responses.activate
     @patch(