Skip to content

Commit a04cd9f

Browse files
NicoHinderlingclaudegetsantry[bot]
authored
feat(preprod): Upgrade admin panel's "rerun analysis" action to support batch (up to 100) (#112481)
Closes EME-926 Add `PreprodArtifactAdminBatchRerunAnalysisEndpoint` at `/internal/preprod-artifact/batch-rerun-analysis/` so the admin launchpad can rerun analysis for multiple artifacts in a single request instead of one at a time. The endpoint accepts `{"artifact_ids": [1, 2, 3]}`, deduplicates IDs, enforces a 100-item batch limit (matching the batch delete endpoint), and returns per-artifact results with success/failure status and cleanup stats. On dispatch failure, the response includes a detail message noting that cleanup completed but dispatch failed. The admin UI card is updated from "Rerun Analysis" (single ID) to "Batch Rerun Analyses" (comma-separated IDs) and now surfaces partial failures with per-artifact error toasts. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent dc175d1 commit a04cd9f

File tree

4 files changed

+154
-16
lines changed

4 files changed

+154
-16
lines changed

src/sentry/preprod/api/endpoints/preprod_artifact_rerun_analysis.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,126 @@ def post(self, request: Request) -> Response:
237237
)
238238

239239

240+
@internal_cell_silo_endpoint
241+
class PreprodArtifactAdminBatchRerunAnalysisEndpoint(Endpoint):
242+
owner = ApiOwner.EMERGE_TOOLS
243+
permission_classes = (StaffPermission,)
244+
publish_status = {
245+
"POST": ApiPublishStatus.PRIVATE,
246+
}
247+
248+
def post(self, request: Request) -> Response:
249+
try:
250+
data = orjson.loads(request.body)
251+
except (orjson.JSONDecodeError, TypeError):
252+
return Response({"detail": "Invalid JSON body"}, status=400)
253+
254+
raw_ids = data.get("artifact_ids", [])
255+
if not isinstance(raw_ids, list) or not raw_ids:
256+
return Response(
257+
{"detail": "artifact_ids is required and must be a non-empty list"},
258+
status=400,
259+
)
260+
261+
try:
262+
artifact_ids = list(dict.fromkeys(int(aid) for aid in raw_ids))
263+
except (ValueError, TypeError):
264+
return Response(
265+
{"detail": "artifact_ids must be a list of integers"},
266+
status=400,
267+
)
268+
269+
if len(artifact_ids) > 100:
270+
return Response(
271+
{"detail": "Cannot rerun analysis for more than 100 artifacts at once"},
272+
status=400,
273+
)
274+
275+
artifacts = list(
276+
PreprodArtifact.objects.select_related("project__organization").filter(
277+
id__in=artifact_ids
278+
)
279+
)
280+
artifacts_by_id = {a.id: a for a in artifacts}
281+
282+
missing_ids = set(artifact_ids) - set(artifacts_by_id.keys())
283+
if missing_ids:
284+
return Response(
285+
{"detail": f"Artifacts not found: {sorted(missing_ids)}"},
286+
status=404,
287+
)
288+
289+
results: list[dict[str, object]] = []
290+
for artifact_id in artifact_ids:
291+
artifact = artifacts_by_id[artifact_id]
292+
organization = artifact.project.organization
293+
294+
analytics.record(
295+
PreprodArtifactApiRerunAnalysisEvent(
296+
organization_id=organization.id,
297+
project_id=artifact.project.id,
298+
user_id=request.user.id,
299+
artifact_id=str(artifact_id),
300+
)
301+
)
302+
303+
cleanup_stats = cleanup_old_metrics(artifact)
304+
reset_artifact_data(artifact)
305+
306+
if features.has("organizations:launchpad-taskbroker-rollout", organization):
307+
dispatched = dispatch_taskbroker(artifact.project.id, organization.id, artifact_id)
308+
else:
309+
try:
310+
produce_preprod_artifact_to_kafka(
311+
project_id=artifact.project.id,
312+
organization_id=organization.id,
313+
artifact_id=artifact_id,
314+
requested_features=[
315+
PreprodFeature.SIZE_ANALYSIS,
316+
PreprodFeature.BUILD_DISTRIBUTION,
317+
],
318+
)
319+
dispatched = True
320+
except Exception:
321+
logger.exception(
322+
"preprod_artifact.admin_batch_rerun_analysis.dispatch_error",
323+
extra={
324+
"artifact_id": artifact_id,
325+
"user_id": request.user.id,
326+
"organization_id": organization.id,
327+
"project_id": artifact.project.id,
328+
},
329+
)
330+
dispatched = False
331+
332+
if not dispatched:
333+
artifact.refresh_from_db()
334+
335+
result: dict[str, object] = {
336+
"artifact_id": str(artifact_id),
337+
"success": dispatched,
338+
"new_state": artifact.state,
339+
"cleanup_stats": asdict(cleanup_stats),
340+
}
341+
if not dispatched:
342+
result["detail"] = "Cleanup completed but dispatch failed"
343+
results.append(result)
344+
345+
if dispatched:
346+
logger.info(
347+
"preprod_artifact.admin_batch_rerun_analysis",
348+
extra={
349+
"artifact_id": artifact_id,
350+
"user_id": request.user.id,
351+
"organization_id": organization.id,
352+
"project_id": artifact.project.id,
353+
"cleanup_stats": asdict(cleanup_stats),
354+
},
355+
)
356+
357+
return Response({"results": results})
358+
359+
240360
def cleanup_old_metrics(preprod_artifact: PreprodArtifact) -> CleanupStats:
241361
"""Deletes old size metrics and comparisons associated with an artifact along with any associated files."""
242362

src/sentry/preprod/api/endpoints/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .preprod_artifact_admin_info import PreprodArtifactAdminInfoEndpoint
2424
from .preprod_artifact_approve import OrganizationPreprodArtifactApproveEndpoint
2525
from .preprod_artifact_rerun_analysis import (
26+
PreprodArtifactAdminBatchRerunAnalysisEndpoint,
2627
PreprodArtifactAdminRerunAnalysisEndpoint,
2728
PreprodArtifactRerunAnalysisEndpoint,
2829
)
@@ -219,6 +220,11 @@
219220
PreprodArtifactAdminRerunAnalysisEndpoint.as_view(),
220221
name="sentry-admin-preprod-artifact-rerun-analysis",
221222
),
223+
re_path(
224+
r"^preprod-artifact/batch-rerun-analysis/$",
225+
PreprodArtifactAdminBatchRerunAnalysisEndpoint.as_view(),
226+
name="sentry-admin-preprod-artifact-batch-rerun-analysis",
227+
),
222228
re_path(
223229
r"^preprod-artifact/(?P<head_artifact_id>[^/]+)/info/$",
224230
PreprodArtifactAdminInfoEndpoint.as_view(),

static/app/utils/api/knownSentryApiUrls.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export type KnownSentryApiUrls =
114114
| '/internal/packages/'
115115
| '/internal/preprod-artifact/$headArtifactId/info/'
116116
| '/internal/preprod-artifact/batch-delete/'
117+
| '/internal/preprod-artifact/batch-rerun-analysis/'
117118
| '/internal/preprod-artifact/rerun-analysis/'
118119
| '/internal/project-config/'
119120
| '/internal/projectkey-cell-mappings/'

static/gsAdmin/views/launchpadAdminPage.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,35 @@ export function LaunchpadAdminPage() {
3333

3434
const {mutate: rerunAnalysis} = useMutation({
3535
mutationFn: () => {
36+
const ids = rerunArtifactId
37+
.split(',')
38+
.map(id => id.trim())
39+
.filter(id => id);
3640
return fetchMutation({
37-
url: '/internal/preprod-artifact/rerun-analysis/',
41+
url: '/internal/preprod-artifact/batch-rerun-analysis/',
3842
method: 'POST',
39-
data: {
40-
preprod_artifact_id: rerunArtifactId,
41-
},
42-
options: {
43-
host: region?.url,
44-
},
43+
data: {artifact_ids: ids},
44+
options: {host: region?.url},
4545
});
4646
},
47-
onSuccess: () => {
48-
addSuccessMessage(
49-
`Analysis rerun initiated successfully for artifact: ${rerunArtifactId}`
50-
);
47+
onSuccess: (data: any) => {
48+
const results = data?.results ?? [];
49+
const succeeded = results.filter((r: any) => r.success);
50+
const failed = results.filter((r: any) => !r.success);
51+
if (failed.length > 0) {
52+
addErrorMessage(
53+
`Failed to dispatch ${failed.length} artifact${failed.length > 1 ? 's' : ''}: ${failed.map((r: any) => r.artifact_id).join(', ')}`
54+
);
55+
}
56+
if (succeeded.length > 0) {
57+
addSuccessMessage(
58+
`Analysis rerun initiated for ${succeeded.length} artifact${succeeded.length > 1 ? 's' : ''}`
59+
);
60+
}
5161
setRerunArtifactId('');
5262
},
5363
onError: () => {
54-
addErrorMessage(`Failed to rerun analysis for artifact: ${rerunArtifactId}`);
64+
addErrorMessage('Failed to rerun analysis');
5565
},
5666
});
5767

@@ -370,19 +380,20 @@ export function LaunchpadAdminPage() {
370380
<form onSubmit={handleRerunSubmit}>
371381
<Container background="secondary" border="primary" radius="md" padding="lg">
372382
<Flex direction="column" gap="md">
373-
<Heading as="h3">Rerun Analysis</Heading>
383+
<Heading as="h3">Batch Rerun Analyses</Heading>
374384
<Text as="p" variant="muted">
375-
Rerun analysis for a specific preprod artifact.
385+
Rerun analysis for one or more preprod artifacts using comma-separated
386+
IDs.
376387
</Text>
377388
<label htmlFor="rerunArtifactId">
378-
<Text bold>Preprod Artifact ID:</Text>
389+
<Text bold>Preprod Artifact ID (comma-separated):</Text>
379390
</label>
380391
<StyledInput
381392
type="text"
382393
name="rerunArtifactId"
383394
value={rerunArtifactId}
384395
onChange={e => setRerunArtifactId(e.target.value)}
385-
placeholder="Enter preprod artifact ID"
396+
placeholder="e.g., 123, 456, 789"
386397
/>
387398
<Button
388399
priority="primary"

0 commit comments

Comments
 (0)