Browse Source

chore(api): publish organizations_sessions endpoint (#64032)

Ogi 1 year ago
parent
commit
3ed32f38ab

+ 0 - 3
api-docs/openapi.json

@@ -108,9 +108,6 @@
     "/api/0/organizations/{organization_slug}/repos/{repo_id}/commits/": {
       "$ref": "paths/organizations/repo-commits.json"
     },
-    "/api/0/organizations/{organization_slug}/sessions/": {
-      "$ref": "paths/releases/sessions.json"
-    },
     "/api/0/organizations/{organization_slug}/users/": {
       "$ref": "paths/organizations/users.json"
     },

+ 0 - 198
api-docs/paths/releases/sessions.json

@@ -1,198 +0,0 @@
-{
-  "get": {
-    "tags": ["Releases"],
-    "description": "Returns a time series of release health session statistics for projects bound to an organization.\n\nThe interval and date range are subject to certain restrictions and rounding rules.\n\nThe date range is rounded to align with the interval, and is rounded to at least one hour. The interval can at most be one day and at least one hour currently. It has to cleanly divide one day, for rounding reasons.\n\nBecause of technical limitations, this endpoint returns at most 10000 data points. For example, if you select a 90 day window grouped by releases, you will see at most `floor(10k / (90 + 1)) = 109` releases. To get more results, reduce the `statsPeriod`.",
-    "operationId": "Retrieve Release Health Session Statistics",
-    "parameters": [
-      {
-        "name": "organization_slug",
-        "in": "path",
-        "description": "The slug of the organization.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "project",
-        "in": "query",
-        "description": "The ID of the projects to filter by.\n\nUse `-1` to include all accessible projects.",
-        "required": true,
-        "schema": {
-          "type": "array",
-          "items": {
-            "type": "integer"
-          }
-        },
-        "style": "form",
-        "explode": true
-      },
-      {
-        "name": "field",
-        "in": "query",
-        "description": "The list of fields to query.\n\nThe available fields are\n  - `sum(session)`\n  - `count_unique(user)`\n  - `avg`, `p50`, `p75`, `p90`, `p95`, `p99`, `max` applied to `session.duration`. For example, `p99(session.duration)`. Session duration is [no longer being recorded](https://github.com/getsentry/sentry/discussions/42716) as of on Jan 12, 2023. Returned data may be incomplete.\n  - `crash_rate`, `crash_free_rate` applied to `user` or `session`. For example, `crash_free_rate(user)`",
-        "required": true,
-        "schema": {
-          "type": "array",
-          "items": {
-            "type": "string"
-          }
-        },
-        "style": "form",
-        "explode": true
-      },
-      {
-        "name": "environment",
-        "in": "query",
-        "description": "The name of environments to filter by.",
-        "required": false,
-        "schema": {
-          "type": "array",
-          "items": {
-            "type": "string"
-          }
-        },
-        "style": "form",
-        "explode": true
-      },
-      {
-        "name": "groupBy",
-        "in": "query",
-        "description": "The list of properties to group by.\n\nThe available groupBy conditions are `project`, `release`, `environment` and `session.status`.\n\nGrouping by `session.status` does not work when `crash_rate` or `crash_free_rate` are queried.",
-        "required": false,
-        "schema": {
-          "type": "array",
-          "items": {
-            "type": "string"
-          }
-        },
-        "style": "form",
-        "explode": true
-      },
-      {
-        "name": "orderBy",
-        "in": "query",
-        "description": "An optional field to order by, which must be one of the fields provided in `field`. Use `-` for descending order, for example `-sum(session)`. \n\nThis alters the order of the `groups` returned, so it only makes sense in combination with `groupBy`. \n\nOrdering by more than one field is currently not supported.",
-        "required": false,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "query",
-        "in": "query",
-        "description": "A free-form query that is applied as a filter.\n\nAn example query could be `release:\"1.1.0\" or release:\"1.2.0\"`.",
-        "required": false,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "name": "statsPeriod",
-        "in": "query",
-        "description": "This defines the range of the time series, relative to now.\n\nThe range is given in a `\"<number><unit>\"` format.\n\nFor example `1d` for a one day range. Possible units are `m` for minutes, `h` for hours, `d` for days and `w` for weeks.\n\nIt defaults to `90d`.",
-        "required": false,
-        "schema": { "type": "string" }
-      },
-      {
-        "name": "interval",
-        "in": "query",
-        "description": "This is the resolution of the time series, given in the same format as `statsPeriod`.\n\nThe default resolution is `1h` and the minimum resolution is currently restricted to `1h` as well.\n\nIntervals larger than `1d` are not supported, and the interval has to cleanly divide one day.",
-        "required": false,
-        "schema": { "type": "string" }
-      },
-      {
-        "name": "statsPeriodStart",
-        "in": "query",
-        "description": "This defines the start of the time series range, in the same format as the `interval`, relative to now.",
-        "required": false,
-        "schema": { "type": "string" }
-      },
-      {
-        "name": "statsPeriodEnd",
-        "in": "query",
-        "description": "This defines the end of the time series range, in the same format as the `interval`, relative to now.",
-        "required": false,
-        "schema": { "type": "string" }
-      },
-      {
-        "name": "start",
-        "in": "query",
-        "description": "This defines the start of the time series range as an explicit datetime.",
-        "required": false,
-        "schema": { "type": "string", "format": "date-time" }
-      },
-      {
-        "name": "end",
-        "in": "query",
-        "description": "This defines the inclusive end of the time series range as an explicit datetime.",
-        "required": false,
-        "schema": { "type": "string", "format": "date-time" }
-      }
-    ],
-    "responses": {
-      "200": {
-        "description": "Time-series Session Statistics.",
-        "content": {
-          "application/json": {
-            "schema": {
-              "$ref": "../../components/schemas/sessions.json#/Sessions"
-            },
-            "example": {
-              "start": "2021-02-01T00:00:00Z",
-              "end": "2021-02-04T00:00:00Z",
-              "intervals": [
-                "2021-02-01T00:00:00Z",
-                "2021-02-02T00:00:00Z",
-                "2021-02-03T00:00:00Z"
-              ],
-              "groups": [
-                {
-                  "by": { "session.status": "healthy" },
-                  "totals": { "sum(session)": 1715553 },
-                  "series": { "sum(session)": [683772, 677788, 353993] }
-                },
-                {
-                  "by": { "session.status": "abnormal" },
-                  "totals": { "sum(session)": 0 },
-                  "series": { "sum(session)": [0, 0, 0] }
-                },
-                {
-                  "by": { "session.status": "crashed" },
-                  "totals": { "sum(session)": 383 },
-                  "series": { "sum(session)": [33, 26, 324] }
-                },
-                {
-                  "by": { "session.status": "errored" },
-                  "totals": { "sum(session)": 1565 },
-                  "series": { "sum(session)": [163, 201, 1201] }
-                }
-              ]
-            }
-          }
-        }
-      },
-      "400": {
-        "description": "Wrong Parameters",
-        "content": {
-          "application/json": {
-            "schema": {
-              "$ref": "../../components/schemas/error.json#/ApiError"
-            }
-          }
-        }
-      },
-      "401": {
-        "description": "Unauthorized"
-      },
-      "403": {
-        "description": "Forbidden"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["org: read"]
-      }
-    ]
-  }
-}

+ 50 - 2
src/sentry/api/endpoints/organization_sessions.py

@@ -3,6 +3,7 @@ from typing import Optional
 
 import sentry_sdk
 from django.utils.datastructures import MultiValueDict
+from drf_spectacular.utils import extend_schema
 from rest_framework.exceptions import ParseError
 from rest_framework.request import Request
 from rest_framework.response import Response
@@ -15,21 +16,68 @@ from sentry.api.bases import NoProjects
 from sentry.api.bases.organization import OrganizationEndpoint
 from sentry.api.paginator import GenericOffsetPaginator
 from sentry.api.utils import handle_query_errors
+from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_UNAUTHORIZED
+from sentry.apidocs.examples.session_examples import SessionExamples
+from sentry.apidocs.parameters import (
+    GlobalParams,
+    OrganizationParams,
+    SessionsParams,
+    VisibilityParams,
+)
+from sentry.apidocs.utils import inline_sentry_response_serializer
 from sentry.exceptions import InvalidParams
 from sentry.models.organization import Organization
+from sentry.release_health.base import SessionsQueryResult
 from sentry.snuba.sessions_v2 import SNUBA_LIMIT, InvalidField, QueryDefinition
 from sentry.utils.cursors import Cursor, CursorResult
 
 
+@extend_schema(tags=["Releases"])
 @region_silo_endpoint
 class OrganizationSessionsEndpoint(OrganizationEndpoint):
     publish_status = {
-        "GET": ApiPublishStatus.UNKNOWN,
+        "GET": ApiPublishStatus.PUBLIC,
     }
     owner = ApiOwner.TELEMETRY_EXPERIENCE
 
+    @extend_schema(
+        operation_id="Retrieve Release Health Session Statistics",
+        parameters=[
+            GlobalParams.START,
+            GlobalParams.END,
+            GlobalParams.ENVIRONMENT,
+            GlobalParams.ORG_SLUG,
+            GlobalParams.STATS_PERIOD,
+            OrganizationParams.PROJECT,
+            SessionsParams.FIELD,
+            SessionsParams.PER_PAGE,
+            SessionsParams.INTERVAL,
+            SessionsParams.GROUP_BY,
+            SessionsParams.ORDER_BY,
+            SessionsParams.INCLUDE_TOTALS,
+            SessionsParams.INCLUDE_SERIES,
+            VisibilityParams.QUERY,
+        ],
+        responses={
+            200: inline_sentry_response_serializer("SessionsQueryResult", SessionsQueryResult),
+            400: RESPONSE_BAD_REQUEST,
+            401: RESPONSE_UNAUTHORIZED,
+        },
+        examples=SessionExamples.QUERY_SESSIONS,
+    )
     def get(self, request: Request, organization) -> Response:
-        def data_fn(offset: int, limit: int):
+        """
+        Returns a time series of release health session statistics for projects bound to an "
+        "organization.\n\nThe interval and date range are subject to certain restrictions and rounding "
+        "rules.\n\nThe date range is rounded to align with the interval, and is rounded to at least one "
+        "hour. The interval can at most be one day and at least one hour currently. It has to cleanly "
+        "divide one day, for rounding reasons.\n\nBecause of technical limitations, this endpoint returns "
+        "at most 10000 data points. For example, if you select a 90 day window grouped by releases, "
+        "you will see at most `floor(10k / (90 + 1)) = 109` releases. To get more results, reduce the "
+        "`statsPeriod`."
+        """
+
+        def data_fn(offset: int, limit: int) -> SessionsQueryResult:
             with self.handle_query_errors():
                 with sentry_sdk.start_span(
                     op="sessions.endpoint", description="build_sessions_query"

+ 33 - 0
src/sentry/apidocs/examples/session_examples.py

@@ -0,0 +1,33 @@
+from drf_spectacular.utils import OpenApiExample
+
+
+class SessionExamples:
+    QUERY_SESSIONS = [
+        OpenApiExample(
+            "Query Sessions",
+            value={
+                "groups": [
+                    {
+                        "by": {"session.status": "errored"},
+                        "totals": {"sum(session)": 1000},
+                        "series": {"sum(session)": [368, 392, 240]},
+                    },
+                    {
+                        "by": {"session.status": "healthy"},
+                        "totals": {"sum(session)": 17905998},
+                        "series": {"sum(session)": [6230841, 6923689, 4751465]},
+                    },
+                ],
+                "start": "2024-01-29T07:30:00Z",
+                "end": "2024-01-29T09:00:00Z",
+                "intervals": [
+                    "2024-01-29T07:30:00Z",
+                    "2024-01-29T08:00:00Z",
+                    "2024-01-29T08:30:00Z",
+                ],
+                "query": "",
+            },
+            status_codes=["200"],
+            response_only=True,
+        )
+    ]

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

@@ -375,3 +375,64 @@ class IntegrationParams:
         type=bool,
         description="""Specify `True` to fetch third-party integration configurations. Note that this can add several seconds to the response time.""",
     )
+
+
+class SessionsParams:
+    FIELD = OpenApiParameter(
+        name="field",
+        location="query",
+        required=True,
+        type=str,
+        many=True,
+        description="""The list of fields to query.\n\nThe available fields are\n  - `sum(session)`\n  - `count_unique("
+                    "user)`\n  - `avg`, `p50`, `p75`, `p90`, `p95`, `p99`, `max` applied to `session.duration`. For "
+                    "example, `p99(session.duration)`. Session duration is [no longer being recorded]("
+                    "https://github.com/getsentry/sentry/discussions/42716) as of on Jan 12, 2023. Returned data may "
+                    "be incomplete.\n  - `crash_rate`, `crash_free_rate` applied to `user` or `session`. For example, "
+                    "`crash_free_rate(user)`""",
+    )
+    INTERVAL = OpenApiParameter(
+        name="interval",
+        location="query",
+        required=False,
+        type=str,
+        description="""Resolution of the time series, given in the same format as `statsPeriod`.\n\nThe default and
+        the minimum interval is `1h`.""",
+    )
+    PER_PAGE = OpenApiParameter(
+        name="per_page",
+        location="query",
+        required=False,
+        type=int,
+        description="""The number of groups to return per request.""",
+    )
+    GROUP_BY = OpenApiParameter(
+        name="groupBy",
+        location="query",
+        required=False,
+        type=str,
+        description="""The list of properties to group by.\n\nThe available groupBy conditions are `project`,
+        `release`, `environment` and `session.status`.""",
+    )
+    ORDER_BY = OpenApiParameter(
+        name="orderBy",
+        location="query",
+        required=False,
+        type=str,
+        description="""An optional field to order by, which must be one of the fields provided in `field`. Use `-`
+        for descending order, for example `-sum(session)`""",
+    )
+    INCLUDE_TOTALS = OpenApiParameter(
+        name="includeTotals",
+        location="query",
+        required=False,
+        type=int,
+        description="""Specify `0` to exclude totals from the response. The default is `1`""",
+    )
+    INCLUDE_SERIES = OpenApiParameter(
+        name="includeSeries",
+        location="query",
+        required=False,
+        type=int,
+        description="""Specify `0` to exclude series from the response. The default is `1`""",
+    )

+ 8 - 6
src/sentry/release_health/base.py

@@ -7,6 +7,8 @@ from typing import (
     TYPE_CHECKING,
     Any,
     Collection,
+    Dict,
+    List,
     Literal,
     Mapping,
     Optional,
@@ -85,22 +87,22 @@ class SessionsQuery(TypedDict):
     rollup: int  # seconds
 
 
-SessionsQueryValue = Union[None, float, int]
+SessionsQueryValue = Union[None, float]
 
 ProjectWithCount = Tuple[ProjectId, int]
 
 
 class SessionsQueryGroup(TypedDict):
-    by: Mapping[GroupByFieldName, Union[str, int]]
-    series: Mapping[SessionsQueryFunction, Sequence[SessionsQueryValue]]
-    totals: Mapping[SessionsQueryFunction, SessionsQueryValue]
+    by: Dict[str, Union[str, int]]
+    series: Dict[str, List[SessionsQueryValue]]
+    totals: Dict[str, SessionsQueryValue]
 
 
 class SessionsQueryResult(TypedDict):
     start: DateString
     end: DateString
-    intervals: Sequence[DateString]
-    groups: Sequence[SessionsQueryGroup]
+    intervals: List[DateString]
+    groups: List[SessionsQueryGroup]
     query: str