Skip to content

Commit 5020aac

Browse files
committed
feat(investigations): Add response serializers
This splits out the response serializers from #121403 to keep the total pr size down, and restructures them into separate folders.
1 parent 45dad6e commit 5020aac

8 files changed

Lines changed: 892 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
__all__ = (
2+
"InvestigationBlockSerializer",
3+
"InvestigationBlockSerializerResponse",
4+
"InvestigationDetailsSerializer",
5+
"InvestigationDetailsSerializerResponse",
6+
"InvestigationParameterSerializer",
7+
"InvestigationParameterSerializerResponse",
8+
"InvestigationSerializer",
9+
"InvestigationSerializerResponse",
10+
)
11+
12+
13+
from .block import InvestigationBlockSerializer, InvestigationBlockSerializerResponse
14+
from .investigation import (
15+
InvestigationDetailsSerializer,
16+
InvestigationDetailsSerializerResponse,
17+
InvestigationSerializer,
18+
InvestigationSerializerResponse,
19+
)
20+
from .parameter import (
21+
InvestigationParameterSerializer,
22+
InvestigationParameterSerializerResponse,
23+
)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from __future__ import annotations
2+
3+
from collections import defaultdict
4+
from collections.abc import Mapping, MutableMapping, Sequence
5+
from datetime import datetime
6+
from typing import Any, TypedDict, override
7+
8+
from django.contrib.auth.models import AnonymousUser
9+
10+
from sentry.api.serializers import Serializer
11+
from sentry.investigations.models import (
12+
InvestigationBlock,
13+
InvestigationBlockDependency,
14+
InvestigationBlockExecution,
15+
InvestigationBlockExecutionStatus,
16+
InvestigationBlockKind,
17+
InvestigationBlockParameter,
18+
)
19+
from sentry.users.models.user import User
20+
from sentry.users.services.user.model import RpcUser
21+
22+
23+
class InvestigationBlockExecutionSerializerResponse(TypedDict):
24+
id: str
25+
status: str
26+
executor: str
27+
schemaVersion: int
28+
startedAt: datetime | None
29+
completedAt: datetime | None
30+
error: Any | None
31+
32+
33+
class InvestigationBlockSerializerResponse(TypedDict):
34+
id: str
35+
position: int
36+
kind: str
37+
title: str
38+
content: str
39+
generationPrompt: str
40+
generatedContent: str
41+
output: Any | None
42+
outputStatus: str
43+
currentExecution: InvestigationBlockExecutionSerializerResponse | None
44+
config: dict[str, Any]
45+
display: dict[str, Any]
46+
dependencies: list[str]
47+
parameterKeys: list[str]
48+
version: int
49+
staleAt: datetime | None
50+
createdBy: str | None
51+
lastEditedBy: str | None
52+
53+
54+
class InvestigationBlockSerializer(Serializer[InvestigationBlockSerializerResponse]):
55+
"""
56+
Serializes a block, hiding output the viewer may not see.
57+
58+
``accessible_project_ids`` is the set of projects the viewer can read. A
59+
block's persisted output is withheld unless every project that contributed
60+
to it is in that set, so it must be supplied by the caller.
61+
"""
62+
63+
def __init__(self, accessible_project_ids: set[int]) -> None:
64+
self.accessible_project_ids = accessible_project_ids
65+
66+
@override
67+
def get_attrs(
68+
self,
69+
item_list: Sequence[InvestigationBlock],
70+
user: User | RpcUser | AnonymousUser,
71+
**kwargs: Any,
72+
) -> MutableMapping[InvestigationBlock, dict[str, Any]]:
73+
dependencies: MutableMapping[int, list[str]] = defaultdict(list)
74+
for link in (
75+
InvestigationBlockDependency.objects.filter(block__in=item_list)
76+
.values_list("block_id", "depends_on_id")
77+
.order_by("id")
78+
):
79+
dependencies[link[0]].append(str(link[1]))
80+
81+
parameter_keys: MutableMapping[int, list[str]] = defaultdict(list)
82+
for block_id, key in (
83+
InvestigationBlockParameter.objects.filter(block__in=item_list)
84+
.values_list("block_id", "parameter__key")
85+
.order_by("parameter__position")
86+
):
87+
parameter_keys[block_id].append(key)
88+
89+
return {
90+
block: {
91+
"dependencies": dependencies[block.id],
92+
"parameter_keys": parameter_keys[block.id],
93+
}
94+
for block in item_list
95+
}
96+
97+
def _execution_project_ids(self, execution: InvestigationBlockExecution) -> set[int]:
98+
return {project.id for project in execution.data_projects.all()}
99+
100+
@override
101+
def serialize(
102+
self,
103+
obj: InvestigationBlock,
104+
attrs: Mapping[Any, Any],
105+
user: User | RpcUser | AnonymousUser,
106+
**kwargs: Any,
107+
) -> InvestigationBlockSerializerResponse:
108+
execution = obj.current_execution
109+
result_execution = obj.result_execution
110+
content_execution = obj.content_execution
111+
content_restricted = bool(
112+
obj.kind == InvestigationBlockKind.TEXT
113+
and content_execution is not None
114+
and not self._execution_project_ids(content_execution).issubset(
115+
self.accessible_project_ids
116+
)
117+
)
118+
if execution is None:
119+
output = None
120+
output_status = "notRun"
121+
else:
122+
visible_execution = (
123+
result_execution if obj.kind == InvestigationBlockKind.QUERY else execution
124+
)
125+
data_project_ids = (
126+
self._execution_project_ids(visible_execution)
127+
if visible_execution is not None
128+
else set()
129+
)
130+
if not data_project_ids.issubset(self.accessible_project_ids):
131+
output = None
132+
output_status = "restricted"
133+
else:
134+
output = visible_execution.result if visible_execution is not None else None
135+
output_status = (
136+
"available"
137+
if execution.status == InvestigationBlockExecutionStatus.COMPLETED
138+
else execution.status
139+
)
140+
if content_restricted:
141+
output = None
142+
output_status = "restricted"
143+
144+
content = obj.content
145+
generated_content = obj.generated_content
146+
if obj.kind == InvestigationBlockKind.TEXT and output_status == "restricted":
147+
content = ""
148+
generated_content = ""
149+
150+
return {
151+
"id": str(obj.id),
152+
"position": obj.position,
153+
"kind": obj.kind,
154+
"title": obj.title,
155+
"content": content,
156+
"generationPrompt": obj.prompt,
157+
"generatedContent": generated_content,
158+
"output": output,
159+
"outputStatus": output_status,
160+
"currentExecution": (
161+
{
162+
"id": str(execution.id),
163+
"status": execution.status,
164+
"executor": execution.executor,
165+
"schemaVersion": execution.result_schema_version,
166+
"startedAt": execution.started_at,
167+
"completedAt": execution.completed_at,
168+
"error": execution.error,
169+
}
170+
if execution is not None
171+
else None
172+
),
173+
"config": obj.config,
174+
"display": obj.display,
175+
"dependencies": attrs["dependencies"],
176+
"parameterKeys": attrs["parameter_keys"],
177+
"version": obj.version,
178+
"staleAt": obj.stale_at,
179+
"createdBy": str(obj.created_by_id) if obj.created_by_id is not None else None,
180+
"lastEditedBy": (
181+
str(obj.last_edited_by_id) if obj.last_edited_by_id is not None else None
182+
),
183+
}

0 commit comments

Comments
 (0)