Repository: getsentry/sentry

Issue:
Publish `ProjectRuleDetailsEndpoint`
We need to publish the GET, PUT, and DELETE endpoints at `src/sentry/api/endpoints/project_rule_details.py` to allow users to fetch, update, and delete rules for a project given a specific rule ID.

Blocked by #56674 


Discussion:

Resolving patch:
diff --git a/src/sentry/api/endpoints/project_rule_details.py b/src/sentry/api/endpoints/project_rule_details.py
--- a/src/sentry/api/endpoints/project_rule_details.py
+++ b/src/sentry/api/endpoints/project_rule_details.py
@@ -2,7 +2,8 @@
 
 import logging
 
-from rest_framework import status
+from drf_spectacular.utils import extend_schema
+from rest_framework import serializers, status
 from rest_framework.request import Request
 from rest_framework.response import Response
 
@@ -11,9 +12,19 @@
 from sentry.api.base import region_silo_endpoint
 from sentry.api.bases.rule import RuleEndpoint
 from sentry.api.endpoints.project_rules import find_duplicate_rule
+from sentry.api.fields.actor import ActorField
 from sentry.api.serializers import serialize
 from sentry.api.serializers.models.rule import RuleSerializer
+from sentry.api.serializers.rest_framework.rule import RuleNodeField
 from sentry.api.serializers.rest_framework.rule import RuleSerializer as DrfRuleSerializer
+from sentry.apidocs.constants import (
+    RESPONSE_ACCEPTED,
+    RESPONSE_FORBIDDEN,
+    RESPONSE_NOT_FOUND,
+    RESPONSE_UNAUTHORIZED,
+)
+from sentry.apidocs.examples.issue_alert_examples import IssueAlertExamples
+from sentry.apidocs.parameters import GlobalParams, IssueAlertParams
 from sentry.constants import ObjectStatus
 from sentry.integrations.slack.utils import RedisRuleStatus
 from sentry.mediators import project_rules
@@ -38,23 +49,88 @@
 logger = logging.getLogger(__name__)
 
 
+class ProjectRuleDetailsPutSerializer(serializers.Serializer):
+    name = serializers.CharField(max_length=64, required=False, help_text="The name for the rule.")
+    environment = serializers.CharField(
+        required=False, allow_null=True, help_text="The name of the environment to filter by."
+    )
+    actionMatch = serializers.ChoiceField(
+        choices=(
+            ("all", "All conditions must evaluate to true."),
+            ("any", "At least one of the conditions must evaluate to true."),
+            ("none", "All conditions must evaluate to false."),
+        ),
+        required=False,
+        help_text="A string determining which of the conditions need to be true before any filters are evaluated.",
+    )
+    conditions = serializers.ListField(
+        child=RuleNodeField(type="condition/event"),
+        required=False,
+        help_text="A list of triggers that determine when the rule fires. See [Create an Issue Alert Rule](/api/events/create-an-issue-alert-rule-for-a-project) for valid conditions.",
+    )
+    filterMatch = serializers.ChoiceField(
+        choices=(
+            ("all", "All filters must evaluate to true."),
+            ("any", "At least one of the filters must evaluate to true."),
+            ("none", "All filters must evaluate to false."),
+        ),
+        required=False,
+        help_text="A string determining which filters need to be true before any actions take place.",
+    )
+    filters = serializers.ListField(
+        child=RuleNodeField(type="filter/event"),
+        required=False,
+        help_text="A list of filters that determine if a rule fires after the necessary conditions have been met. See [Create an Issue Alert Rule](/api/events/create-an-issue-alert-rule-for-a-project) for valid filters.",
+    )
+    actions = serializers.ListField(
+        child=RuleNodeField(type="action/event"),
+        required=False,
+        help_text="A list of actions that take place when all required conditions and filters for the rule are met. See [Create an Issue Alert Rule](/api/events/create-an-issue-alert-rule-for-a-project) for valid actions.",
+    )
+    frequency = serializers.IntegerField(
+        min_value=5,
+        max_value=60 * 24 * 30,
+        required=False,
+        help_text="How often to perform the actions once for an issue, in minutes. The valid range is `5` to `43200`.",
+    )
+    owner = ActorField(
+        required=False, allow_null=True, help_text="The ID of the team or user that owns the rule."
+    )
+
+
+@extend_schema(tags=["Events"])
 @region_silo_endpoint
 class ProjectRuleDetailsEndpoint(RuleEndpoint):
     publish_status = {
-        "DELETE": ApiPublishStatus.UNKNOWN,
-        "GET": ApiPublishStatus.UNKNOWN,
-        "PUT": ApiPublishStatus.UNKNOWN,
+        "DELETE": ApiPublishStatus.PUBLIC,
+        "GET": ApiPublishStatus.PUBLIC,
+        "PUT": ApiPublishStatus.PUBLIC,
     }
 
+    @extend_schema(
+        operation_id="Retrieve an Issue Alert Rule for a Project",
+        parameters=[
+            GlobalParams.ORG_SLUG,
+            GlobalParams.PROJECT_SLUG,
+            IssueAlertParams.ISSUE_RULE_ID,
+        ],
+        responses={
+            200: RuleSerializer,
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=IssueAlertExamples.GET_PROJECT_RULE,
+    )
     @transaction_start("ProjectRuleDetailsEndpoint")
     def get(self, request: Request, project, rule) -> Response:
         """
-        Retrieve a rule
-
-        Return details on an individual rule.
-
-            {method} {path}
+        Return details on an individual issue alert rule.
 
+        An issue alert rule triggers whenever a new event is received for any issue in a project that matches the specified alert conditions. These conditions can include a resolved issue re-appearing or an issue affecting many users. Alert conditions have three parts:
+        - Triggers - specify what type of activity you'd like monitored or when an alert should be triggered.
+        - Filters - help control noise by triggering an alert only if the issue matches the specified criteria.
+        - Actions - specify what should happen when the trigger conditions are met and the filters match.
         """
         # Serialize Rule object
         serialized_rule = serialize(
@@ -101,23 +177,31 @@ def get(self, request: Request, project, rule) -> Response:
 
         return Response(serialized_rule)
 
+    @extend_schema(
+        operation_id="Update an Issue Alert Rule",
+        parameters=[
+            GlobalParams.ORG_SLUG,
+            GlobalParams.PROJECT_SLUG,
+            IssueAlertParams.ISSUE_RULE_ID,
+        ],
+        request=ProjectRuleDetailsPutSerializer,
+        responses={
+            200: RuleSerializer,
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=IssueAlertExamples.UPDATE_PROJECT_RULE,
+    )
     @transaction_start("ProjectRuleDetailsEndpoint")
     def put(self, request: Request, project, rule) -> Response:
         """
-        Update a rule
-
-        Update various attributes for the given rule.
-
-            {method} {path}
-            {{
-              "name": "My rule name",
-              "conditions": [],
-              "filters": [],
-              "actions": [],
-              "actionMatch": "all",
-              "filterMatch": "all"
-            }}
+        Updates an issue alert rule. Only the attributes submitted are modified.
 
+        An issue alert rule triggers whenever a new event is received for any issue in a project that matches the specified alert conditions. These conditions can include a resolved issue re-appearing or an issue affecting many users. Alert conditions have three parts:
+        - Triggers - specify what type of activity you'd like monitored or when an alert should be triggered.
+        - Filters - help control noise by triggering an alert only if the issue matches the specified criteria.
+        - Actions - specify what should happen when the trigger conditions are met and the filters match.
         """
         serializer = DrfRuleSerializer(
             context={"project": project, "organization": project.organization},
@@ -254,10 +338,29 @@ def put(self, request: Request, project, rule) -> Response:
 
         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
+    @extend_schema(
+        operation_id="Delete an Issue Alert Rule",
+        parameters=[
+            GlobalParams.ORG_SLUG,
+            GlobalParams.PROJECT_SLUG,
+            IssueAlertParams.ISSUE_RULE_ID,
+        ],
+        responses={
+            202: RESPONSE_ACCEPTED,
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+    )
     @transaction_start("ProjectRuleDetailsEndpoint")
     def delete(self, request: Request, project, rule) -> Response:
         """
-        Delete a rule
+        Delete a specific issue alert rule.
+
+        An issue alert rule triggers whenever a new event is received for any issue in a project that matches the specified alert conditions. These conditions can include a resolved issue re-appearing or an issue affecting many users. Alert conditions have three parts:
+        - Triggers: specify what type of activity you'd like monitored or when an alert should be triggered.
+        - Filters: help control noise by triggering an alert only if the issue matches the specified criteria.
+        - Actions: specify what should happen when the trigger conditions are met and the filters match.
         """
         rule.update(status=ObjectStatus.PENDING_DELETION)
         RuleActivity.objects.create(
diff --git a/src/sentry/api/endpoints/project_rules.py b/src/sentry/api/endpoints/project_rules.py
--- a/src/sentry/api/endpoints/project_rules.py
+++ b/src/sentry/api/endpoints/project_rules.py
@@ -108,6 +108,7 @@ def find_duplicate_rule(project, rule_data=None, rule_id=None, rule=None):
 
 
 class ProjectRulesPostSerializer(serializers.Serializer):
+    name = serializers.CharField(max_length=64, help_text="The name for the rule.")
     actionMatch = serializers.ChoiceField(
         choices=(
             ("all", "All conditions must evaluate to true."),
@@ -116,6 +117,59 @@ class ProjectRulesPostSerializer(serializers.Serializer):
         ),
         help_text="A string determining which of the conditions need to be true before any filters are evaluated.",
     )
+    conditions = serializers.ListField(
+        child=RuleNodeField(type="condition/event"),
+        help_text="""
+A list of triggers that determine when the rule fires. See below for a list of possible conditions.
+
+**A new issue is created**
+```json
+{
+    "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"
+}
+```
+
+**The issue changes state from resolved to unresolved**
+```json
+{
+    "id": "sentry.rules.conditions.regression_event.RegressionEventCondition"
+}
+```
+
+**The issue is seen more than `value` times in `interval`**
+- `value` - An integer
+- `interval` - Valid values are `1m`, `5m`, `15m`, `1h`, `1d`, `1w` and `30d` (`m` for minutes, `h` for hours, `d` for days, and `w` for weeks).
+```json
+{
+    "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition",
+    "value": 500,
+    "interval": "1h"
+}
+```
+
+**The issue is seen by more than `value` users in `interval`**
+- `value` - An integer
+- `interval` - Valid values are `1m`, `5m`, `15m`, `1h`, `1d`, `1w` and `30d` (`m` for minutes, `h` for hours, `d` for days, and `w` for weeks).
+```json
+{
+    "id": "sentry.rules.conditions.event_frequency.EventUniqueUserFrequencyCondition",
+    "value": 1000,
+    "interval": "15m"
+}
+```
+
+**The issue affects more than `value` percent of sessions in `interval`**
+- `value` - An integer from 0 to 100
+- `interval` - Valid values are `5m`, `10m`, `30m`, and `1h` (`m` for minutes, `h` for hours).
+```json
+{
+    "id": "sentry.rules.conditions.event_frequency.EventFrequencyPercentCondition",
+    "value": 50,
+    "interval": "10m"
+}
+```
+""",
+    )
     actions = serializers.ListField(
         child=RuleNodeField(type="action/event"),
         help_text="""
@@ -262,67 +316,13 @@ class ProjectRulesPostSerializer(serializers.Serializer):
     "issueType": "1"
 }
 ```
-""",
-    )
-    conditions = serializers.ListField(
-        child=RuleNodeField(type="condition/event"),
-        help_text="""
-A list of triggers that determine when the rule fires. See below for a list of possible conditions.
-
-**A new issue is created**
-```json
-{
-    "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"
-}
-```
-
-**The issue changes state from resolved to unresolved**
-```json
-{
-    "id": "sentry.rules.conditions.regression_event.RegressionEventCondition"
-}
-```
-
-**The issue is seen more than `value` times in `interval`**
-- `value` - An integer
-- `interval` - Valid values are `1m`, `5m`, `15m`, `1h`, `1d`, `1w` and `30d` (`m` for minutes, `h` for hours, `d` for days, and `w` for weeks).
-```json
-{
-    "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition",
-    "value": 500,
-    "interval": "1h"
-}
-```
-
-**The issue is seen by more than `value` users in `interval`**
-- `value` - An integer
-- `interval` - Valid values are `1m`, `5m`, `15m`, `1h`, `1d`, `1w` and `30d` (`m` for minutes, `h` for hours, `d` for days, and `w` for weeks).
-```json
-{
-    "id": "sentry.rules.conditions.event_frequency.EventUniqueUserFrequencyCondition",
-    "value": 1000,
-    "interval": "15m"
-}
-```
-
-**The issue affects more than `value` percent of sessions in `interval`**
-- `value` - An integer from 0 to 100
-- `interval` - Valid values are `5m`, `10m`, `30m`, and `1h` (`m` for minutes, `h` for hours).
-```json
-{
-    "id": "sentry.rules.conditions.event_frequency.EventFrequencyPercentCondition",
-    "value": 50,
-    "interval": "10m"
-}
-```
 """,
     )
     frequency = serializers.IntegerField(
         min_value=5,
         max_value=60 * 24 * 30,
-        help_text="How often to perform the actions once for an issue, in minutes.",
+        help_text="How often to perform the actions once for an issue, in minutes. The valid range is `5` to `43200`.",
     )
-    name = serializers.CharField(max_length=64, help_text="The name for the rule.")
     environment = serializers.CharField(
         required=False, allow_null=True, help_text="The name of the environment to filter by."
     )
diff --git a/src/sentry/apidocs/examples/issue_alert_examples.py b/src/sentry/apidocs/examples/issue_alert_examples.py
--- a/src/sentry/apidocs/examples/issue_alert_examples.py
+++ b/src/sentry/apidocs/examples/issue_alert_examples.py
@@ -11,6 +11,101 @@ class IssueAlertExamples:
         )
     ]
 
+    GET_PROJECT_RULE = [
+        OpenApiExample(
+            "Get detailed view about an issue alert rule",
+            value={
+                "id": "7",
+                "conditions": [
+                    {
+                        "id": "sentry.rules.conditions.regression_event.RegressionEventCondition",
+                    }
+                ],
+                "filters": [
+                    {
+                        "id": "sentry.rules.filters.age_comparison.AgeComparisonFilter",
+                        "comparison_type": "older",
+                        "value": 4,
+                        "time": "week",
+                    },
+                    {
+                        "id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter",
+                        "value": 1000,
+                    },
+                ],
+                "actions": [
+                    {
+                        "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
+                        "workspace": 976462356,
+                        "channel": "#fatal",
+                        "tags": "browser,release",
+                    }
+                ],
+                "actionMatch": "all",
+                "filterMatch": "all",
+                "frequency": 60,
+                "name": "Many Old Regressions!",
+                "dateCreated": "2023-02-17T18:31:14.246012Z",
+                "owner": "user:635623",
+                "createdBy": {"id": 635623, "name": "John Doe", "email": "john.doe@email.com"},
+                "environment": None,
+                "projects": ["javascript"],
+                "status": "active",
+                "snooze": False,
+            },
+            status_codes=["200"],
+            response_only=True,
+        )
+    ]
+
+    UPDATE_PROJECT_RULE = [
+        OpenApiExample(
+            "Get detailed view about an issue alert rule",
+            value={
+                "id": "7",
+                "conditions": [
+                    {
+                        "id": "sentry.rules.conditions.regression_event.RegressionEventCondition",
+                    }
+                ],
+                "filters": [
+                    {
+                        "id": "sentry.rules.filters.age_comparison.AgeComparisonFilter",
+                        "comparison_type": "older",
+                        "value": 4,
+                        "time": "week",
+                    },
+                    {
+                        "id": "sentry.rules.filters.issue_occurrences.IssueOccurrencesFilter",
+                        "value": 1000,
+                    },
+                    {"id": "sentry.rules.filters.level.LevelFilter", "match": "gte", "level": "40"},
+                ],
+                "actions": [
+                    {
+                        "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
+                        "workspace": 976462356,
+                        "channel": "#fatal",
+                        "tags": "browser,release",
+                    }
+                ],
+                "actionMatch": "all",
+                "filterMatch": "all",
+                "frequency": 60,
+                "name": "Many Old Regressions!",
+                "dateCreated": "2023-02-17T18:31:14.246012Z",
+                "owner": "user:635623",
+                "createdBy": {"id": 635623, "name": "John Doe", "email": "john.doe@email.com"},
+                "environment": None,
+                "projects": ["javascript"],
+                "status": "active",
+                "snooze": False,
+            },
+            status_codes=["200"],
+            response_only=True,
+        )
+    ]
+
     LIST_PROJECT_RULES = [
         OpenApiExample(
             "List issue alert rules for a project",


Test patch: