Skip to content
Merged
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
120 changes: 120 additions & 0 deletions src/sentry/preprod/api/endpoints/preprod_artifact_rerun_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,126 @@ def post(self, request: Request) -> Response:
)


@internal_cell_silo_endpoint
class PreprodArtifactAdminBatchRerunAnalysisEndpoint(Endpoint):
owner = ApiOwner.EMERGE_TOOLS
permission_classes = (StaffPermission,)
publish_status = {
"POST": ApiPublishStatus.PRIVATE,
}

def post(self, request: Request) -> Response:
try:
data = orjson.loads(request.body)
except (orjson.JSONDecodeError, TypeError):
return Response({"detail": "Invalid JSON body"}, status=400)

raw_ids = data.get("artifact_ids", [])
if not isinstance(raw_ids, list) or not raw_ids:
return Response(
{"detail": "artifact_ids is required and must be a non-empty list"},
status=400,
)

try:
artifact_ids = list(dict.fromkeys(int(aid) for aid in raw_ids))
except (ValueError, TypeError):
return Response(
{"detail": "artifact_ids must be a list of integers"},
status=400,
)

if len(artifact_ids) > 100:
return Response(
{"detail": "Cannot rerun analysis for more than 100 artifacts at once"},
status=400,
)

artifacts = list(
PreprodArtifact.objects.select_related("project__organization").filter(
id__in=artifact_ids
)
)
artifacts_by_id = {a.id: a for a in artifacts}

missing_ids = set(artifact_ids) - set(artifacts_by_id.keys())
if missing_ids:
return Response(
{"detail": f"Artifacts not found: {sorted(missing_ids)}"},
status=404,
)

results: list[dict[str, object]] = []
for artifact_id in artifact_ids:
artifact = artifacts_by_id[artifact_id]
organization = artifact.project.organization

analytics.record(
PreprodArtifactApiRerunAnalysisEvent(
organization_id=organization.id,
project_id=artifact.project.id,
user_id=request.user.id,
artifact_id=str(artifact_id),
)
)

cleanup_stats = cleanup_old_metrics(artifact)
reset_artifact_data(artifact)

if features.has("organizations:launchpad-taskbroker-rollout", organization):
dispatched = dispatch_taskbroker(artifact.project.id, organization.id, artifact_id)
else:
try:
produce_preprod_artifact_to_kafka(
project_id=artifact.project.id,
organization_id=organization.id,
artifact_id=artifact_id,
requested_features=[
PreprodFeature.SIZE_ANALYSIS,
PreprodFeature.BUILD_DISTRIBUTION,
],
)
dispatched = True
except Exception:
logger.exception(
"preprod_artifact.admin_batch_rerun_analysis.dispatch_error",
extra={
"artifact_id": artifact_id,
"user_id": request.user.id,
"organization_id": organization.id,
"project_id": artifact.project.id,
},
)
dispatched = False

if not dispatched:
artifact.refresh_from_db()

result: dict[str, object] = {
"artifact_id": str(artifact_id),
"success": dispatched,
Comment thread
sentry[bot] marked this conversation as resolved.
"new_state": artifact.state,
Comment thread
NicoHinderling marked this conversation as resolved.
"cleanup_stats": asdict(cleanup_stats),
}
if not dispatched:
result["detail"] = "Cleanup completed but dispatch failed"
results.append(result)

if dispatched:
logger.info(
"preprod_artifact.admin_batch_rerun_analysis",
extra={
"artifact_id": artifact_id,
"user_id": request.user.id,
"organization_id": organization.id,
"project_id": artifact.project.id,
"cleanup_stats": asdict(cleanup_stats),
},
)

return Response({"results": results})


def cleanup_old_metrics(preprod_artifact: PreprodArtifact) -> CleanupStats:
"""Deletes old size metrics and comparisons associated with an artifact along with any associated files."""

Expand Down
6 changes: 6 additions & 0 deletions src/sentry/preprod/api/endpoints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .preprod_artifact_admin_info import PreprodArtifactAdminInfoEndpoint
from .preprod_artifact_approve import OrganizationPreprodArtifactApproveEndpoint
from .preprod_artifact_rerun_analysis import (
PreprodArtifactAdminBatchRerunAnalysisEndpoint,
PreprodArtifactAdminRerunAnalysisEndpoint,
PreprodArtifactRerunAnalysisEndpoint,
)
Expand Down Expand Up @@ -219,6 +220,11 @@
PreprodArtifactAdminRerunAnalysisEndpoint.as_view(),
name="sentry-admin-preprod-artifact-rerun-analysis",
),
re_path(
r"^preprod-artifact/batch-rerun-analysis/$",
PreprodArtifactAdminBatchRerunAnalysisEndpoint.as_view(),
name="sentry-admin-preprod-artifact-batch-rerun-analysis",
),
re_path(
r"^preprod-artifact/(?P<head_artifact_id>[^/]+)/info/$",
PreprodArtifactAdminInfoEndpoint.as_view(),
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/api/knownSentryApiUrls.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export type KnownSentryApiUrls =
| '/internal/packages/'
| '/internal/preprod-artifact/$headArtifactId/info/'
| '/internal/preprod-artifact/batch-delete/'
| '/internal/preprod-artifact/batch-rerun-analysis/'
| '/internal/preprod-artifact/rerun-analysis/'
| '/internal/project-config/'
| '/internal/projectkey-cell-mappings/'
Expand Down
43 changes: 27 additions & 16 deletions static/gsAdmin/views/launchpadAdminPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,35 @@ export function LaunchpadAdminPage() {

const {mutate: rerunAnalysis} = useMutation({
mutationFn: () => {
const ids = rerunArtifactId
.split(',')
.map(id => id.trim())
.filter(id => id);
return fetchMutation({
url: '/internal/preprod-artifact/rerun-analysis/',
url: '/internal/preprod-artifact/batch-rerun-analysis/',
method: 'POST',
data: {
preprod_artifact_id: rerunArtifactId,
},
options: {
host: region?.url,
},
data: {artifact_ids: ids},
options: {host: region?.url},
});
},
onSuccess: () => {
addSuccessMessage(
`Analysis rerun initiated successfully for artifact: ${rerunArtifactId}`
);
onSuccess: (data: any) => {
const results = data?.results ?? [];
const succeeded = results.filter((r: any) => r.success);
const failed = results.filter((r: any) => !r.success);
if (failed.length > 0) {
addErrorMessage(
`Failed to dispatch ${failed.length} artifact${failed.length > 1 ? 's' : ''}: ${failed.map((r: any) => r.artifact_id).join(', ')}`
);
}
if (succeeded.length > 0) {
addSuccessMessage(
`Analysis rerun initiated for ${succeeded.length} artifact${succeeded.length > 1 ? 's' : ''}`
);
}
setRerunArtifactId('');
},
onError: () => {
addErrorMessage(`Failed to rerun analysis for artifact: ${rerunArtifactId}`);
addErrorMessage('Failed to rerun analysis');
},
});

Expand Down Expand Up @@ -370,19 +380,20 @@ export function LaunchpadAdminPage() {
<form onSubmit={handleRerunSubmit}>
<Container background="secondary" border="primary" radius="md" padding="lg">
<Flex direction="column" gap="md">
<Heading as="h3">Rerun Analysis</Heading>
<Heading as="h3">Batch Rerun Analyses</Heading>
<Text as="p" variant="muted">
Rerun analysis for a specific preprod artifact.
Rerun analysis for one or more preprod artifacts using comma-separated
IDs.
</Text>
<label htmlFor="rerunArtifactId">
<Text bold>Preprod Artifact ID:</Text>
<Text bold>Preprod Artifact ID (comma-separated):</Text>
</label>
<StyledInput
type="text"
name="rerunArtifactId"
value={rerunArtifactId}
onChange={e => setRerunArtifactId(e.target.value)}
placeholder="Enter preprod artifact ID"
placeholder="e.g., 123, 456, 789"
/>
<Button
priority="primary"
Expand Down
Loading