Browse Source

feat(notification settings): Read team settings issueowners (#28617)

* ref(notification settings): Send to team slack channel for issue owners
Colleen O'Rourke 3 years ago
parent
commit
86ad4946f8

+ 31 - 4
src/sentry/notifications/utils/participants.py

@@ -185,7 +185,9 @@ def get_send_to(
     return {}
 
 
-def get_send_to_owners(event: "Event", project: Project) -> Mapping[ExternalProviders, Set[User]]:
+def get_send_to_owners(
+    event: "Event", project: Project
+) -> Mapping[ExternalProviders, Union[Set[User], Set[Team]]]:
     owners, _ = ProjectOwnership.get_owners(project.id, event.data)
     if owners == ProjectOwnership.Everyone:
         metrics.incr(
@@ -221,13 +223,38 @@ def get_send_to_owners(event: "Event", project: Project) -> Mapping[ExternalProv
     if user_ids_to_resolve:
         all_possible_users |= set(User.objects.filter(id__in=user_ids_to_resolve))
 
-    # Get all users in teams.
+    team_mapping = {ExternalProviders.SLACK: set()}
+    team_ids_to_remove = set()
     if team_ids_to_resolve:
+        # check for team Slack settings. if present, notify there instead
+        for team_id in team_ids_to_resolve:
+            team = Team.objects.get(id=team_id)
+            team_slack_settings = NotificationSetting.objects.get_settings(
+                provider=ExternalProviders.SLACK,
+                type=NotificationSettingTypes.ISSUE_ALERTS,
+                team=team,
+            )
+            if team_slack_settings == NotificationSettingOptionValues.ALWAYS:
+                team_mapping[ExternalProviders.SLACK].add(team)
+                team_ids_to_remove.add(team_id)
+        # Get all users in teams that don't have Slack settings.
+        team_ids_to_resolve -= team_ids_to_remove
         all_possible_users |= get_users_for_teams_to_resolve(team_ids_to_resolve)
-
     mapping: Mapping[
-        ExternalProviders, Set[User]
+        ExternalProviders, Union[Set[User], Set[Team]]
     ] = NotificationSetting.objects.filter_to_subscribed_users(project, all_possible_users)
+
+    if not mapping:
+        return team_mapping
+
+    # combine the user and team mappings
+    if team_mapping:
+        for provider in set.union(set(team_mapping.keys()), set(mapping.keys())):
+            if mapping.get(provider) and team_mapping.get(provider):
+                mapping[provider].update(list(team_mapping[provider]))
+            else:
+                if not mapping.get(provider) and team_mapping.get(provider):
+                    mapping[provider] = team_mapping[provider]
     return mapping
 
 

+ 94 - 0
tests/sentry/integrations/slack/test_notifications.py

@@ -18,6 +18,7 @@ from sentry.models import (
     IdentityStatus,
     Integration,
     NotificationSetting,
+    ProjectOwnership,
     Release,
     Rule,
     UserOption,
@@ -38,6 +39,9 @@ from sentry.notifications.types import (
     NotificationSettingOptionValues,
     NotificationSettingTypes,
 )
+from sentry.ownership.grammar import Matcher, Owner
+from sentry.ownership.grammar import Rule as GrammarRule
+from sentry.ownership.grammar import dump_schema
 from sentry.plugins.base import Notification
 from sentry.tasks.digests import deliver_digest
 from sentry.testutils import TestCase
@@ -569,6 +573,96 @@ class SlackActivityNotificationTest(ActivityTestCase, TestCase):
             == f"{self.project.slug} | <http://testserver/settings/account/notifications/alerts/?referrer=AlertRuleSlack|Notification Settings>"
         )
 
+    @responses.activate
+    @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
+    def test_issue_alert_team_issue_owners(self, mock_func):
+        """Test that issue alerts are sent to a team in Slack via an Issue Owners rule action."""
+
+        # add a second user to the team so we can be sure it's only
+        # sent once (to the team, and not to each individual user)
+        user2 = self.create_user(is_superuser=False)
+        self.create_member(teams=[self.team], user=user2, organization=self.organization)
+        self.idp = IdentityProvider.objects.create(type="slack", external_id="TXXXXXXX2", config={})
+        self.identity = Identity.objects.create(
+            external_id="UXXXXXXX2",
+            idp=self.idp,
+            user=user2,
+            status=IdentityStatus.VALID,
+            scopes=[],
+        )
+        NotificationSetting.objects.update_settings(
+            ExternalProviders.SLACK,
+            NotificationSettingTypes.ISSUE_ALERTS,
+            NotificationSettingOptionValues.ALWAYS,
+            user=user2,
+        )
+        # update the team's notification settings
+        ExternalActor.objects.create(
+            actor=self.team.actor,
+            organization=self.organization,
+            integration=self.integration,
+            provider=ExternalProviders.SLACK.value,
+            external_name="goma",
+            external_id="CXXXXXXX2",
+        )
+        NotificationSetting.objects.update_settings(
+            ExternalProviders.SLACK,
+            NotificationSettingTypes.ISSUE_ALERTS,
+            NotificationSettingOptionValues.ALWAYS,
+            team=self.team,
+        )
+
+        rule = GrammarRule(Matcher("path", "*"), [Owner("team", self.team.slug)])
+        ProjectOwnership.objects.create(
+            project_id=self.project.id, schema=dump_schema([rule]), fallthrough=True
+        )
+
+        event = self.store_event(
+            data={
+                "message": "Hello world",
+                "level": "error",
+                "stacktrace": {"frames": [{"filename": "foo.py"}]},
+            },
+            project_id=self.project.id,
+        )
+
+        action_data = {
+            "id": "sentry.mail.actions.NotifyEmailAction",
+            "targetType": "IssueOwners",
+            "targetIdentifier": "",
+        }
+        rule = Rule.objects.create(
+            project=self.project,
+            label="ja rule",
+            data={
+                "match": "all",
+                "actions": [action_data],
+            },
+        )
+
+        notification = AlertRuleNotification(
+            Notification(event=event, rule=rule), ActionTargetType.ISSUE_OWNERS, self.team.id
+        )
+
+        with self.tasks():
+            notification.send()
+
+        # check that only one was sent out - more would mean each user is being notified
+        # rather than the team
+        assert len(responses.calls) == 1
+
+        # check that the team got a notification
+        data = parse_qs(responses.calls[0].request.body)
+        assert data["channel"] == ["CXXXXXXX2"]
+        assert "attachments" in data
+        attachments = json.loads(data["attachments"][0])
+        assert len(attachments) == 1
+        assert attachments[0]["title"] == "Hello world"
+        assert (
+            attachments[0]["footer"]
+            == f"{self.project.slug} | <http://testserver/settings/{self.organization.slug}/teams/{self.team.slug}/notifications/?referrer=AlertRuleSlack|Notification Settings>"
+        )
+
     @responses.activate
     @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
     def test_issue_alert_team(self, mock_func):