Repository: getsentry/sentry
Pull request: feat(api): Publish `OrganizationAlertRuleIndexEndpoint`
This PR publishes the `GET` and `POST` endpoints.

Closes #56676 

~~❗ Blocked by #55956~~
~~❗ Blocked by https://github.com/getsentry/sentry-docs/pull/8027~~
~~❗ Blocked by https://github.com/getsentry/sentry-docs/pull/8127~~

# List Metric Alert Rules
![localhost_3000_api_alerts_create-a-metric-alert-rule-for-an-organization_ (1)](https://github.com/getsentry/sentry/assets/45607721/1c0877bb-d141-4f87-9d7c-e1ced65041eb)


# Create a Metric Alert Rule
![localhost_3000_api_alerts_create-a-metric-alert-rule-for-an-organization_](https://github.com/getsentry/sentry/assets/45607721/4490f9a9-a53c-41ba-9e73-739f3c1e9ccb)





Problem statement:
Publish `OrganizationAlertRuleIndexEndpoint`
We need to publish the GET and POST endpoints at `src/sentry/incidents/endpoints/organization_alert_rule_index.py` to allow users to get a list of metric alert rules for an organization, as well as create them via API request.

❗ Currently on hold until Metric Alert Rule types are documented.

Hints from issue discussion:

Commit message: pr feedback changes + change api category

Patch to review:
diff --git a/src/sentry/api/serializers/models/alert_rule.py b/src/sentry/api/serializers/models/alert_rule.py
index 617b4a506d82b3..fdd41c5b83e3e9 100644
--- a/src/sentry/api/serializers/models/alert_rule.py
+++ b/src/sentry/api/serializers/models/alert_rule.py
@@ -1,7 +1,8 @@
 from collections import defaultdict
-from typing import MutableMapping
+from typing import List, MutableMapping, Optional
 
 from django.db.models import Max, Q, prefetch_related_objects
+from typing_extensions import TypedDict
 
 from sentry.api.serializers import Serializer, register, serialize
 from sentry.api.serializers.models.rule import RuleSerializer
@@ -24,6 +25,44 @@
 from sentry.snuba.models import SnubaQueryEventType
 
 
+class AlertRuleSerializerResponseOptional(TypedDict, total=False):
+    environment: Optional[str]
+    projects: Optional[List[str]]
+    excludedProjects: Optional[List[dict]]
+    queryType: Optional[int]
+    resolveThreshold: Optional[float]
+    dataset: Optional[str]
+    eventTypes: Optional[List[str]]
+    owner: Optional[str]
+    originalAlertRuleId: Optional[str]
+    comparisonDelta: Optional[str]
+    weeklyAvg: Optional[float]
+    totalThisWeek: Optional[int]
+    snooze: Optional[bool]
+
+
+class AlertRuleSerializerResponse(AlertRuleSerializerResponseOptional):
+    """
+    This represents a Sentry Metric Alert Rule.
+    """
+
+    id: str
+    name: str
+    organizationId: str
+    status: int
+    query: str
+    aggregate: str
+    thresholdType: int
+    timeWindow: str
+    resolution: float
+    thresholdPeriod: int
+    triggers: List[dict]
+    includeAllProjects: bool
+    dateModified: str
+    dateCreated: str
+    createdBy: dict
+
+
 @register(AlertRule)
 class AlertRuleSerializer(Serializer):
     def __init__(self, expand=None):
@@ -141,7 +180,7 @@ def get_attrs(self, item_list, user, **kwargs):
 
         return result
 
-    def serialize(self, obj, attrs, user, **kwargs):
+    def serialize(self, obj, attrs, user, **kwargs) -> AlertRuleSerializerResponse:
         from sentry.incidents.endpoints.utils import translate_threshold
         from sentry.incidents.logic import translate_aggregate_field
 
diff --git a/src/sentry/apidocs/api_ownership_stats_dont_modify.json b/src/sentry/apidocs/api_ownership_stats_dont_modify.json
index 320aafbf1ece84..dbbb5e3feb10e3 100644
--- a/src/sentry/apidocs/api_ownership_stats_dont_modify.json
+++ b/src/sentry/apidocs/api_ownership_stats_dont_modify.json
@@ -95,6 +95,8 @@
         "public": [
             "MonitorIngestCheckInDetailsEndpoint::PUT",
             "MonitorIngestCheckInIndexEndpoint::POST",
+            "OrganizationAlertRuleIndexEndpoint::GET",
+            "OrganizationAlertRuleIndexEndpoint::POST",
             "OrganizationMemberDetailsEndpoint::DELETE",
             "OrganizationMemberDetailsEndpoint::GET",
             "OrganizationMemberDetailsEndpoint::PUT",
@@ -238,8 +240,6 @@
             "OrganizationAlertRuleDetailsEndpoint::DELETE",
             "OrganizationAlertRuleDetailsEndpoint::GET",
             "OrganizationAlertRuleDetailsEndpoint::PUT",
-            "OrganizationAlertRuleIndexEndpoint::GET",
-            "OrganizationAlertRuleIndexEndpoint::POST",
             "OrganizationApiKeyDetailsEndpoint::DELETE",
             "OrganizationApiKeyDetailsEndpoint::GET",
             "OrganizationApiKeyDetailsEndpoint::PUT",
diff --git a/src/sentry/apidocs/examples/metric_alert_examples.py b/src/sentry/apidocs/examples/metric_alert_examples.py
new file mode 100644
index 00000000000000..2e1cc95ffa835a
--- /dev/null
+++ b/src/sentry/apidocs/examples/metric_alert_examples.py
@@ -0,0 +1,131 @@
+from drf_spectacular.utils import OpenApiExample
+
+
+class MetricAlertExamples:
+    LIST_METRIC_ALERT_RULES = [
+        OpenApiExample(
+            "List metric alert rules for an organization",
+            value=[
+                {
+                    "id": "7",
+                    "name": "Counting Bad Request and Unauthorized Errors in Prod",
+                    "organizationId": "237655244234",
+                    "status": 0,
+                    "queryType": 0,
+                    "dataset": "events",
+                    "query": "tags[http.status_code]:[400, 401]",
+                    "aggregate": "count()",
+                    "thresholdType": 0,
+                    "resolveThreshold": None,
+                    "timeWindow": "1440",
+                    "environment": "prod",
+                    "resolution": 1.0,
+                    "thresholdPeriod": 1,
+                    "triggers": [
+                        {
+                            "id": "394289",
+                            "alertRuleId": "17723",
+                            "label": "critical",
+                            "thresholdType": 0,
+                            "alertThreshold": 100,
+                            "resolveThreshold": None,
+                            "dateCreated": "2023-09-25T22:15:26.375126Z",
+                            "actions": [
+                                {
+                                    "id": "394280",
+                                    "alertRuleTriggerId": "92481",
+                                    "type": "slack",
+                                    "targetType": "specific",
+                                    "targetIdentifier": "30489048931789",
+                                    "inputChannelId": "#my-channel",
+                                    "integrationId": "8753467",
+                                    "sentryAppId": None,
+                                    "dateCreated": "2023-09-25T22:15:26.375126Z",
+                                }
+                            ],
+                        },
+                    ],
+                    "projects": ["super-cool-project"],
+                    "includeAllProjects": False,
+                    "owner": "user:53256",
+                    "originalAlertRuleId": None,
+                    "comparisonDelta": None,
+                    "dateModified": "2023-09-25T22:15:26.375126Z",
+                    "dateCreated": "2023-09-25T22:15:26.375126Z",
+                    "createdBy": {"id": 983948, "name": "John Doe", "email": "john.doe@sentry.io"},
+                }
+            ],
+            status_codes=["200"],
+            response_only=True,
+        )
+    ]
+
+    CREATE_METRIC_ALERT_RULE = [
+        OpenApiExample(
+            "Create a metric alert rule for an organization",
+            value={
+                "id": "177104",
+                "name": "Apdex % Check",
+                "organizationId": "4505676595200000",
+                "status": 0,
+                "queryType": 2,
+                "dataset": "metrics",
+                "query": "",
+                "aggregate": "percentage(sessions_crashed, sessions) AS _crash_rate_alert_aggregate",
+                "thresholdType": 0,
+                "resolveThreshold": 80.0,
+                "timeWindow": "120",
+                "environment": None,
+                "resolution": 2.0,
+                "thresholdPeriod": 1,
+                "triggers": [
+                    {
+                        "id": "293990",
+                        "alertRuleId": "177104",
+                        "label": "critical",
+                        "thresholdType": 0,
+                        "alertThreshold": 75,
+                        "resolveThreshold": 80.0,
+                        "dateCreated": "2023-09-25T22:01:28.673305Z",
+                        "actions": [
+                            {
+                                "id": "281887",
+                                "alertRuleTriggerId": "293990",
+                                "type": "email",
+                                "targetType": "team",
+                                "targetIdentifier": "2378589792734981",
+                                "inputChannelId": None,
+                                "integrationId": None,
+                                "sentryAppId": None,
+                                "dateCreated": "2023-09-25T22:01:28.680793Z",
+                            }
+                        ],
+                    },
+                    {
+                        "id": "492849",
+                        "alertRuleId": "482923",
+                        "label": "warning",
+                        "thresholdType": 1,
+                        "alertThreshold": 50,
+                        "resolveThreshold": 80,
+                        "dateCreated": "2023-09-25T22:01:28.673305Z",
+                        "actions": [],
+                    },
+                ],
+                "projects": ["our-project"],
+                "includeAllProjects": False,
+                "owner": "team:4505676595200000",
+                "originalAlertRuleId": None,
+                "comparisonDelta": "10080",
+                "dateModified": "2023-09-25T22:01:28.637506Z",
+                "dateCreated": "2023-09-25T22:01:28.637514Z",
+                "createdBy": {
+                    "id": 2837708,
+                    "name": "Jane Doe",
+                    "email": "jane.doe@sentry.io",
+                },
+            },
+            status_codes=["201"],
+            response_only=True,
+        )
+    ]
diff --git a/src/sentry/incidents/endpoints/organization_alert_rule_index.py b/src/sentry/incidents/endpoints/organization_alert_rule_index.py
index f26cdca3e8206c..c4cc6c63951e08 100644
--- a/src/sentry/incidents/endpoints/organization_alert_rule_index.py
+++ b/src/sentry/incidents/endpoints/organization_alert_rule_index.py
@@ -1,10 +1,12 @@
 from copy import deepcopy
 from datetime import datetime
+from typing import List
 
 from django.db.models import DateTimeField, IntegerField, OuterRef, Q, Subquery, Value
 from django.db.models.functions import Coalesce
 from django.utils.timezone import make_aware
-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
 
@@ -13,14 +15,24 @@
 from sentry.api.base import Endpoint, region_silo_endpoint
 from sentry.api.bases.organization import OrganizationAlertRulePermission, OrganizationEndpoint
 from sentry.api.exceptions import ResourceDoesNotExist
+from sentry.api.fields.actor import ActorField
 from sentry.api.paginator import (
     CombinedQuerysetIntermediary,
     CombinedQuerysetPaginator,
     OffsetPaginator,
 )
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.alert_rule import CombinedRuleSerializer
+from sentry.api.serializers.models.alert_rule import AlertRuleSerializer as AlertRuleModelSerializer
+from sentry.api.serializers.models.alert_rule import (
+    AlertRuleSerializerResponse,
+    CombinedRuleSerializer,
+)
+from sentry.api.serializers.rest_framework.project import ProjectField
 from sentry.api.utils import InvalidParams
+from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED
+from sentry.apidocs.examples.metric_alert_examples import MetricAlertExamples
+from sentry.apidocs.parameters import GlobalParams
+from sentry.apidocs.utils import inline_sentry_response_serializer
 from sentry.constants import ObjectStatus
 from sentry.incidents.logic import get_slack_actions_with_async_lookups
 from sentry.incidents.models import AlertRule, Incident
@@ -247,22 +259,333 @@ def get(self, request: Request, organization) -> Response:
         return response
 
 
+class OrganizationAlertRuleIndexPostSerializer(serializers.Serializer):
+    name = serializers.CharField(
+        max_length=64,
+        help_text="The name for the rule, which has a maximimum length of 64 characters.",
+    )
+    aggregate = serializers.CharField(
+        help_text="A string representing the aggregate used in this alert rule. Valid aggregate functions are `count`, `count_unique`, `percentage`, `avg`, `apdex`, `failure_rate`, `p50`, `p75`, `p95`, `p99`, `p100`, and `percentile`. See [Metric Alert Rule Types](#metric-alert-rule-types) for valid configurations."
+    )
+    timeWindow = serializers.ChoiceField(
+        choices=(
+            (1, "1 minute"),
+            (5, "5 minutes"),
+            (10, "10 minutes"),
+            (15, "15 minutes"),
+            (30, "30 minutes"),
+            (60, "1 hour"),
+            (120, "2 hours"),
+            (240, "4 hours"),
+            (1440, "24 hours"),
+        ),
+        help_text="The time period to aggregate over.",
+    )
+    # projects is not required in the serializer, however, the UI requires a project is chosen
+    projects = serializers.ListField(
+        child=ProjectField(scope="project:read"),
+        help_text="The names of the projects to filter by.",
+    )
+    query = serializers.CharField(
+        help_text='An event search query to subscribe to and monitor for alerts. For example, to filter transactions so that only those with status code 400 are included, you could use `"query": "http.status_code:400"`. Use an empty string for no filter.'
+    )
+    thresholdType = serializers.ChoiceField(
+        choices=((0, "Above"), (1, "Below")),
+        help_text='The comparison operator for the critical and warning thresholds. The comparison operator for the resolved threshold is automatically set to the opposite operator. When a percentage change threshold is used, `0` is equivalent to "Higher than" and `1` is equivalent to "Lower than".',
+    )
+    triggers = serializers.ListField(
+        help_text="""
+A list of triggers, where each trigger is an object with the following fields:
+- `label`: One of `critical` or `warning`. A `critical` trigger is always required.
+- `alertThreshold`: The value that the subscription needs to reach to trigger the
+alert rule.
+- `actions`: A list of actions that take place when the threshold is met. Set as an empty list if no actions are to take place.
+```json
+triggers: [
+    {
+        "label": "critical",
+        "alertThreshold": 50,
+        "actions": [
+            {
+                "type": "slack",
+                "targetType": "specific",
+                "targetIdentifier": "#get-crit",
+                "inputChannelId": 2454362
+                "integrationId": 653532,
+            }
+        ]
+    },
+    {
+        "label": "warning",
+        "alertThreshold": 25,
+        "actions": []
+    }
+]
+```
+Metric alert rule trigger actions follow the following structure:
+- `type`: The type of trigger action. Valid values are `email`, `slack`, `msteams`, `pagerduty`, `sentry_app`, `sentry_notification`, and `opsgenie`.
+- `targetType`: The type of target the notification will be sent to. Valid values are `specific`, `user`, `team`, and `sentry_app`.
+- `targetIdentifier`: The ID of the target. This is required as an integer for PagerDuty and Sentry apps, and as a string for all others. Examples of appropriate values include a Slack channel name (`#my-channel`), a user ID, a team ID, a Sentry app ID, etc.
+- `inputChannelId`: The ID of the Slack channel. This is only used for the Slack action, and can be used as an alternative to providing the `targetIdentifier`.
+- `integrationId`: The integration ID. This is required for every action type excluding `email` and `sentry_app.`
+- `sentryAppId`: The ID of the Sentry app. This is required when `type` is `sentry_app`.
+"""
+    )
+    environment = serializers.CharField(
+        required=False,
+        allow_null=True,
+        help_text="The name of the environment to filter by. Defaults to all environments.",
+    )
+    dataset = serializers.CharField(
+        required=False,
+        help_text="The name of the dataset that this query will be executed on. Valid values are `events`, `transactions`, `metrics`, `sessions`, and `generic-metrics`. Defaults to `events`. See [Metric Alert Rule Types](#metric-alert-rule-types) for valid configurations.",
+    )
+    queryType = serializers.ChoiceField(
+        required=False,
+        choices=((0, "event.type:error"), (1, "event.type:transaction"), (2, "")),
+        help_text="The `SnubaQuery.Type` of the query. If no value is provided, `queryType` is set to the default for the specified `dataset.` See [Metric Alert Rule Types](#metric-alert-rule-types) for valid configurations.",
+    )
+    eventTypes = serializers.ListField(
+        child=serializers.CharField(),
+        required=False,
+        help_text="List of event types that this alert will be related to. Valid values are `default`, `error` and `transaction`.",
+    )
+    comparisonDelta = serializers.IntegerField(
+        required=False,
+        help_text='An optional int representing the time delta to use to determine the comparison period, in minutes. Required when using a percentage change threshold ("x%" higher or lower compared to `comparisonDelta` minutes ago). A percentage change threshold cannot be used for [Crash Free Session Rate](#crash-free-session-rate) or [Crash Free User Rate](#crash-free-user-rate).',
+    )
+    resolveThreshold = serializers.FloatField(
+        required=False,
+        help_text="Optional value that the subscription needs to reach to resolve the alert. Set automatically if no value is provided. If `thresholdType` is `0`, `resolveThreshold` must be greater than the critical threshold, otherwise, it must be less than the critical threshold.",
+    )
+    owner = ActorField(
+        required=False, allow_null=True, help_text="The ID of the team or user that owns the rule."
+    )
+
+
+@extend_schema(tags=["Organizations"])
 @region_silo_endpoint
 class OrganizationAlertRuleIndexEndpoint(OrganizationEndpoint, AlertRuleIndexMixin):
     publish_status = {
-        "GET": ApiPublishStatus.UNKNOWN,
-        "POST": ApiPublishStatus.UNKNOWN,
+        "GET": ApiPublishStatus.PUBLIC,
+        "POST": ApiPublishStatus.PUBLIC,
     }
     permission_classes = (OrganizationAlertRulePermission,)
 
+    @extend_schema(
+        operation_id="List an Organization's Metric Alert Rules",
+        parameters=[GlobalParams.ORG_SLUG],
+        request=None,
+        responses={
+            200: inline_sentry_response_serializer(
+                "ListMetricAlertRules", List[AlertRuleSerializerResponse]
+            ),
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=MetricAlertExamples.LIST_METRIC_ALERT_RULES,  # TODO: make
+    )
     def get(self, request: Request, organization) -> Response:
         """
-        Fetches metric alert rules for an organization
+        Return a list of active metric alert rules bound to an organization.
+
+        A metric alert rule is a configuration that defines the conditions for triggering an alert
+        based on specific metrics. It specifies the metric type, function, time interval, and threshold
+        values that determine when an alert should be triggered. Metric alert rules are used to monitor
+        and notify you when certain metrics, such as error count, latency, or failure rate, cross a
+        predefined threshold. These rules help you proactively identify and address issues in your
+        application or system.
         """
         return self.fetch_metric_alert(request, organization)
 
+    @extend_schema(
+        operation_id="Create a Metric Alert Rule for an Organization",
+        parameters=[GlobalParams.ORG_SLUG],
+        request=OrganizationAlertRuleIndexPostSerializer,
+        responses={
+            201: AlertRuleModelSerializer,
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=MetricAlertExamples.CREATE_METRIC_ALERT_RULE,
+    )
     def post(self, request: Request, organization) -> Response:
         """
-        Create a metric alert rule
+        Create a new metric alert rule for the given organization.
+
+        A metric alert rule is a configuration that defines the conditions for triggering an alert
+        based on specific metrics. It specifies the metric type, function, time interval, and threshold
+        values that determine when an alert should be triggered. Metric alert rules are used to monitor
+        and notify you when certain metrics, such as error count, latency, or failure rate, cross a
+        predefined threshold. These rules help you proactively identify and address issues in your
+        application or system.
+
+        ## Metric Alert Rule Types
+        Listed below are the types of metric alert rules you can create with Sentry, and the values they
+        require. All other parameters can be customized depending on how you want the alert rule to work.
+        Scroll down to Body Parameters for more information.
+
+        ### Number of Errors
+        Alert when the number of errors in a project matching your filters crosses a threshold. This is
+        useful for monitoring the overall level or errors in your project or errors occurring in specific
+        parts of your app.
+        - `eventTypes`: Any of `error` or `default`.
+        ```json
+        {
+            "queryType": 0,
+            "dataset": "events",
+            "aggregate": "count()",
+            "eventTypes": ["error", "default"]
+        }
+        ```
+
+        ### Users Experiencing Errors
+        Alert when the number of users affected by errors in your project crosses a threshold.
+        - `eventTypes`: Any of `error` or `default`.
+        ```json
+        {
+            "queryType": 0,
+            "dataset": "events",
+            "aggregate": "count_unique(user)"
+        }
+        ```
+
+        ### Crash Free Session Rate
+        A session begins when a user starts the application and ends when it’s closed or sent to the background.
+        A crash is when a session ends due to an error and this type of alert lets you monitor when those crashed
+        sessions exceed a threshold. This lets you get a better picture of the health of your app.
+        ```json
+        {
+            "queryType": 2,
+            "dataset": "metrics",
+            "aggregate": "percentage(sessions_crashed, sessions) AS _crash_rate_alert_aggregate"
+        }
+        ```
+
+        ### Crash Free User Rate
+        Crash Free Users is the percentage of distinct users that haven’t experienced a crash and so this type of
+        alert tells you when the overall user experience dips below a certain unacceptable threshold.
+        ```json
+        {
+            "queryType": 2,
+            "dataset": "metrics",
+            "aggregate": "percentage(users_crashed, users) AS _crash_rate_alert_aggregate"
+        }
+        ```
+
+        ### Throughput
+        Throughput is the total number of transactions in a project and you can alert when it reaches a threshold
+        within a period of time.
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "transactions",
+            "aggregate": "count()"
+        }
+        ```
+
+        ### Transaction Duration
+        Monitor how long it takes for transactions to complete. Use flexible aggregates like percentiles, averages,
+        and min/max.
+        -  `dataset`: If a custom percentile is used, `dataset` is `transactions`. Otherwise, `dataset` is `generic_metrics`.
+        -  `aggregate`: Valid values are `avg(transaction.duration)`, `p50(transaction.duration)`, `p75(transaction.duration)`, `p95(transaction.duration)`, `p99(transaction.duration)`, `p100(transaction.duration)`, and `percentile(transaction.duration,x)`, where `x` is your custom percentile.
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "generic_metrics",
+            "aggregate": "avg(transaction.duration)"
+        }
+        ```
+
+        ### Apdex
+        Apdex is a metric used to track and measure user satisfaction based on your application response times.
+        The Apdex score provides the ratio of satisfactory, tolerable, and frustrated requests in a specific
+        transaction or endpoint.
+        - `aggregate`: `apdex(x)` where `x` is the value of the Apdex score.
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "transactions",
+            "aggregate": "apdex(300)"
+        }
+        ```
+
+        ### Failure Rate
+        Failure rate is the percentage of unsuccessful transactions. Sentry treats transactions with a
+        status other than “ok,” “canceled,” and “unknown” as failures.
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "transactions",
+            "aggregate": "failure_rate()"
+        }
+        ```
+
+        ### Largest Contentful Paint
+        Largest Contentful Paint (LCP) measures loading performance. It marks the point when the largest
+        image or text block is visible within the viewport. A fast LCP helps reassure the user that the
+        page is useful, and so we recommend an LCP of less than 2.5 seconds.
+        - `dataset`: If a custom percentile is used, `dataset` is `transactions`. Otherwise, `dataset` is `generic_metrics`.
+        - `aggregate`: Valid values are `avg(measurements.lcp)`, `p50(measurements.lcp)`, `p75(measurements.lcp)`, `p95(measurements.lcp)`, `p99(measurements.lcp)`, `p100(measurements.lcp)`, and `percentile(measurements.lcp,x)`, where `x` is your custom percentile.
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "generic_metrics",
+            "aggregate": "p50(measurements.lcp)"
+        }
+        ```
+
+        ### First Input Delay
+        First Input Delay (FID) measures interactivity as the response time when the user tries to
+        interact with the viewport. A low FID helps ensure that a page is useful, and we recommend a FID
+        of less than 100 milliseconds.
+        - `dataset`: If a custom percentile is used, `dataset` is `transactions`. Otherwise, `dataset` is `generic_metrics`.
+        - `aggregate`: Valid values are `avg(measurements.fid)`, `p50(measurements.fid)`, `p75(measurements.fid)`, `p95(measurements.fid)`, `p99(measurements.fid)`, `p100(measurements.fid)`, and `percentile(measurements.fid,x)`, where `x` is your custom percentile.
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "generic_metrics",
+            "aggregate": "p100(measurements.fid)"
+        }
+        ```
+
+        ### Cumulative Layout Shift
+        Cumulative Layout Shift (CLS) measures visual stability by quantifying unexpected layout shifts
+        that occur during the entire lifespan of the page. A CLS of less than 0.1 is a good user experience,
+        while anything greater than 0.25 is poor.
+        - `dataset`: If a custom percentile is used, `dataset` is `transactions`. Otherwise, `dataset` is `generic_metrics`.
+        - `aggregate`: Valid values are `avg(measurements.cls)`, `p50(measurements.cls)`, `p75(measurements.cls)`, `p95(measurements.cls)`, `p99(measurements.cls)`, `p100(measurements.cls)`, and `percentile(measurements.cls,x)`, where `x` is your custom percentile.
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "transactions",
+            "aggregate": "percentile(measurements.cls,0.2)"
+        }
+        ```
+
+        ### Custom Metrics
+        Alert on metrics which are not listed above, such as first paint (FP), first contentful paint (FCP),
+        and time to first byte (TTFB).
+        - `dataset`: If a custom percentile is used, `dataset` is `transactions`. Otherwise, `dataset` is `generic_metrics`.
+        - `aggregate`: Valid values are:
+            - `avg(x)`, where `x` is `transaction.duration`, `measurements.cls`, `measurements.fcp`, `measurements.fid`, `measurements.fp`, `measurements.lcp`, `measurements.ttfb`, or `measurements.ttfb.requesttime`.
+            - `p50(x)`, where `x` is `transaction.duration`, `measurements.cls`, `measurements.fcp`, `measurements.fid`, `measurements.fp`, `measurements.lcp`, `measurements.ttfb`, or `measurements.ttfb.requesttime`.
+            - `p75(x)`, where `x` is `transaction.duration`, `measurements.cls`, `measurements.fcp`, `measurements.fid`, `measurements.fp`, `measurements.lcp`, `measurements.ttfb`, or `measurements.ttfb.requesttime`.
+            - `p95(x)`, where `x` is `transaction.duration`, `measurements.cls`, `measurements.fcp`, `measurements.fid`, `measurements.fp`, `measurements.lcp`, `measurements.ttfb`, or `measurements.ttfb.requesttime`.
+            - `p99(x)`, where `x` is `transaction.duration`, `measurements.cls`, `measurements.fcp`, `measurements.fid`, `measurements.fp`, `measurements.lcp`, `measurements.ttfb`, or `measurements.ttfb.requesttime`.
+            - `p100(x)`, where `x` is `transaction.duration`, `measurements.cls`, `measurements.fcp`, `measurements.fid`, `measurements.fp`, `measurements.lcp`, `measurements.ttfb`, or `measurements.ttfb.requesttime`.
+            - `percentile(x,y)`, where `x` is `transaction.duration`, `measurements.cls`, `measurements.fcp`, `measurements.fid`, `measurements.fp`, `measurements.lcp`, `measurements.ttfb`, or `measurements.ttfb.requesttime`, and `y` is the custom percentile.
+            - `failure_rate()`
+            - `apdex(x)`, where `x` is the value of the Apdex score.
+            - `count()`
+        ```json
+        {
+            "queryType": 1,
+            "dataset": "generic_metrics",
+            "aggregate": "p75(measurements.ttfb)"
+        }
+        ```
         """
         return self.create_metric_alert(request, organization)


Code review comments:
path: src/sentry/api/serializers/models/alert_rule.py
text: @user1:
Please remove `sentry.api.serializers.models.alert_rule` from `pyproject.toml` or the typing changes won't be meaningful.

diff_hunk: @@ -1,7 +1,8 @@
 from collections import defaultdict
-from typing import MutableMapping
+from typing import List, MutableMapping, Optional
path: src/sentry/api/serializers/models/alert_rule.py
text: @user1:
Can you please add `from __future__ import annotations` and then use `list`?

@author:
To my understanding `List` is used more commonly within `src/sentry/api`

diff_hunk: @@ -141,7 +180,7 @@ def get_attrs(self, item_list, user, **kwargs):
 
         return result
 
-    def serialize(self, obj, attrs, user, **kwargs):
+    def serialize(self, obj, attrs, user, **kwargs) -> AlertRuleSerializerResponse:
path: src/sentry/api/serializers/models/alert_rule.py
text: @user1:
The signature has to be completely typed to do a formal typing.