Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(sentry-apps): Adds sentry app details API documentation #80508

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/sentry/apidocs/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@ class MetricAlertParams:
)


class SentryAppParams:
SENTRY_APP_ID_OR_SLUG = OpenApiParameter(
name="sentry_app_id_or_slug",
location="path",
required=True,
many=False,
type=str,
description="The ID or slug of the custom integration.",
)


class VisibilityParams:
QUERY = OpenApiParameter(
name="query",
Expand Down
75 changes: 60 additions & 15 deletions src/sentry/sentry_apps/api/parsers/sentry_app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
from jsonschema.exceptions import ValidationError as SchemaValidationError
from rest_framework import serializers
from rest_framework.serializers import Serializer, ValidationError

from sentry.api.serializers.rest_framework.base import camel_to_snake_case
from sentry.apidocs.parameters import build_typed_list
from sentry.integrations.models.integration_feature import Feature
from sentry.models.apiscopes import ApiScopes
from sentry.sentry_apps.api.parsers.schema import validate_ui_element_schema
Expand All @@ -13,6 +16,7 @@
)


@extend_schema_field(build_typed_list(OpenApiTypes.STR))
class ApiScopesField(serializers.Field):
def to_internal_value(self, data):
valid_scopes = ApiScopes()
Expand All @@ -26,6 +30,7 @@ def to_internal_value(self, data):
return data


@extend_schema_field(build_typed_list(OpenApiTypes.STR))
class EventListField(serializers.Field):
def to_internal_value(self, data):
if data is None:
Expand All @@ -40,6 +45,7 @@ def to_internal_value(self, data):
return data


@extend_schema_field(OpenApiTypes.OBJECT)
class SchemaField(serializers.Field):
def to_internal_value(self, data):
if data is None:
Expand All @@ -66,24 +72,63 @@ def to_internal_value(self, url):
return url


@extend_schema_serializer(exclude_fields=["popularity"])
class SentryAppParser(Serializer):
name = serializers.CharField()
author = serializers.CharField(required=False, allow_null=True)
scopes = ApiScopesField(allow_null=True)
status = serializers.CharField(required=False, allow_null=True)
events = EventListField(required=False, allow_null=True)
name = serializers.CharField(help_text="The name of the custom integration.")
author = serializers.CharField(
required=False, allow_null=True, help_text="The custom integration's author."
)
scopes = ApiScopesField(
allow_null=True, help_text="The custom integration's permission scopes for API access."
)
status = serializers.CharField(
required=False, allow_null=True, help_text="The custom integration's status."
)
events = EventListField(
required=False,
allow_null=True,
help_text="Webhook events the custom integration is subscribed to.",
)
features = serializers.MultipleChoiceField(
choices=Feature.as_choices(), allow_blank=True, allow_null=True, required=False
choices=Feature.as_choices(),
allow_blank=True,
allow_null=True,
required=False,
help_text="The features available via the custom integration",
)
schema = SchemaField(required=False, allow_null=True, help_text="??")
webhookUrl = URLField(
required=False,
allow_null=True,
allow_blank=True,
help_text="The URL where webhook events will be sent.",
)
redirectUrl = URLField(
required=False,
allow_null=True,
allow_blank=True,
help_text="The authentication redirect URL.",
)
isInternal = serializers.BooleanField(
required=False,
default=False,
help_text="Whether or not the integration is internal only. False means the integration is public.",
)
isAlertable = serializers.BooleanField(
required=False,
default=False,
help_text="Marks whether or not the custom integration can be used in an alert rule.",
)
overview = serializers.CharField(
required=False, allow_null=True, help_text="The custom integration's description."
)
verifyInstall = serializers.BooleanField(
required=False,
default=True,
help_text="Whether or not an installation of the custom integration should be verified.",
)
schema = SchemaField(required=False, allow_null=True)
webhookUrl = URLField(required=False, allow_null=True, allow_blank=True)
redirectUrl = URLField(required=False, allow_null=True, allow_blank=True)
isInternal = serializers.BooleanField(required=False, default=False)
isAlertable = serializers.BooleanField(required=False, default=False)
overview = serializers.CharField(required=False, allow_null=True)
verifyInstall = serializers.BooleanField(required=False, default=True)
allowedOrigins = serializers.ListField(
child=serializers.CharField(max_length=255), required=False
child=serializers.CharField(max_length=255), required=False, help_text="FILL THIS IN"
)
# Bounds chosen to match PositiveSmallIntegerField (https://docs.djangoproject.com/en/3.2/ref/models/fields/#positivesmallintegerfield)
popularity = serializers.IntegerField(
Expand All @@ -95,7 +140,7 @@ class SentryAppParser(Serializer):

def __init__(self, *args, **kwargs):
self.active_staff = kwargs.pop("active_staff", False)
self.access = kwargs.pop("access")
self.access = kwargs.pop("access", None)
Serializer.__init__(self, *args, **kwargs)

# an abstraction to pull fields from attrs if they are available or the sentry_app if not
Expand Down
82 changes: 59 additions & 23 deletions src/sentry/sentry_apps/api/serializers/sentry_app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections.abc import Mapping, Sequence
from datetime import timedelta
from typing import Any
from datetime import datetime, timedelta
from typing import Any, TypedDict

from django.utils import timezone

Expand All @@ -16,12 +16,46 @@
from sentry.sentry_apps.api.serializers.sentry_app_avatar import (
SentryAppAvatarSerializer as ResponseSentryAppAvatarSerializer,
)
from sentry.sentry_apps.api.serializers.sentry_app_avatar import SentryAppAvatarSerializerResponse
from sentry.sentry_apps.models.sentry_app import MASKED_VALUE, SentryApp
from sentry.sentry_apps.models.sentry_app_avatar import SentryAppAvatar
from sentry.users.models.user import User
from sentry.users.services.user.service import user_service


class OwnerResponseField(TypedDict):
id: int
slug: str


class SentryAppSerializerOptionalFields(TypedDict, total=False):
author: str | None
overview: str | None
popularity: int | None
redirectUrl: str | None
webhookUrl: str | None
datePublished: datetime | None
clientSecret: str | None
clientId: str | None
owner: OwnerResponseField | None


class SentryAppSerializerResponse(SentryAppSerializerOptionalFields):
allowedOrigins: list[str]
avatars: SentryAppAvatarSerializerResponse
events: set[str]
featureData: list[str]
isAlertable: bool
metadata: str
name: str
schema: str
scopes: list[str]
slug: str
status: str
uuid: str
verifyInstall: bool


@register(SentryApp)
class SentryAppSerializer(Serializer):
def get_attrs(self, item_list: Sequence[SentryApp], user: User, **kwargs: Any):
Expand Down Expand Up @@ -57,35 +91,37 @@ def get_attrs(self, item_list: Sequence[SentryApp], user: User, **kwargs: Any):
for item in item_list
}

def serialize(self, obj: SentryApp, attrs: Mapping[str, Any], user: User, **kwargs: Any):
def serialize(
self, obj: SentryApp, attrs: Mapping[str, Any], user: User, **kwargs: Any
) -> SentryAppSerializerResponse:
from sentry.sentry_apps.logic import consolidate_events

application = attrs["application"]

data = {
"allowedOrigins": application.get_allowed_origins(),
"author": obj.author,
"avatars": serialize(
data = SentryAppSerializerResponse(
allowedOrigins=application.get_allowed_origins(),
author=obj.author,
avatars=serialize(
objects=attrs.get("avatars"),
user=user,
serializer=ResponseSentryAppAvatarSerializer(),
),
"events": consolidate_events(obj.events),
"featureData": [],
"isAlertable": obj.is_alertable,
"metadata": obj.metadata,
"name": obj.name,
"overview": obj.overview,
"popularity": obj.popularity,
"redirectUrl": obj.redirect_url,
"schema": obj.schema,
"scopes": obj.get_scopes(),
"slug": obj.slug,
"status": obj.get_status_display(),
"uuid": obj.uuid,
"verifyInstall": obj.verify_install,
"webhookUrl": obj.webhook_url,
}
events=consolidate_events(obj.events),
featureData=[],
isAlertable=obj.is_alertable,
metadata=obj.metadata,
name=obj.name,
overview=obj.overview,
popularity=obj.popularity,
redirectUrl=obj.redirect_url,
schema=obj.schema,
scopes=obj.get_scopes(),
slug=obj.slug,
status=obj.get_status_display(),
uuid=obj.uuid,
verifyInstall=obj.verify_install,
webhookUrl=obj.webhook_url,
)

if obj.status != SentryAppStatus.INTERNAL:
data["featureData"] = [serialize(x, user) for x in attrs.get("features", [])]
Expand Down
14 changes: 11 additions & 3 deletions src/sentry/sentry_apps/api/serializers/sentry_app_avatar.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
from collections.abc import MutableMapping
from typing import Any
from typing import TypedDict

from sentry.api.serializers import Serializer, register
from sentry.sentry_apps.models.sentry_app_avatar import SentryAppAvatar


class SentryAppAvatarSerializerResponse(TypedDict):
avatarType: str
avatarUuid: str
avatarUrl: str
color: bool


@register(SentryAppAvatar)
class SentryAppAvatarSerializer(Serializer):
def serialize(self, obj: SentryAppAvatar, attrs, user, **kwargs) -> MutableMapping[str, Any]:
def serialize(
self, obj: SentryAppAvatar, attrs, user, **kwargs
) -> SentryAppAvatarSerializerResponse:
return {
"avatarType": obj.get_avatar_type_display(),
"avatarUuid": obj.ident,
Expand Down
Loading