Browse Source

chore(api): Publish `OrganizationReleaseDetailsEndpoint` (#71286)

This PR removes previous JSON documentation for
OrganizationReleaseDetailsEndpoint and adds new documentation to publish
the endpoint.

<img width="1367" alt="image"
src="https://github.com/getsentry/sentry/assets/33237075/4712f153-d466-4c7c-b897-905b7b8a3e1e">

<img width="1261" alt="image"
src="https://github.com/getsentry/sentry/assets/33237075/bf05af02-44f4-4775-847e-f4e6cd0106c8">

<img width="1345" alt="image"
src="https://github.com/getsentry/sentry/assets/33237075/a324c536-a072-4ccf-ada8-fd4bffce5c1e">

For:
https://github.com/getsentry/team-core-product-foundations/issues/325
Raj Joshi 9 months ago
parent
commit
de0cbb9c9d

+ 0 - 3
api-docs/openapi.json

@@ -162,9 +162,6 @@
     "/api/0/organizations/{organization_id_or_slug}/releases/": {
       "$ref": "paths/releases/organization-releases.json"
     },
-    "/api/0/organizations/{organization_id_or_slug}/releases/{version}/": {
-      "$ref": "paths/releases/organization-release.json"
-    },
     "/api/0/organizations/{organization_id_or_slug}/releases/{version}/files/": {
       "$ref": "paths/releases/release-files.json"
     },

+ 0 - 225
api-docs/paths/releases/organization-release.json

@@ -1,225 +0,0 @@
-{
-  "get": {
-    "tags": ["Releases"],
-    "description": "Return a release for a given organization.",
-    "operationId": "Retrieve an Organization's Releases",
-    "parameters": [
-      {
-        "name": "organization_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the organization the release belongs to.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "version",
-        "in": "path",
-        "description": "The version identifier of the release.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      }
-    ],
-    "responses": {
-      "200": {
-        "description": "Success",
-        "content": {
-          "application/json": {
-            "schema": {
-              "$ref": "../../components/schemas/releases/organization-release.json#/OrganizationRelease"
-            },
-            "example": {
-              "id": 2,
-              "authors": [],
-              "commitCount": 0,
-              "data": {},
-              "dateCreated": "2018-11-06T21:20:08.033Z",
-              "dateReleased": null,
-              "deployCount": 0,
-              "firstEvent": null,
-              "lastCommit": null,
-              "lastDeploy": null,
-              "lastEvent": null,
-              "newGroups": 0,
-              "owner": null,
-              "projects": [
-                {
-                  "name": "Pump Station",
-                  "slug": "pump-station"
-                }
-              ],
-              "ref": "6ba09a7c53235ee8a8fa5ee4c1ca8ca886e7fdbb",
-              "shortVersion": "2.0rc2",
-              "url": null,
-              "version": "2.0rc2"
-            }
-          }
-        }
-      },
-      "403": {
-        "description": "Forbidden"
-      },
-      "404": {
-        "description": "Not Found"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["project:releases"]
-      }
-    ]
-  },
-  "put": {
-    "tags": ["Releases"],
-    "description": "Update a release for a given organization.",
-    "operationId": "Update an Organization's Release",
-    "parameters": [
-      {
-        "name": "organization_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the organization the release belongs to.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "version",
-        "in": "path",
-        "description": "The version identifier of the release.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      }
-    ],
-    "requestBody": {
-      "content": {
-        "application/json": {
-          "schema": {
-            "type": "object",
-            "properties": {
-              "ref": {
-                "type": "string",
-                "description": "An optional commit reference. This is useful if a tagged version has been provided."
-              },
-              "url": {
-                "type": "string",
-                "description": "A URL that points to the release. This can be the path to an online interface to the source code for instance."
-              },
-              "dateReleased": {
-                "type": "string",
-                "format": "date-time",
-                "description": "An optional date that indicates when the release went live. If not provided the current time is assumed."
-              },
-              "commits": {
-                "type": "array",
-                "items": {
-                  "type": "object"
-                },
-                "description": "An optional list of commit data to be associated with the release. Commits must include parameters `id` (the sha of the commit), and can optionally include `repository`, `message`, `author_name`, `author_email`, and `timestamp`."
-              },
-              "refs": {
-                "type": "array",
-                "items": {
-                  "type": "object"
-                },
-                "description": "An optional way to indicate the start and end commits for each repository included in a release. Head commits must include parameters `repository` and `commit` (the HEAD sha). They can optionally include `previousCommit` (the sha of the HEAD of the previous release), which should be specified if this is the first time you've sent commit data."
-              }
-            }
-          },
-          "example": {
-            "ref": "freshtofu",
-            "url": "https://vcshub.invalid/user/project/refs/freshtofu"
-          }
-        }
-      }
-    },
-    "responses": {
-      "200": {
-        "description": "Success",
-        "content": {
-          "application/json": {
-            "schema": {
-              "$ref": "../../components/schemas/releases/organization-release.json#/OrganizationRelease"
-            },
-            "example": {
-              "id": 2,
-              "authors": [],
-              "commitCount": 0,
-              "data": {},
-              "dateCreated": "2019-01-03T00:12:55.109Z",
-              "dateReleased": null,
-              "deployCount": 0,
-              "firstEvent": null,
-              "lastCommit": null,
-              "lastDeploy": null,
-              "lastEvent": null,
-              "newGroups": 0,
-              "owner": null,
-              "projects": [
-                {
-                  "name": "Pump Station",
-                  "slug": "pump-station"
-                }
-              ],
-              "ref": "6ba09a7c53235ee8a8fa5ee4c1ca8ca886e7fdbb",
-              "shortVersion": "2.0rc2",
-              "url": null,
-              "version": "2.0rc2"
-            }
-          }
-        }
-      },
-      "403": {
-        "description": "Forbidden"
-      },
-      "404": {
-        "description": "Not Found"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["project:releases"]
-      }
-    ]
-  },
-  "delete": {
-    "tags": ["Releases"],
-    "description": "Delete a release for a given organization.",
-    "operationId": "Delete an Organization's Release",
-    "parameters": [
-      {
-        "name": "organization_id_or_slug",
-        "in": "path",
-        "description": "The id or slug of the organization the release belongs to.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "version",
-        "in": "path",
-        "description": "The version identifier of the release.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      }
-    ],
-    "responses": {
-      "204": {
-        "description": "Success"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["project:releases"]
-      }
-    ]
-  }
-}

+ 77 - 46
src/sentry/api/endpoints/organization_release_details.py

@@ -1,10 +1,12 @@
 from django.db.models import Q
+from drf_spectacular.utils import extend_schema, extend_schema_serializer
 from rest_framework.exceptions import ParseError
 from rest_framework.request import Request
 from rest_framework.response import Response
 from rest_framework.serializers import ListField
 
 from sentry import release_health
+from sentry.api.api_owners import ApiOwner
 from sentry.api.api_publish_status import ApiPublishStatus
 from sentry.api.base import ReleaseAnalyticsMixin, region_silo_endpoint
 from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint
@@ -20,6 +22,16 @@ from sentry.api.serializers.rest_framework import (
     ReleaseHeadCommitSerializerDeprecated,
     ReleaseSerializer,
 )
+from sentry.api.serializers.types import ReleaseSerializerResponse
+from sentry.apidocs.constants import (
+    RESPONSE_FORBIDDEN,
+    RESPONSE_NO_CONTENT,
+    RESPONSE_NOT_FOUND,
+    RESPONSE_UNAUTHORIZED,
+)
+from sentry.apidocs.examples.organization_examples import OrganizationExamples
+from sentry.apidocs.parameters import GlobalParams, ReleaseParams, VisibilityParams
+from sentry.apidocs.utils import inline_sentry_response_serializer
 from sentry.models.activity import Activity
 from sentry.models.project import Project
 from sentry.models.release import Release, ReleaseStatus
@@ -33,11 +45,19 @@ class InvalidSortException(Exception):
     pass
 
 
+@extend_schema_serializer(exclude_fields=["headCommits", "status"])
 class OrganizationReleaseSerializer(ReleaseSerializer):
     headCommits = ListField(
-        child=ReleaseHeadCommitSerializerDeprecated(), required=False, allow_null=False
+        child=ReleaseHeadCommitSerializerDeprecated(),
+        required=False,
+        allow_null=False,
+    )
+    refs = ListField(
+        child=ReleaseHeadCommitSerializer(),
+        required=False,
+        allow_null=False,
+        help_text="""An optional way to indicate the start and end commits for each repository included in a release. Head commits must include parameters ``repository`` and ``commit`` (the HEAD SHA). They can optionally include ``previousCommit`` (the SHA of the HEAD of the previous release), which should be specified if this is the first time you've sent commit data.""",
     )
-    refs = ListField(child=ReleaseHeadCommitSerializer(), required=False, allow_null=False)
 
 
 def add_status_filter_to_queryset(queryset, status_filter):
@@ -269,29 +289,46 @@ class OrganizationReleaseDetailsPaginationMixin:
         }
 
 
+@extend_schema(tags=["Releases"])
 @region_silo_endpoint
 class OrganizationReleaseDetailsEndpoint(
     OrganizationReleasesBaseEndpoint,
     ReleaseAnalyticsMixin,
     OrganizationReleaseDetailsPaginationMixin,
 ):
+    owner = ApiOwner.UNOWNED
     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 Organization's Release",
+        parameters=[
+            GlobalParams.ORG_ID_OR_SLUG,
+            ReleaseParams.VERSION,
+            ReleaseParams.PROJECT_ID,
+            ReleaseParams.HEALTH,
+            ReleaseParams.ADOPTION_STAGES,
+            ReleaseParams.SUMMARY_STATS_PERIOD,
+            ReleaseParams.HEALTH_STATS_PERIOD,
+            ReleaseParams.SORT,
+            ReleaseParams.STATUS_FILTER,
+            VisibilityParams.QUERY,
+        ],
+        responses={
+            200: inline_sentry_response_serializer("OrgReleaseResponse", ReleaseSerializerResponse),
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=OrganizationExamples.RELEASE_DETAILS,
+    )
     def get(self, request: Request, organization, version) -> Response:
         """
-        Retrieve an Organization's Release
-        ``````````````````````````````````
 
         Return details on an individual release.
-
-        :pparam string organization_id_or_slug: the id or slug of the organization the
-                                          release belongs to.
-        :pparam string version: the version identifier of the release.
-        :auth: required
         """
         # Dictionary responsible for storing selected project meta data
         current_project_meta = {}
@@ -375,38 +412,26 @@ class OrganizationReleaseDetailsEndpoint(
             )
         )
 
+    @extend_schema(
+        operation_id="Update an Organization's Release",
+        parameters=[
+            GlobalParams.ORG_ID_OR_SLUG,
+            ReleaseParams.VERSION,
+        ],
+        request=OrganizationReleaseSerializer,
+        responses={
+            200: inline_sentry_response_serializer("OrgReleaseResponse", ReleaseSerializerResponse),
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+        examples=OrganizationExamples.RELEASE_DETAILS,
+    )
     def put(self, request: Request, organization, version) -> Response:
         """
-        Update an Organization's Release
-        ````````````````````````````````
 
         Update a release. This can change some metadata associated with
         the release (the ref, url, and dates).
-
-        :pparam string organization_id_or_slug: the id or slug of the organization the
-                                          release belongs to.
-        :pparam string version: the version identifier of the release.
-        :param string ref: an optional commit reference.  This is useful if
-                           a tagged version has been provided.
-        :param url url: a URL that points to the release.  This can be the
-                        path to an online interface to the sourcecode
-                        for instance.
-        :param datetime dateReleased: an optional date that indicates when
-                                      the release went live.  If not provided
-                                      the current time is assumed.
-        :param array commits: an optional list of commit data to be associated
-
-                              with the release. Commits must include parameters
-                              ``id`` (the sha of the commit), and can optionally
-                              include ``repository``, ``message``, ``author_name``,
-                              ``author_email``, and ``timestamp``.
-        :param array refs: an optional way to indicate the start and end commits
-                           for each repository included in a release. Head commits
-                           must include parameters ``repository`` and ``commit``
-                           (the HEAD sha). They can optionally include ``previousCommit``
-                           (the sha of the HEAD of the previous release), which should
-                           be specified if this is the first time you've sent commit data.
-        :auth: required
         """
         bind_organization_context(organization)
 
@@ -503,17 +528,23 @@ class OrganizationReleaseDetailsEndpoint(
 
             return Response(serialize(release, request.user))
 
+    @extend_schema(
+        operation_id="Delete an Organization's Release",
+        parameters=[
+            GlobalParams.ORG_ID_OR_SLUG,
+            ReleaseParams.VERSION,
+        ],
+        responses={
+            204: RESPONSE_NO_CONTENT,
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOT_FOUND,
+        },
+    )
     def delete(self, request: Request, organization, version) -> Response:
         """
-        Delete an Organization's Release
-        ````````````````````````````````
 
         Permanently remove a release and all of its files.
-
-        :pparam string organization_id_or_slug: the id or slug of the organization the
-                                          release belongs to.
-        :pparam string version: the version identifier of the release.
-        :auth: required
         """
         try:
             release = Release.objects.get(organization_id=organization.id, version=version)

+ 3 - 2
src/sentry/api/serializers/models/release.py

@@ -12,6 +12,7 @@ from django.db.models import Sum
 from sentry import release_health, tagstore
 from sentry.api.serializers import Serializer, register, serialize
 from sentry.api.serializers.models.user import UserSerializerResponse
+from sentry.api.serializers.types import ReleaseSerializerResponse
 from sentry.models.commit import Commit
 from sentry.models.commitauthor import CommitAuthor
 from sentry.models.deploy import Deploy
@@ -546,8 +547,8 @@ class ReleaseSerializer(Serializer):
             result[item] = p
         return result
 
-    def serialize(self, obj, attrs, user, **kwargs):
-        d = {
+    def serialize(self, obj, attrs, user, **kwargs) -> ReleaseSerializerResponse:
+        d: ReleaseSerializerResponse = {
             "id": obj.id,
             "version": obj.version,
             "status": ReleaseStatus.to_string(obj.status),

+ 119 - 0
src/sentry/api/serializers/release_details_types.py

@@ -0,0 +1,119 @@
+from datetime import datetime
+from typing import Any, TypedDict
+
+
+class ReleaseOptional(TypedDict, total=False):
+    status: int | None
+    project_id: int | None
+    ref: str | None
+    url: str | None
+    date_started: datetime | None
+    date_released: datetime | None
+    owner_id: int | None
+    commit_count: int | None
+    last_commit_id: int | None
+    authors: list[int] | None
+    total_deploys: int | None
+    last_deploy_id: int | None
+    package: str | None
+    major: int | None
+    minor: int | None
+    patch: int | None
+    revision: int | None
+    prerelease: str | None
+    build_code: str | None
+    build_number: int | None
+    user_agent: str | None
+
+
+class ReleaseTypedDict(ReleaseOptional):
+    organization: int
+    projects: list[int]
+    version: str
+    date_added: datetime
+    data: dict[str, Any]
+    new_groups: int
+
+
+class VersionInfoOptional(TypedDict, total=False):
+    description: str
+
+
+class VersionInfo(VersionInfoOptional):
+    package: str | None
+    version: dict[str, str]
+    buildHash: str | None
+
+
+class LastDeployOptional(TypedDict, total=False):
+    dateStarted: str | None
+    url: str | None
+
+
+class LastDeploy(LastDeployOptional):
+    id: int
+    environment: str
+    dateFinished: str
+    name: str
+
+
+class AuthorOptional(TypedDict, total=False):
+    lastLogin: str
+    has2fa: bool
+    lastActive: str
+    isSuperuser: bool
+    isStaff: bool
+    experiments: dict[str, str | int | float | bool | None]
+    emails: list[dict[str, int | str | bool]]
+    avatar: dict[str, str | None]
+
+
+class Author(AuthorOptional):
+    id: int
+    name: str
+    username: str
+    email: str
+    avatarUrl: str
+    isActive: bool
+    hasPasswordAuth: bool
+    isManaged: bool
+    dateJoined: str
+
+
+class HealthDataOptional(TypedDict, total=False):
+    durationP50: float | None
+    durationP90: float | None
+    crashFreeUsers: float | None
+    crashFreeSessions: float | None
+    totalUsers: int | None
+    totalUsers24h: int | None
+    totalProjectUsers24h: int | None
+    totalSessions: int | None
+    totalSessions24h: int | None
+    totalProjectSessions24h: int | None
+    adoption: float | None
+    sessionsAdoption: float | None
+
+
+class HealthData(HealthDataOptional):
+    sessionsCrashed: int
+    sessionsErrored: int
+    hasHealthData: bool
+    stats: dict[str, Any]
+
+
+class ProjectOptional(TypedDict, total=False):
+    healthData: HealthData | None
+    dateReleased: datetime | None
+    dateCreated: datetime | None
+    dateStarted: datetime | None
+
+
+class Project(ProjectOptional):
+    id: int
+    slug: str
+    name: str
+    newGroups: int
+    platform: str | None
+    platforms: list[str] | None
+    hasHealthData: bool

+ 22 - 4
src/sentry/api/serializers/rest_framework/release.py

@@ -52,11 +52,29 @@ class ReleaseHeadCommitSerializer(serializers.Serializer):
 
 class ReleaseSerializer(serializers.Serializer):
     ref = serializers.CharField(
-        max_length=MAX_VERSION_LENGTH, required=False, allow_null=True, allow_blank=True
+        max_length=MAX_VERSION_LENGTH,
+        required=False,
+        allow_null=True,
+        allow_blank=True,
+        help_text="An optional commit reference. This is useful if a tagged version has been provided.",
+    )
+    url = serializers.URLField(
+        required=False,
+        allow_null=True,
+        allow_blank=True,
+        help_text="A URL that points to the release. For instance, this can be the path to an online interface to the source code, such as a GitHub URL.",
+    )
+    dateReleased = serializers.DateTimeField(
+        required=False,
+        allow_null=True,
+        help_text="An optional date that indicates when the release went live.  If not provided the current time is used.",
+    )
+    commits = serializers.ListField(
+        child=CommitSerializer(),
+        required=False,
+        allow_null=False,
+        help_text="An optional list of commit data to be associated.",
     )
-    url = serializers.URLField(required=False, allow_null=True, allow_blank=True)
-    dateReleased = serializers.DateTimeField(required=False, allow_null=True)
-    commits = serializers.ListField(child=CommitSerializer(), required=False, allow_null=False)
 
     status = serializers.CharField(required=False, allow_null=False)
 

+ 34 - 1
src/sentry/api/serializers/types.py

@@ -1,5 +1,7 @@
 from datetime import datetime
-from typing import TypedDict
+from typing import Any, TypedDict
+
+from sentry.api.serializers.release_details_types import Author, LastDeploy, Project, VersionInfo
 
 
 class SerializedAvatarFields(TypedDict, total=False):
@@ -33,3 +35,34 @@ class OrganizationSerializerResponse(TypedDict):
     features: list[str]
     links: _Links
     hasAuthProvider: bool
+
+
+# Reponse type for OrganizationReleaseDetailsEndpoint
+class ReleaseSerializerResponseOptional(TypedDict, total=False):
+    ref: str | None
+    url: str | None
+    dateReleased: datetime | None
+    dateCreated: datetime | None
+    dateStarted: datetime | None
+    owner: dict[str, Any] | None
+    lastCommit: dict[str, Any] | None
+    lastDeploy: LastDeploy | None
+    firstEvent: datetime | None
+    lastEvent: datetime | None
+    currentProjectMeta: dict[str, Any] | None
+    userAgent: str | None
+    adoptionStages: dict[str, Any] | None  # Only included if with_adoption_stages is True
+
+
+class ReleaseSerializerResponse(ReleaseSerializerResponseOptional):
+    id: int
+    version: str
+    newGroups: int
+    status: str
+    shortVersion: str
+    versionInfo: VersionInfo
+    data: dict[str, Any]
+    commitCount: int
+    deployCount: int
+    authors: list[Author]
+    projects: list[Project]

+ 238 - 0
src/sentry/apidocs/examples/organization_examples.py

@@ -403,3 +403,241 @@ class OrganizationExamples:
             response_only=True,
         ),
     ]
+
+    RELEASE_DETAILS = [
+        OpenApiExample(
+            "Retrieve release details",
+            value={
+                "id": 1122684517,
+                "version": "control@abc123",
+                "status": "open",
+                "shortVersion": "control@abc123",
+                "versionInfo": {
+                    "package": "control",
+                    "version": {"raw": "abc123"},
+                    "description": "abc123",
+                    "buildHash": "abc123",
+                },
+                "ref": None,
+                "url": None,
+                "dateReleased": None,
+                "dateCreated": "2024-05-21T11:26:16.190281Z",
+                "data": {},
+                "newGroups": 0,
+                "owner": None,
+                "commitCount": 2,
+                "lastCommit": {
+                    "id": "xyz123",
+                    "message": "feat(raspberries): Made raspberries even more delicious",
+                    "dateCreated": "2024-05-21T11:04:55Z",
+                    "pullRequest": {
+                        "id": "70214",
+                        "title": "feat(raspberries): Made raspberries even more delicious",
+                        "message": "Made raspberries even more delicious",
+                        "dateCreated": "2024-05-03T07:32:28.205043Z",
+                        "repository": {
+                            "id": "1",
+                            "name": "raj/raspberries",
+                            "url": "https://raspberries.raspberries/raj/raspberries",
+                            "provider": {"id": "integrations:github", "name": "GitHub"},
+                            "status": "active",
+                            "dateCreated": "2016-10-10T21:36:42.414678Z",
+                            "integrationId": "2933",
+                            "externalSlug": "raj/raspberries",
+                            "externalId": "873328",
+                        },
+                        "author": {
+                            "id": "2837091",
+                            "name": "Raj's Raspberries",
+                            "username": "rajraspberry",
+                            "avatarUrl": "https://gravatar.com/avatar/bf99685de539465db9208ab3a888843ba0e5e85b1f156084484c7c6c31312be5?s=32&d=mm",
+                            "isActive": True,
+                            "hasPasswordAuth": False,
+                            "isManaged": False,
+                            "dateJoined": "2023-08-07T12:32:09.091427Z",
+                            "lastLogin": "2024-05-21T05:46:23.824074Z",
+                            "has2fa": True,
+                            "lastActive": "2024-05-21T13:59:10.614891Z",
+                            "isSuperuser": True,
+                            "isStaff": True,
+                            "experiments": {},
+                            "emails": [
+                                {
+                                    "id": "2972219",
+                                    "email": "raj@raspberries",
+                                    "is_verified": True,
+                                }
+                            ],
+                            "avatar": {
+                                "avatarType": "upload",
+                                "avatarUuid": "xyz123",
+                                "avatarUrl": "https://sentry.io/avatar/xyz123/",
+                            },
+                        },
+                        "externalUrl": "https://github.com/raj/raspberries/pull/70214",
+                    },
+                    "suspectCommitType": "",
+                    "repository": {
+                        "id": "1",
+                        "name": "raj/raspberries",
+                        "url": "https://github.com/raj/raspberries",
+                        "provider": {"id": "integrations:github", "name": "GitHub"},
+                        "status": "active",
+                        "dateCreated": "2016-10-10T21:36:42.414678Z",
+                        "integrationId": "2933",
+                        "externalSlug": "raj/raspberries",
+                        "externalId": "873328",
+                    },
+                    "author": {
+                        "id": "2837091",
+                        "name": "Raj's Raspberries",
+                        "username": "rajraspberry",
+                        "avatarUrl": "https://gravatar.com/avatar/bf99685de539465db9208ab3a888843ba0e5e85b1f156084484c7c6c31312be5?s=32&d=mm",
+                        "isActive": True,
+                        "hasPasswordAuth": False,
+                        "isManaged": False,
+                        "dateJoined": "2023-08-07T12:32:09.091427Z",
+                        "lastLogin": "2024-05-21T05:46:23.824074Z",
+                        "has2fa": True,
+                        "lastActive": "2024-05-21T13:59:10.614891Z",
+                        "isSuperuser": True,
+                        "isStaff": True,
+                        "experiments": {},
+                        "emails": [
+                            {"id": "2972219", "email": "raj@raspberries", "is_verified": True}
+                        ],
+                        "avatar": {
+                            "avatarType": "upload",
+                            "avatarUuid": "xyz123",
+                            "avatarUrl": "https://sentry.io/avatar/xyz123/",
+                        },
+                    },
+                    "releases": [
+                        {
+                            "version": "control@abc123",
+                            "shortVersion": "control@abc123",
+                            "ref": None,
+                            "url": None,
+                            "dateReleased": None,
+                            "dateCreated": "2024-05-21T11:26:16.190281Z",
+                        },
+                        {
+                            "version": "backend@abc123",
+                            "shortVersion": "backend@abc123",
+                            "ref": None,
+                            "url": None,
+                            "dateReleased": None,
+                            "dateCreated": "2024-05-21T11:56:36.790866Z",
+                        },
+                        {
+                            "version": "control@def789",
+                            "shortVersion": "control@def789",
+                            "ref": None,
+                            "url": None,
+                            "dateReleased": None,
+                            "dateCreated": "2024-05-21T12:44:25.923663Z",
+                        },
+                        {
+                            "version": "frontend@ghi012",
+                            "shortVersion": "frontend@ghi012",
+                            "ref": None,
+                            "url": None,
+                            "dateReleased": None,
+                            "dateCreated": "2024-05-21T12:46:42.338358Z",
+                        },
+                    ],
+                },
+                "deployCount": 1,
+                "lastDeploy": {
+                    "id": 53070941,
+                    "environment": "canary-test-control",
+                    "dateStarted": None,
+                    "dateFinished": "2024-05-21T11:26:17.597793Z",
+                    "name": "control@abc123 to canary-test-",
+                    "url": None,
+                },
+                "authors": [
+                    {
+                        "id": 2837091,
+                        "name": "Raj's Raspberries",
+                        "username": "rajraspberry",
+                        "email": "raj@raspberries",
+                        "avatarUrl": "https://gravatar.com/avatar/bf99685de539465db9208ab3a888843ba0e5e85b1f156084484c7c6c31312be5?s=32&d=mm",
+                        "isActive": True,
+                        "hasPasswordAuth": False,
+                        "isManaged": False,
+                        "dateJoined": "2023-08-07T12:32:09.091427Z",
+                        "lastLogin": "2024-05-21T05:46:23.824074Z",
+                        "has2fa": True,
+                        "lastActive": "2024-05-21T13:59:10.614891Z",
+                        "isSuperuser": True,
+                        "isStaff": True,
+                        "experiments": {},
+                        "emails": [
+                            {"id": "2972219", "email": "raj@raspberries", "is_verified": True}
+                        ],
+                        "avatar": {
+                            "avatarType": "upload",
+                            "avatarUuid": "xyz123",
+                            "avatarUrl": "https://sentry.io/avatar/xyz123/",
+                        },
+                    }
+                ],
+                "projects": [
+                    {
+                        "id": 1,
+                        "slug": "sentry",
+                        "name": "Backend",
+                        "newGroups": 0,
+                        "platform": "python",
+                        "platforms": ["native", "other", "python"],
+                        "hasHealthData": False,
+                        "healthData": {
+                            "durationP50": None,
+                            "durationP90": None,
+                            "crashFreeUsers": None,
+                            "crashFreeSessions": None,
+                            "sessionsCrashed": 0,
+                            "sessionsErrored": 0,
+                            "totalUsers": None,
+                            "totalUsers24h": None,
+                            "totalProjectUsers24h": None,
+                            "totalSessions": None,
+                            "totalSessions24h": None,
+                            "totalProjectSessions24h": None,
+                            "adoption": None,
+                            "sessionsAdoption": None,
+                            "stats": {
+                                "24h": [
+                                    [1715126400, 0],
+                                    [1715212800, 0],
+                                    [1715299200, 0],
+                                    [1715385600, 0],
+                                    [1715472000, 0],
+                                    [1715558400, 0],
+                                    [1715644800, 0],
+                                    [1715731200, 0],
+                                    [1715817600, 0],
+                                    [1715904000, 0],
+                                    [1715990400, 0],
+                                    [1716076800, 0],
+                                    [1716163200, 0],
+                                    [1716249600, 0],
+                                ]
+                            },
+                            "hasHealthData": False,
+                        },
+                    }
+                ],
+                "firstEvent": None,
+                "lastEvent": None,
+                "currentProjectMeta": {},
+                "userAgent": "Python-urllib/3.11",
+                "adoptionStages": {
+                    "sentry": {"stage": "low_adoption", "adopted": None, "unadopted": None}
+                },
+            },
+            status_codes=["200"],
+            response_only=True,
+        )
+    ]

+ 65 - 0
src/sentry/apidocs/parameters.py

@@ -5,6 +5,8 @@ from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import OpenApiParameter
 from rest_framework import serializers
 
+from sentry.snuba.sessions import STATS_PERIODS
+
 # NOTE: Please add new params by path vs query, then in alphabetical order
 
 
@@ -168,6 +170,69 @@ Valid fields include:
     )
 
 
+class ReleaseParams:
+    VERSION = OpenApiParameter(
+        name="version",
+        location="path",
+        required=True,
+        type=str,
+        description="The version identifier of the release",
+    )
+    PROJECT_ID = OpenApiParameter(
+        name="project_id",
+        location="query",
+        required=False,
+        type=str,
+        description="The project id to filter by.",
+    )
+    HEALTH = OpenApiParameter(
+        name="health",
+        location="query",
+        required=False,
+        type=bool,
+        description="Whether or not to include health data with the release. By default, this is false.",
+    )
+    ADOPTION_STAGES = OpenApiParameter(
+        name="adoptionStages",
+        location="query",
+        required=False,
+        type=bool,
+        description="Whether or not to include adoption stages with the release. By default, this is false.",
+    )
+    SUMMARY_STATS_PERIOD = OpenApiParameter(
+        name="summaryStatsPeriod",
+        location="query",
+        required=False,
+        type=str,
+        description="The period of time used to query summary stats for the release. By default, this is 14d.",
+        enum=list(STATS_PERIODS.keys()),
+    )
+    HEALTH_STATS_PERIOD = OpenApiParameter(
+        name="healthStatsPeriod",
+        location="query",
+        required=False,
+        type=str,
+        description="The period of time used to query health stats for the release. By default, this is 24h if health is enabled.",
+        enum=list(STATS_PERIODS.keys()),
+    )
+    SORT = OpenApiParameter(
+        name="sort",
+        location="query",
+        required=False,
+        type=str,
+        description="The field used to sort results by. By default, this is `date`.",
+        enum=["date", "sessions", "users", "crash_free_users", "crash_free_sessions"],
+    )
+    STATUS_FILTER = OpenApiParameter(
+        name="status",
+        location="query",
+        required=False,
+        type=str,
+        description="Release statuses that you can filter by.",
+        enum=["open", "archived"],
+    )
+
+
 class SCIMParams:
     TEAM_ID = OpenApiParameter(
         name="team_id",