Skip to content
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
37 changes: 35 additions & 2 deletions src/sentry/api/serializers/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,39 @@ class GroupProjectResponse(TypedDict):
platform: str | None


class FeedbackSdkMetadataResponse(TypedDict):
"""SDK identity stored in Feedback issue metadata when available."""

name: str
name_normalized: str


class FeedbackGroupMetadataOptional(TypedDict, total=False):
"""Feedback metadata omitted from historical issues or absent by design."""

associated_event_id: str
initial_priority: int
sdk: FeedbackSdkMetadataResponse
source: str | None
summary: str | None


class FeedbackGroupMetadataResponse(FeedbackGroupMetadataOptional):
"""Metadata returned for modern User Feedback issue groups."""

contact_email: str | None
message: str
name: str | None
title: str
value: str


# Issue endpoints return heterogeneous group categories. Keeping the generic
# branch preserves metadata from other issue types while exposing Feedback's
# stable fields to generated OpenAPI clients.
GroupMetadataResponse = dict[str, Any] | FeedbackGroupMetadataResponse


class BaseGroupResponseOptional(TypedDict, total=False):
isUnhandled: bool
count: str
Expand Down Expand Up @@ -145,7 +178,7 @@ class BaseGroupSerializerResponse(BaseGroupResponseOptional):
type: EventTypeStr
issueType: str
issueCategory: str
metadata: dict[str, Any]
metadata: GroupMetadataResponse
numComments: int
assignedTo: ActorSerializerResponse | None
isBookmarked: bool
Expand Down Expand Up @@ -1222,7 +1255,7 @@ class SimpleGroupSerializerResponse(TypedDict):
type: EventTypeStr
issueType: str
issueCategory: str
metadata: dict[str, Any]
metadata: GroupMetadataResponse
numComments: int
firstSeen: datetime | None
lastSeen: datetime | None
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/api/serializers/models/group_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
BaseGroupSerializerResponse,
GroupAnnotation,
GroupLevelStr,
GroupMetadataResponse,
GroupProjectResponse,
GroupSerializer,
GroupSerializerSnuba,
Expand Down Expand Up @@ -270,7 +271,7 @@ class StreamGroupSerializerSnubaResponse(TypedDict):
type: NotRequired[EventTypeStr]
issueType: NotRequired[str]
issueCategory: NotRequired[str]
metadata: NotRequired[dict[str, Any]]
metadata: NotRequired[GroupMetadataResponse]
numComments: NotRequired[int]
assignedTo: NotRequired[ActorSerializerResponse | None]
isBookmarked: NotRequired[bool]
Expand Down
65 changes: 65 additions & 0 deletions tests/sentry/apidocs/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from collections.abc import Mapping
from typing import Any, Literal, TypedDict
from unittest.mock import patch

import pytest
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers

from sentry.api.serializers import Serializer
from sentry.api.serializers.models.group import GroupDetailsResponse
from sentry.api.serializers.models.group_stream import StreamGroupSerializerSnubaResponse
from sentry.apidocs.extensions import (
RestrictedJsonFieldExtension,
SentryInlineResponseSerializerExtension,
Expand Down Expand Up @@ -123,6 +126,68 @@ def test_sentry_inline_response_serializer_extension() -> None:
}


@patch.dict("os.environ", {"OPENAPIGENERATE": "1"})
def test_issue_responses_document_feedback_metadata() -> None:
detail_serializer = inline_sentry_response_serializer("IssueDetail", GroupDetailsResponse)
detail_schema = SentryInlineResponseSerializerExtension(detail_serializer).map_serializer(
AutoSchema(), "response"
)
list_serializer = inline_sentry_response_serializer(
"IssueList", list[StreamGroupSerializerSnubaResponse]
)
list_schema = SentryInlineResponseSerializerExtension(list_serializer).map_serializer(
AutoSchema(), "response"
)
metadata_schemas = [
detail_schema["properties"]["metadata"],
list_schema["items"]["properties"]["metadata"],
]

for metadata_schema in metadata_schemas:
assert metadata_schema["anyOf"][0] == {
"type": "object",
"additionalProperties": {},
}
feedback_schema = metadata_schema["anyOf"][1]

assert set(feedback_schema["properties"]) == {
"associated_event_id",
"contact_email",
"initial_priority",
"message",
"name",
"sdk",
"source",
"summary",
"title",
"value",
}
assert set(feedback_schema["required"]) == {
"contact_email",
"message",
"name",
"title",
"value",
}
assert feedback_schema["properties"]["initial_priority"] == {"type": "integer"}
assert feedback_schema["properties"]["contact_email"] == {
"type": "string",
"nullable": True,
}
assert feedback_schema["properties"]["name"] == {
"type": "string",
"nullable": True,
}
assert feedback_schema["properties"]["sdk"]["properties"] == {
"name": {"type": "string"},
"name_normalized": {"type": "string"},
}
assert feedback_schema["properties"]["sdk"]["required"] == [
"name",
"name_normalized",
]


def test_sentry_fails_when_serializer_not_typed() -> None:
seralizer_extension = SentryResponseSerializerExtension(FailSerializer)
with pytest.raises(TypeError):
Expand Down
Loading