Repository: getsentry/sentry

Issue:
Add Issue Alerts Analytic after Message Sent for each Provider
Add issue alerts analytics after message sent for each provider: Slack, MS Teams, Discord, Email

AC:
- For each provider (Slack, MS Teams, Discord, Email):
  - After the message is successfully sent for an issue alert (ex. [Slack](https://github.com/getsentry/sentry/blob/a8bcd95b6f7fad15a7180d3cd74bc50547cd1cd3/src/sentry/integrations/slack/actions/notification.py#L71)), `record integrations.{provider.name}.alert_sent` that includes the `notification_uuid`


Discussion:

Resolving patch:
diff --git a/src/sentry/notifications/notifications/digest.py b/src/sentry/notifications/notifications/digest.py
--- a/src/sentry/notifications/notifications/digest.py
+++ b/src/sentry/notifications/notifications/digest.py
@@ -5,7 +5,7 @@
 from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Sequence
 from urllib.parse import urlencode
 
-from sentry import features
+from sentry import analytics, features
 from sentry.db.models import Model
 from sentry.digests import Digest
 from sentry.digests.utils import (
@@ -229,3 +229,17 @@ def get_log_params(self, recipient: RpcActor) -> Mapping[str, Any]:
             "alert_id": alert_id,
             **super().get_log_params(recipient),
         }
+
+    def record_notification_sent(self, recipient: RpcActor, provider: ExternalProviders) -> None:
+        super().record_notification_sent(recipient, provider)
+        log_params = self.get_log_params(recipient)
+        analytics.record(
+            "alert.sent",
+            organization_id=self.organization.id,
+            project_id=self.project.id,
+            provider=provider.name,
+            alert_id=log_params["alert_id"] if log_params["alert_id"] else "",
+            alert_type="issue_alert",
+            external_id=str(recipient.id),
+            notification_uuid=self.notification_uuid,
+        )
diff --git a/src/sentry/notifications/notifications/rules.py b/src/sentry/notifications/notifications/rules.py
--- a/src/sentry/notifications/notifications/rules.py
+++ b/src/sentry/notifications/notifications/rules.py
@@ -7,7 +7,7 @@
 
 import pytz
 
-from sentry import features
+from sentry import analytics, features
 from sentry.db.models import Model
 from sentry.eventstore.models import GroupEvent
 from sentry.issues.grouptype import GROUP_CATEGORIES_CUSTOM_EMAIL, GroupCategory
@@ -294,3 +294,17 @@ def get_log_params(self, recipient: RpcActor) -> Mapping[str, Any]:
             "alert_id": self.rules[0].id if self.rules else None,
             **super().get_log_params(recipient),
         }
+
+    def record_notification_sent(self, recipient: RpcActor, provider: ExternalProviders) -> None:
+        super().record_notification_sent(recipient, provider)
+        log_params = self.get_log_params(recipient)
+        analytics.record(
+            "alert.sent",
+            organization_id=self.organization.id,
+            project_id=self.project.id,
+            provider=provider.name,
+            alert_id=log_params["alert_id"] if log_params["alert_id"] else "",
+            alert_type="issue_alert",
+            external_id=str(recipient.id),
+            notification_uuid=self.notification_uuid,
+        )


Test patch:
diff --git a/tests/sentry/mail/test_adapter.py b/tests/sentry/mail/test_adapter.py
--- a/tests/sentry/mail/test_adapter.py
+++ b/tests/sentry/mail/test_adapter.py
@@ -191,7 +191,7 @@ def test_simple_notification(self, mock_record):
         assert isinstance(msg.alternatives[0][0], str)
         assert "my rule" in msg.alternatives[0][0]
         assert "notification_uuid" in msg.body
-        mock_record.assert_called_with(
+        mock_record.assert_any_call(
             "integrations.email.notification_sent",
             category="issue_alert",
             target_type=ANY,
@@ -206,6 +206,16 @@ def test_simple_notification(self, mock_record):
             notification_uuid=ANY,
             alert_id=rule.id,
         )
+        mock_record.assert_called_with(
+            "alert.sent",
+            organization_id=self.organization.id,
+            project_id=self.project.id,
+            provider="email",
+            alert_id=rule.id,
+            alert_type="issue_alert",
+            external_id=ANY,
+            notification_uuid=ANY,
+        )
 
     def test_simple_snooze(self):
         """Test that notification for alert snoozed by user is not send to that user."""
diff --git a/tests/sentry/notifications/notifications/test_digests.py b/tests/sentry/notifications/notifications/test_digests.py
--- a/tests/sentry/notifications/notifications/test_digests.py
+++ b/tests/sentry/notifications/notifications/test_digests.py
@@ -103,7 +103,7 @@ def test_sends_digest_to_every_member(self, mock_record):
         assert "N+1 Query" in mail.outbox[0].body
         assert "oh no" in mail.outbox[0].body
         assert self.build_occurrence_data()["issue_title"] in mail.outbox[0].body
-        mock_record.assert_called_with(
+        mock_record.assert_any_call(
             "integrations.email.notification_sent",
             category="digest",
             notification_uuid=ANY,
@@ -118,6 +118,16 @@ def test_sends_digest_to_every_member(self, mock_record):
             group_id=None,
             user_id=ANY,
         )
+        mock_record.assert_called_with(
+            "alert.sent",
+            organization_id=self.organization.id,
+            project_id=self.project.id,
+            provider="email",
+            alert_id=self.rule.id,
+            alert_type="issue_alert",
+            external_id=ANY,
+            notification_uuid=ANY,
+        )
 
     def test_sends_alert_rule_notification_to_each_member(self):
         """Test that if there is only one event it is sent as a regular alert rule notification"""