We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Discord `alert.sent` event
start sending `alert.sent` analytic events for discord

add category to existing notification_sent event

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/sentry/rules/actions/integrations/base.py`
Content:
```
1 from __future__ import annotations
2 
3 import abc
4 from typing import List
5 
6 from django import forms
7 
8 from sentry.models import OrganizationStatus
9 from sentry.rules.actions import EventAction
10 from sentry.services.hybrid_cloud.integration import (
11     RpcIntegration,
12     RpcOrganizationIntegration,
13     integration_service,
14 )
15 
16 INTEGRATION_KEY = "integration"
17 
18 
19 class IntegrationEventAction(EventAction, abc.ABC):
20     """Intermediate abstract class to help DRY some event actions code."""
21 
22     @property
23     @abc.abstractmethod
24     def prompt(self) -> str:
25         pass
26 
27     @property
28     @abc.abstractmethod
29     def provider(self) -> str:
30         pass
31 
32     @property
33     @abc.abstractmethod
34     def integration_key(self) -> str:
35         pass
36 
37     def is_enabled(self) -> bool:
38         enabled: bool = bool(self.get_integrations())
39         return enabled
40 
41     def get_integration_name(self) -> str:
42         """Get the integration's name for the label."""
43         integration = self.get_integration()
44         if not integration:
45             return "[removed]"
46 
47         _name: str = integration.name
48         return _name
49 
50     def get_integrations(self) -> List[RpcIntegration]:
51         return integration_service.get_integrations(
52             organization_id=self.project.organization_id,
53             status=OrganizationStatus.ACTIVE,
54             org_integration_status=OrganizationStatus.ACTIVE,
55             providers=[self.provider],
56         )
57 
58     def get_integration_id(self) -> int:
59         integration_id: str | None = self.get_option(self.integration_key)
60         if integration_id:
61             return int(integration_id)
62         return 0
63 
64     def get_integration(self) -> RpcIntegration | None:
65         """
66         Uses the required class variables `provider` and `integration_key` with
67         RuleBase.get_option to get the integration object from DB.
68         """
69         for integration in integration_service.get_integrations(
70             organization_id=self.project.organization_id,
71             status=OrganizationStatus.ACTIVE,
72             org_integration_status=OrganizationStatus.ACTIVE,
73             providers=[self.provider],
74         ):
75             if integration.id == self.get_integration_id():
76                 return integration
77         return None
78 
79     def get_organization_integration(self) -> RpcOrganizationIntegration | None:
80         return integration_service.get_organization_integration(
81             integration_id=self.get_integration_id(), organization_id=self.project.organization_id
82         )
83 
84     def get_form_instance(self) -> forms.Form:
85         return self.form_cls(self.data, integrations=self.get_integrations())
86
```

Path: `src/sentry/integrations/discord/analytics.py`
Content:
```
1 from sentry import analytics
2 
3 
4 class DiscordIntegrationNotificationSent(analytics.Event):
5     type = "integrations.discord.notification_sent"
6 
7     attributes = (
8         analytics.Attribute("organization_id"),
9         analytics.Attribute("project_id"),
10         analytics.Attribute("group_id"),
11         analytics.Attribute("notification_uuid"),
12         analytics.Attribute("alert_id", required=False),
13     )
14 
15 
16 class DiscordIntegrationCommandInteractionReceived(analytics.Event):
17     type = "integrations.discord.command_interaction"
18 
19     attributes = (analytics.Attribute("command_name"),)
20 
21 
22 class DiscordIntegrationIdentityLinked(analytics.Event):
23     type = "integrations.discord.identity_linked"
24 
25     attributes = (
26         analytics.Attribute("provider"),
27         analytics.Attribute("actor_id"),
28         analytics.Attribute("actor_type"),
29     )
30 
31 
32 class DiscordIntegrationIdentityUnlinked(analytics.Event):
33     type = "integrations.discord.identity_unlinked"
34 
35     attributes = (
36         analytics.Attribute("provider"),
37         analytics.Attribute("actor_id"),
38         analytics.Attribute("actor_type"),
39     )
40 
41 
42 class DiscordIntegrationMessageInteractionReceived(analytics.Event):
43     type = "integrations.discord.message_interaction"
44 
45     attributes = (analytics.Attribute("custom_id"),)
46 
47 
48 class DiscordIntegrationAssign(analytics.Event):
49     type = "integrations.discord.assign"
50 
51     attributes = (analytics.Attribute("actor_id"),)
52 
53 
54 class DiscordIntegrationStatus(analytics.Event):
55     type = "integrations.discord.status"
56 
57     attributes = (
58         analytics.Attribute("organization_id"),
59         analytics.Attribute("user_id"),
60         analytics.Attribute("status"),
61     )
62 
63 
64 analytics.register(DiscordIntegrationNotificationSent)
65 analytics.register(DiscordIntegrationCommandInteractionReceived)
66 analytics.register(DiscordIntegrationIdentityLinked)
67 analytics.register(DiscordIntegrationIdentityUnlinked)
68 analytics.register(DiscordIntegrationMessageInteractionReceived)
69 analytics.register(DiscordIntegrationAssign)
70 analytics.register(DiscordIntegrationStatus)
71
```

Path: `src/sentry/integrations/discord/actions/notification.py`
Content:
```
1 from typing import Any, Generator, Optional, Sequence
2 
3 from sentry import analytics, features
4 from sentry.eventstore.models import GroupEvent
5 from sentry.integrations.discord.actions.form import DiscordNotifyServiceForm
6 from sentry.integrations.discord.client import DiscordClient
7 from sentry.integrations.discord.message_builder.issues import DiscordIssuesMessageBuilder
8 from sentry.rules.actions import IntegrationEventAction
9 from sentry.rules.base import CallbackFuture, EventState
10 from sentry.shared_integrations.exceptions.base import ApiError
11 from sentry.types.rules import RuleFuture
12 from sentry.utils import metrics
13 
14 
15 class DiscordNotifyServiceAction(IntegrationEventAction):
16     id = "sentry.integrations.discord.notify_action.DiscordNotifyServiceAction"
17     form_cls = DiscordNotifyServiceForm
18     label = "Send a notification to the {server} Discord server in the channel with ID: {channel_id} and show tags {tags} in the notification."
19     prompt = "Send a Discord notification"
20     provider = "discord"
21     integration_key = "server"
22 
23     def __init__(self, *args: Any, **kwargs: Any) -> None:
24         super().__init__(*args, **kwargs)
25         self.form_fields = {
26             "server": {
27                 "type": "choice",
28                 "choices": [(i.id, i.name) for i in self.get_integrations()],
29             },
30             "channel_id": {"type": "string", "placeholder": "e.g., 1134274732116676679"},
31             "tags": {"type": "string", "placeholder": "e.g., environment,user,my_tag"},
32         }
33 
34     def after(
35         self, event: GroupEvent, state: EventState, notification_uuid: Optional[str] = None
36     ) -> Generator[CallbackFuture, None, None]:
37         channel_id = self.get_option("channel_id")
38         tags = set(self.get_tags_list())
39 
40         integration = self.get_integration()
41         if not integration:
42             # Integration removed, but rule still active
43             return
44 
45         def send_notification(event: GroupEvent, futures: Sequence[RuleFuture]) -> None:
46             if not features.has(
47                 "organizations:integrations-discord-notifications", event.organization
48             ):
49                 return
50 
51             rules = [f.rule for f in futures]
52             message = DiscordIssuesMessageBuilder(event.group, event=event, tags=tags, rules=rules)
53 
54             client = DiscordClient(integration_id=integration.id)
55             try:
56                 client.send_message(channel_id, message, notification_uuid=notification_uuid)
57             except ApiError as e:
58                 self.logger.error(
59                     "rule.fail.discord_post",
60                     extra={
61                         "error": str(e),
62                         "project_id": event.project_id,
63                         "event_id": event.event_id,
64                         "guild_id": integration.external_id,
65                         "channel_id": channel_id,
66                     },
67                 )
68 
69         key = f"discord:{integration.id}:{channel_id}"
70 
71         analytics.record(
72             "integrations.discord.notification_sent",
73             organization_id=event.organization.id,
74             project_id=event.project_id,
75             group_id=event.group_id,
76             notification_uuid=notification_uuid if notification_uuid else "",
77         )
78         metrics.incr("notifications.sent", instance="discord.notifications", skip_internal=False)
79         yield self.future(send_notification, key=key)
80 
81     def render_label(self) -> str:
82         tags = self.get_tags_list()
83 
84         return self.label.format(
85             server=self.get_integration_name(),
86             channel_id=self.get_option("channel_id"),
87             tags="[{}]".format(", ".join(tags)),
88         )
89 
90     def get_tags_list(self) -> Sequence[str]:
91         return [s.strip() for s in self.get_option("tags", "").split(",")]
92 
93     def get_form_instance(self) -> Any:
94         return self.form_cls(self.data, integrations=self.get_integrations())
95
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/sentry/integrations/discord/actions/notification.py b/src/sentry/integrations/discord/actions/notification.py
--- a/src/sentry/integrations/discord/actions/notification.py
+++ b/src/sentry/integrations/discord/actions/notification.py
@@ -1,6 +1,6 @@
 from typing import Any, Generator, Optional, Sequence
 
-from sentry import analytics, features
+from sentry import features
 from sentry.eventstore.models import GroupEvent
 from sentry.integrations.discord.actions.form import DiscordNotifyServiceForm
 from sentry.integrations.discord.client import DiscordClient
@@ -65,16 +65,11 @@
                         "channel_id": channel_id,
                     },
                 )
+            rule = rules[0] if rules else None
+            self.record_notification_sent(event, channel_id, rule, notification_uuid)
 
         key = f"discord:{integration.id}:{channel_id}"
 
-        analytics.record(
-            "integrations.discord.notification_sent",
-            organization_id=event.organization.id,
-            project_id=event.project_id,
-            group_id=event.group_id,
-            notification_uuid=notification_uuid if notification_uuid else "",
-        )
         metrics.incr("notifications.sent", instance="discord.notifications", skip_internal=False)
         yield self.future(send_notification, key=key)
 
diff --git a/src/sentry/integrations/discord/analytics.py b/src/sentry/integrations/discord/analytics.py
--- a/src/sentry/integrations/discord/analytics.py
+++ b/src/sentry/integrations/discord/analytics.py
@@ -7,6 +7,7 @@
     attributes = (
         analytics.Attribute("organization_id"),
         analytics.Attribute("project_id"),
+        analytics.Attribute("category"),
         analytics.Attribute("group_id"),
         analytics.Attribute("notification_uuid"),
         analytics.Attribute("alert_id", required=False),
diff --git a/src/sentry/rules/actions/integrations/base.py b/src/sentry/rules/actions/integrations/base.py
--- a/src/sentry/rules/actions/integrations/base.py
+++ b/src/sentry/rules/actions/integrations/base.py
@@ -5,7 +5,9 @@
 
 from django import forms
 
-from sentry.models import OrganizationStatus
+from sentry import analytics
+from sentry.eventstore.models import GroupEvent
+from sentry.models import OrganizationStatus, Rule
 from sentry.rules.actions import EventAction
 from sentry.services.hybrid_cloud.integration import (
     RpcIntegration,
@@ -83,3 +85,31 @@
 
     def get_form_instance(self) -> forms.Form:
         return self.form_cls(self.data, integrations=self.get_integrations())
+
+    def record_notification_sent(
+        self,
+        event: GroupEvent,
+        external_id: str,
+        rule: Rule | None = None,
+        notification_uuid: str | None = None,
+    ) -> None:
+        # Currently these actions can only be triggered by issue alerts
+        analytics.record(
+            f"integrations.{self.provider}.notification_sent",
+            category="issue_alert",
+            organization_id=event.organization.id,
+            project_id=event.project_id,
+            group_id=event.group_id,
+            notification_uuid=notification_uuid if notification_uuid else "",
+            alert_id=rule.id if rule else None,
+        )
+        analytics.record(
+            "alert.sent",
+            provider=self.provider,
+            alert_id=rule.id if rule else "",
+            alert_type="issue_alert",
+            organization_id=event.organization.id,
+            project_id=event.project_id,
+            external_id=external_id,
+            notification_uuid=notification_uuid if notification_uuid else "",
+        )

{"golden_diff": "diff --git a/src/sentry/integrations/discord/actions/notification.py b/src/sentry/integrations/discord/actions/notification.py\n--- a/src/sentry/integrations/discord/actions/notification.py\n+++ b/src/sentry/integrations/discord/actions/notification.py\n@@ -1,6 +1,6 @@\n from typing import Any, Generator, Optional, Sequence\n \n-from sentry import analytics, features\n+from sentry import features\n from sentry.eventstore.models import GroupEvent\n from sentry.integrations.discord.actions.form import DiscordNotifyServiceForm\n from sentry.integrations.discord.client import DiscordClient\n@@ -65,16 +65,11 @@\n                         \"channel_id\": channel_id,\n                     },\n                 )\n+            rule = rules[0] if rules else None\n+            self.record_notification_sent(event, channel_id, rule, notification_uuid)\n \n         key = f\"discord:{integration.id}:{channel_id}\"\n \n-        analytics.record(\n-            \"integrations.discord.notification_sent\",\n-            organization_id=event.organization.id,\n-            project_id=event.project_id,\n-            group_id=event.group_id,\n-            notification_uuid=notification_uuid if notification_uuid else \"\",\n-        )\n         metrics.incr(\"notifications.sent\", instance=\"discord.notifications\", skip_internal=False)\n         yield self.future(send_notification, key=key)\n \ndiff --git a/src/sentry/integrations/discord/analytics.py b/src/sentry/integrations/discord/analytics.py\n--- a/src/sentry/integrations/discord/analytics.py\n+++ b/src/sentry/integrations/discord/analytics.py\n@@ -7,6 +7,7 @@\n     attributes = (\n         analytics.Attribute(\"organization_id\"),\n         analytics.Attribute(\"project_id\"),\n+        analytics.Attribute(\"category\"),\n         analytics.Attribute(\"group_id\"),\n         analytics.Attribute(\"notification_uuid\"),\n         analytics.Attribute(\"alert_id\", required=False),\ndiff --git a/src/sentry/rules/actions/integrations/base.py b/src/sentry/rules/actions/integrations/base.py\n--- a/src/sentry/rules/actions/integrations/base.py\n+++ b/src/sentry/rules/actions/integrations/base.py\n@@ -5,7 +5,9 @@\n \n from django import forms\n \n-from sentry.models import OrganizationStatus\n+from sentry import analytics\n+from sentry.eventstore.models import GroupEvent\n+from sentry.models import OrganizationStatus, Rule\n from sentry.rules.actions import EventAction\n from sentry.services.hybrid_cloud.integration import (\n     RpcIntegration,\n@@ -83,3 +85,31 @@\n \n     def get_form_instance(self) -> forms.Form:\n         return self.form_cls(self.data, integrations=self.get_integrations())\n+\n+    def record_notification_sent(\n+        self,\n+        event: GroupEvent,\n+        external_id: str,\n+        rule: Rule | None = None,\n+        notification_uuid: str | None = None,\n+    ) -> None:\n+        # Currently these actions can only be triggered by issue alerts\n+        analytics.record(\n+            f\"integrations.{self.provider}.notification_sent\",\n+            category=\"issue_alert\",\n+            organization_id=event.organization.id,\n+            project_id=event.project_id,\n+            group_id=event.group_id,\n+            notification_uuid=notification_uuid if notification_uuid else \"\",\n+            alert_id=rule.id if rule else None,\n+        )\n+        analytics.record(\n+            \"alert.sent\",\n+            provider=self.provider,\n+            alert_id=rule.id if rule else \"\",\n+            alert_type=\"issue_alert\",\n+            organization_id=event.organization.id,\n+            project_id=event.project_id,\n+            external_id=external_id,\n+            notification_uuid=notification_uuid if notification_uuid else \"\",\n+        )\n", "issue": "Discord `alert.sent` event\nstart sending `alert.sent` analytic events for discord\n\nadd category to existing notification_sent event\n", "before_files": [{"content": "from __future__ import annotations\n\nimport abc\nfrom typing import List\n\nfrom django import forms\n\nfrom sentry.models import OrganizationStatus\nfrom sentry.rules.actions import EventAction\nfrom sentry.services.hybrid_cloud.integration import (\n    RpcIntegration,\n    RpcOrganizationIntegration,\n    integration_service,\n)\n\nINTEGRATION_KEY = \"integration\"\n\n\nclass IntegrationEventAction(EventAction, abc.ABC):\n    \"\"\"Intermediate abstract class to help DRY some event actions code.\"\"\"\n\n    @property\n    @abc.abstractmethod\n    def prompt(self) -> str:\n        pass\n\n    @property\n    @abc.abstractmethod\n    def provider(self) -> str:\n        pass\n\n    @property\n    @abc.abstractmethod\n    def integration_key(self) -> str:\n        pass\n\n    def is_enabled(self) -> bool:\n        enabled: bool = bool(self.get_integrations())\n        return enabled\n\n    def get_integration_name(self) -> str:\n        \"\"\"Get the integration's name for the label.\"\"\"\n        integration = self.get_integration()\n        if not integration:\n            return \"[removed]\"\n\n        _name: str = integration.name\n        return _name\n\n    def get_integrations(self) -> List[RpcIntegration]:\n        return integration_service.get_integrations(\n            organization_id=self.project.organization_id,\n            status=OrganizationStatus.ACTIVE,\n            org_integration_status=OrganizationStatus.ACTIVE,\n            providers=[self.provider],\n        )\n\n    def get_integration_id(self) -> int:\n        integration_id: str | None = self.get_option(self.integration_key)\n        if integration_id:\n            return int(integration_id)\n        return 0\n\n    def get_integration(self) -> RpcIntegration | None:\n        \"\"\"\n        Uses the required class variables `provider` and `integration_key` with\n        RuleBase.get_option to get the integration object from DB.\n        \"\"\"\n        for integration in integration_service.get_integrations(\n            organization_id=self.project.organization_id,\n            status=OrganizationStatus.ACTIVE,\n            org_integration_status=OrganizationStatus.ACTIVE,\n            providers=[self.provider],\n        ):\n            if integration.id == self.get_integration_id():\n                return integration\n        return None\n\n    def get_organization_integration(self) -> RpcOrganizationIntegration | None:\n        return integration_service.get_organization_integration(\n            integration_id=self.get_integration_id(), organization_id=self.project.organization_id\n        )\n\n    def get_form_instance(self) -> forms.Form:\n        return self.form_cls(self.data, integrations=self.get_integrations())\n", "path": "src/sentry/rules/actions/integrations/base.py"}, {"content": "from sentry import analytics\n\n\nclass DiscordIntegrationNotificationSent(analytics.Event):\n    type = \"integrations.discord.notification_sent\"\n\n    attributes = (\n        analytics.Attribute(\"organization_id\"),\n        analytics.Attribute(\"project_id\"),\n        analytics.Attribute(\"group_id\"),\n        analytics.Attribute(\"notification_uuid\"),\n        analytics.Attribute(\"alert_id\", required=False),\n    )\n\n\nclass DiscordIntegrationCommandInteractionReceived(analytics.Event):\n    type = \"integrations.discord.command_interaction\"\n\n    attributes = (analytics.Attribute(\"command_name\"),)\n\n\nclass DiscordIntegrationIdentityLinked(analytics.Event):\n    type = \"integrations.discord.identity_linked\"\n\n    attributes = (\n        analytics.Attribute(\"provider\"),\n        analytics.Attribute(\"actor_id\"),\n        analytics.Attribute(\"actor_type\"),\n    )\n\n\nclass DiscordIntegrationIdentityUnlinked(analytics.Event):\n    type = \"integrations.discord.identity_unlinked\"\n\n    attributes = (\n        analytics.Attribute(\"provider\"),\n        analytics.Attribute(\"actor_id\"),\n        analytics.Attribute(\"actor_type\"),\n    )\n\n\nclass DiscordIntegrationMessageInteractionReceived(analytics.Event):\n    type = \"integrations.discord.message_interaction\"\n\n    attributes = (analytics.Attribute(\"custom_id\"),)\n\n\nclass DiscordIntegrationAssign(analytics.Event):\n    type = \"integrations.discord.assign\"\n\n    attributes = (analytics.Attribute(\"actor_id\"),)\n\n\nclass DiscordIntegrationStatus(analytics.Event):\n    type = \"integrations.discord.status\"\n\n    attributes = (\n        analytics.Attribute(\"organization_id\"),\n        analytics.Attribute(\"user_id\"),\n        analytics.Attribute(\"status\"),\n    )\n\n\nanalytics.register(DiscordIntegrationNotificationSent)\nanalytics.register(DiscordIntegrationCommandInteractionReceived)\nanalytics.register(DiscordIntegrationIdentityLinked)\nanalytics.register(DiscordIntegrationIdentityUnlinked)\nanalytics.register(DiscordIntegrationMessageInteractionReceived)\nanalytics.register(DiscordIntegrationAssign)\nanalytics.register(DiscordIntegrationStatus)\n", "path": "src/sentry/integrations/discord/analytics.py"}, {"content": "from typing import Any, Generator, Optional, Sequence\n\nfrom sentry import analytics, features\nfrom sentry.eventstore.models import GroupEvent\nfrom sentry.integrations.discord.actions.form import DiscordNotifyServiceForm\nfrom sentry.integrations.discord.client import DiscordClient\nfrom sentry.integrations.discord.message_builder.issues import DiscordIssuesMessageBuilder\nfrom sentry.rules.actions import IntegrationEventAction\nfrom sentry.rules.base import CallbackFuture, EventState\nfrom sentry.shared_integrations.exceptions.base import ApiError\nfrom sentry.types.rules import RuleFuture\nfrom sentry.utils import metrics\n\n\nclass DiscordNotifyServiceAction(IntegrationEventAction):\n    id = \"sentry.integrations.discord.notify_action.DiscordNotifyServiceAction\"\n    form_cls = DiscordNotifyServiceForm\n    label = \"Send a notification to the {server} Discord server in the channel with ID: {channel_id} and show tags {tags} in the notification.\"\n    prompt = \"Send a Discord notification\"\n    provider = \"discord\"\n    integration_key = \"server\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.form_fields = {\n            \"server\": {\n                \"type\": \"choice\",\n                \"choices\": [(i.id, i.name) for i in self.get_integrations()],\n            },\n            \"channel_id\": {\"type\": \"string\", \"placeholder\": \"e.g., 1134274732116676679\"},\n            \"tags\": {\"type\": \"string\", \"placeholder\": \"e.g., environment,user,my_tag\"},\n        }\n\n    def after(\n        self, event: GroupEvent, state: EventState, notification_uuid: Optional[str] = None\n    ) -> Generator[CallbackFuture, None, None]:\n        channel_id = self.get_option(\"channel_id\")\n        tags = set(self.get_tags_list())\n\n        integration = self.get_integration()\n        if not integration:\n            # Integration removed, but rule still active\n            return\n\n        def send_notification(event: GroupEvent, futures: Sequence[RuleFuture]) -> None:\n            if not features.has(\n                \"organizations:integrations-discord-notifications\", event.organization\n            ):\n                return\n\n            rules = [f.rule for f in futures]\n            message = DiscordIssuesMessageBuilder(event.group, event=event, tags=tags, rules=rules)\n\n            client = DiscordClient(integration_id=integration.id)\n            try:\n                client.send_message(channel_id, message, notification_uuid=notification_uuid)\n            except ApiError as e:\n                self.logger.error(\n                    \"rule.fail.discord_post\",\n                    extra={\n                        \"error\": str(e),\n                        \"project_id\": event.project_id,\n                        \"event_id\": event.event_id,\n                        \"guild_id\": integration.external_id,\n                        \"channel_id\": channel_id,\n                    },\n                )\n\n        key = f\"discord:{integration.id}:{channel_id}\"\n\n        analytics.record(\n            \"integrations.discord.notification_sent\",\n            organization_id=event.organization.id,\n            project_id=event.project_id,\n            group_id=event.group_id,\n            notification_uuid=notification_uuid if notification_uuid else \"\",\n        )\n        metrics.incr(\"notifications.sent\", instance=\"discord.notifications\", skip_internal=False)\n        yield self.future(send_notification, key=key)\n\n    def render_label(self) -> str:\n        tags = self.get_tags_list()\n\n        return self.label.format(\n            server=self.get_integration_name(),\n            channel_id=self.get_option(\"channel_id\"),\n            tags=\"[{}]\".format(\", \".join(tags)),\n        )\n\n    def get_tags_list(self) -> Sequence[str]:\n        return [s.strip() for s in self.get_option(\"tags\", \"\").split(\",\")]\n\n    def get_form_instance(self) -> Any:\n        return self.form_cls(self.data, integrations=self.get_integrations())\n", "path": "src/sentry/integrations/discord/actions/notification.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport abc\nfrom typing import List\n\nfrom django import forms\n\nfrom sentry import analytics\nfrom sentry.eventstore.models import GroupEvent\nfrom sentry.models import OrganizationStatus, Rule\nfrom sentry.rules.actions import EventAction\nfrom sentry.services.hybrid_cloud.integration import (\n    RpcIntegration,\n    RpcOrganizationIntegration,\n    integration_service,\n)\n\nINTEGRATION_KEY = \"integration\"\n\n\nclass IntegrationEventAction(EventAction, abc.ABC):\n    \"\"\"Intermediate abstract class to help DRY some event actions code.\"\"\"\n\n    @property\n    @abc.abstractmethod\n    def prompt(self) -> str:\n        pass\n\n    @property\n    @abc.abstractmethod\n    def provider(self) -> str:\n        pass\n\n    @property\n    @abc.abstractmethod\n    def integration_key(self) -> str:\n        pass\n\n    def is_enabled(self) -> bool:\n        enabled: bool = bool(self.get_integrations())\n        return enabled\n\n    def get_integration_name(self) -> str:\n        \"\"\"Get the integration's name for the label.\"\"\"\n        integration = self.get_integration()\n        if not integration:\n            return \"[removed]\"\n\n        _name: str = integration.name\n        return _name\n\n    def get_integrations(self) -> List[RpcIntegration]:\n        return integration_service.get_integrations(\n            organization_id=self.project.organization_id,\n            status=OrganizationStatus.ACTIVE,\n            org_integration_status=OrganizationStatus.ACTIVE,\n            providers=[self.provider],\n        )\n\n    def get_integration_id(self) -> int:\n        integration_id: str | None = self.get_option(self.integration_key)\n        if integration_id:\n            return int(integration_id)\n        return 0\n\n    def get_integration(self) -> RpcIntegration | None:\n        \"\"\"\n        Uses the required class variables `provider` and `integration_key` with\n        RuleBase.get_option to get the integration object from DB.\n        \"\"\"\n        for integration in integration_service.get_integrations(\n            organization_id=self.project.organization_id,\n            status=OrganizationStatus.ACTIVE,\n            org_integration_status=OrganizationStatus.ACTIVE,\n            providers=[self.provider],\n        ):\n            if integration.id == self.get_integration_id():\n                return integration\n        return None\n\n    def get_organization_integration(self) -> RpcOrganizationIntegration | None:\n        return integration_service.get_organization_integration(\n            integration_id=self.get_integration_id(), organization_id=self.project.organization_id\n        )\n\n    def get_form_instance(self) -> forms.Form:\n        return self.form_cls(self.data, integrations=self.get_integrations())\n\n    def record_notification_sent(\n        self,\n        event: GroupEvent,\n        external_id: str,\n        rule: Rule | None = None,\n        notification_uuid: str | None = None,\n    ) -> None:\n        # Currently these actions can only be triggered by issue alerts\n        analytics.record(\n            f\"integrations.{self.provider}.notification_sent\",\n            category=\"issue_alert\",\n            organization_id=event.organization.id,\n            project_id=event.project_id,\n            group_id=event.group_id,\n            notification_uuid=notification_uuid if notification_uuid else \"\",\n            alert_id=rule.id if rule else None,\n        )\n        analytics.record(\n            \"alert.sent\",\n            provider=self.provider,\n            alert_id=rule.id if rule else \"\",\n            alert_type=\"issue_alert\",\n            organization_id=event.organization.id,\n            project_id=event.project_id,\n            external_id=external_id,\n            notification_uuid=notification_uuid if notification_uuid else \"\",\n        )\n", "path": "src/sentry/rules/actions/integrations/base.py"}, {"content": "from sentry import analytics\n\n\nclass DiscordIntegrationNotificationSent(analytics.Event):\n    type = \"integrations.discord.notification_sent\"\n\n    attributes = (\n        analytics.Attribute(\"organization_id\"),\n        analytics.Attribute(\"project_id\"),\n        analytics.Attribute(\"category\"),\n        analytics.Attribute(\"group_id\"),\n        analytics.Attribute(\"notification_uuid\"),\n        analytics.Attribute(\"alert_id\", required=False),\n    )\n\n\nclass DiscordIntegrationCommandInteractionReceived(analytics.Event):\n    type = \"integrations.discord.command_interaction\"\n\n    attributes = (analytics.Attribute(\"command_name\"),)\n\n\nclass DiscordIntegrationIdentityLinked(analytics.Event):\n    type = \"integrations.discord.identity_linked\"\n\n    attributes = (\n        analytics.Attribute(\"provider\"),\n        analytics.Attribute(\"actor_id\"),\n        analytics.Attribute(\"actor_type\"),\n    )\n\n\nclass DiscordIntegrationIdentityUnlinked(analytics.Event):\n    type = \"integrations.discord.identity_unlinked\"\n\n    attributes = (\n        analytics.Attribute(\"provider\"),\n        analytics.Attribute(\"actor_id\"),\n        analytics.Attribute(\"actor_type\"),\n    )\n\n\nclass DiscordIntegrationMessageInteractionReceived(analytics.Event):\n    type = \"integrations.discord.message_interaction\"\n\n    attributes = (analytics.Attribute(\"custom_id\"),)\n\n\nclass DiscordIntegrationAssign(analytics.Event):\n    type = \"integrations.discord.assign\"\n\n    attributes = (analytics.Attribute(\"actor_id\"),)\n\n\nclass DiscordIntegrationStatus(analytics.Event):\n    type = \"integrations.discord.status\"\n\n    attributes = (\n        analytics.Attribute(\"organization_id\"),\n        analytics.Attribute(\"user_id\"),\n        analytics.Attribute(\"status\"),\n    )\n\n\nanalytics.register(DiscordIntegrationNotificationSent)\nanalytics.register(DiscordIntegrationCommandInteractionReceived)\nanalytics.register(DiscordIntegrationIdentityLinked)\nanalytics.register(DiscordIntegrationIdentityUnlinked)\nanalytics.register(DiscordIntegrationMessageInteractionReceived)\nanalytics.register(DiscordIntegrationAssign)\nanalytics.register(DiscordIntegrationStatus)\n", "path": "src/sentry/integrations/discord/analytics.py"}, {"content": "from typing import Any, Generator, Optional, Sequence\n\nfrom sentry import features\nfrom sentry.eventstore.models import GroupEvent\nfrom sentry.integrations.discord.actions.form import DiscordNotifyServiceForm\nfrom sentry.integrations.discord.client import DiscordClient\nfrom sentry.integrations.discord.message_builder.issues import DiscordIssuesMessageBuilder\nfrom sentry.rules.actions import IntegrationEventAction\nfrom sentry.rules.base import CallbackFuture, EventState\nfrom sentry.shared_integrations.exceptions.base import ApiError\nfrom sentry.types.rules import RuleFuture\nfrom sentry.utils import metrics\n\n\nclass DiscordNotifyServiceAction(IntegrationEventAction):\n    id = \"sentry.integrations.discord.notify_action.DiscordNotifyServiceAction\"\n    form_cls = DiscordNotifyServiceForm\n    label = \"Send a notification to the {server} Discord server in the channel with ID: {channel_id} and show tags {tags} in the notification.\"\n    prompt = \"Send a Discord notification\"\n    provider = \"discord\"\n    integration_key = \"server\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.form_fields = {\n            \"server\": {\n                \"type\": \"choice\",\n                \"choices\": [(i.id, i.name) for i in self.get_integrations()],\n            },\n            \"channel_id\": {\"type\": \"string\", \"placeholder\": \"e.g., 1134274732116676679\"},\n            \"tags\": {\"type\": \"string\", \"placeholder\": \"e.g., environment,user,my_tag\"},\n        }\n\n    def after(\n        self, event: GroupEvent, state: EventState, notification_uuid: Optional[str] = None\n    ) -> Generator[CallbackFuture, None, None]:\n        channel_id = self.get_option(\"channel_id\")\n        tags = set(self.get_tags_list())\n\n        integration = self.get_integration()\n        if not integration:\n            # Integration removed, but rule still active\n            return\n\n        def send_notification(event: GroupEvent, futures: Sequence[RuleFuture]) -> None:\n            if not features.has(\n                \"organizations:integrations-discord-notifications\", event.organization\n            ):\n                return\n\n            rules = [f.rule for f in futures]\n            message = DiscordIssuesMessageBuilder(event.group, event=event, tags=tags, rules=rules)\n\n            client = DiscordClient(integration_id=integration.id)\n            try:\n                client.send_message(channel_id, message, notification_uuid=notification_uuid)\n            except ApiError as e:\n                self.logger.error(\n                    \"rule.fail.discord_post\",\n                    extra={\n                        \"error\": str(e),\n                        \"project_id\": event.project_id,\n                        \"event_id\": event.event_id,\n                        \"guild_id\": integration.external_id,\n                        \"channel_id\": channel_id,\n                    },\n                )\n            rule = rules[0] if rules else None\n            self.record_notification_sent(event, channel_id, rule, notification_uuid)\n\n        key = f\"discord:{integration.id}:{channel_id}\"\n\n        metrics.incr(\"notifications.sent\", instance=\"discord.notifications\", skip_internal=False)\n        yield self.future(send_notification, key=key)\n\n    def render_label(self) -> str:\n        tags = self.get_tags_list()\n\n        return self.label.format(\n            server=self.get_integration_name(),\n            channel_id=self.get_option(\"channel_id\"),\n            tags=\"[{}]\".format(\", \".join(tags)),\n        )\n\n    def get_tags_list(self) -> Sequence[str]:\n        return [s.strip() for s in self.get_option(\"tags\", \"\").split(\",\")]\n\n    def get_form_instance(self) -> Any:\n        return self.form_cls(self.data, integrations=self.get_integrations())\n", "path": "src/sentry/integrations/discord/actions/notification.py"}]}