Skip to content
12 changes: 10 additions & 2 deletions src/sentry/preprod/api/endpoints/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,16 @@ def on_results(artifacts: list[PreprodArtifact]) -> list[dict[str, Any]]:
try:
queryset = queryset_for_query(query, organization)
queryset = queryset.select_related(
"project", "build_configuration", "commit_comparison", "mobile_app_info"
).prefetch_related("preprodartifactsizemetrics_set")
"project",
"build_configuration",
"commit_comparison",
"mobile_app_info",
"preprodsnapshotmetrics",
).prefetch_related(
"preprodartifactsizemetrics_set",
"preprodsnapshotmetrics__snapshot_comparisons_head_metrics",
"preprodcomparisonapproval_set",
)
queryset = queryset.filter(date_added__gte=cutoff)
if start:
queryset = queryset.filter(date_added__gte=start)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
get_download_count_for_artifact,
is_installable_artifact,
)
from sentry.preprod.models import Platform, PreprodArtifact, PreprodArtifactSizeMetrics
from sentry.preprod.models import (
Platform,
PreprodArtifact,
PreprodArtifactSizeMetrics,
PreprodComparisonApproval,
)
from sentry.preprod.snapshots.models import PreprodSnapshotComparison, PreprodSnapshotMetrics
from sentry.preprod.vcs.status_checks.size.tasks import StatusCheckErrorType

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -83,6 +89,17 @@ class PostedStatusChecks(BaseModel):
size: StatusCheckResult | None = None


class SnapshotComparisonInfo(BaseModel):
image_count: int
comparison_state: Literal["pending", "processing", "success", "failed"] | None = None
comparison_error_message: str | None = None
images_added: int = 0
images_removed: int = 0
images_changed: int = 0
images_unchanged: int = 0
approval_status: Literal["approved", "requires_approval"] | None = None


class SizeInfoSizeMetric(BaseModel):
metrics_artifact_type: PreprodArtifactSizeMetrics.MetricsArtifactType
install_size_bytes: int
Expand Down Expand Up @@ -147,6 +164,7 @@ class BuildDetailsApiResponse(BaseModel):
posted_status_checks: PostedStatusChecks | None = None
base_artifact_id: str | None = None
base_build_info: BuildDetailsAppInfo | None = None
snapshot_comparison_info: SnapshotComparisonInfo | None = None


def create_build_details_app_info(artifact: PreprodArtifact) -> BuildDetailsAppInfo:
Expand Down Expand Up @@ -262,6 +280,61 @@ def to_size_info(
raise ValueError(f"Unknown SizeAnalysisState {main_metric.state}")


def to_snapshot_comparison_info(head_artifact: PreprodArtifact) -> SnapshotComparisonInfo | None:
try:
snapshot_metrics = head_artifact.preprodsnapshotmetrics
except PreprodSnapshotMetrics.DoesNotExist:
return None

comparison_state = None
comparison_error_message = None
images_added = 0
images_removed = 0
images_changed = 0
images_unchanged = 0

comparisons = sorted(
snapshot_metrics.snapshot_comparisons_head_metrics.all(),
key=lambda c: c.id,
reverse=True,
)
comparison = comparisons[0] if comparisons else None
if comparison:
comparison_state = PreprodSnapshotComparison.State(comparison.state).name.lower()
if comparison.state == PreprodSnapshotComparison.State.SUCCESS:
images_added = comparison.images_added
images_removed = comparison.images_removed
images_changed = comparison.images_changed
images_unchanged = comparison.images_unchanged
elif comparison.state == PreprodSnapshotComparison.State.FAILED:
comparison_error_message = comparison.error_message

approval_status = None
# REJECTED is no longer used; all non-APPROVED statuses are treated as requires_approval
approvals = [
a
for a in head_artifact.preprodcomparisonapproval_set.all()
if a.preprod_feature_type == PreprodComparisonApproval.FeatureType.SNAPSHOTS
]
approvals.sort(key=lambda a: a.id, reverse=True)
if approvals:
if approvals[0].approval_status == PreprodComparisonApproval.ApprovalStatus.APPROVED:
approval_status = "approved"
else:
approval_status = "requires_approval"

return SnapshotComparisonInfo(
image_count=snapshot_metrics.image_count,
comparison_state=comparison_state,
comparison_error_message=comparison_error_message,
images_added=images_added,
images_removed=images_removed,
images_changed=images_changed,
images_unchanged=images_unchanged,
approval_status=approval_status,
)


def transform_preprod_artifact_to_build_details(
artifact: PreprodArtifact,
) -> BuildDetailsApiResponse:
Expand Down Expand Up @@ -315,6 +388,8 @@ def transform_preprod_artifact_to_build_details(

posted_status_checks = _parse_posted_status_checks(artifact)

snapshot_comparison_info = to_snapshot_comparison_info(artifact)

return BuildDetailsApiResponse(
id=artifact.id,
state=artifact.state,
Expand All @@ -327,6 +402,7 @@ def transform_preprod_artifact_to_build_details(
posted_status_checks=posted_status_checks,
base_artifact_id=base_artifact.id if base_artifact else None,
base_build_info=base_build_info,
snapshot_comparison_info=snapshot_comparison_info,
)


Expand Down
3 changes: 3 additions & 0 deletions static/app/components/preprod/preprodBuildsDisplay.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum PreprodBuildsDisplay {
SIZE = 'size',
DISTRIBUTION = 'distribution',
SNAPSHOT = 'snapshot',
}

export function getPreprodBuildsDisplay(
Expand All @@ -13,6 +14,8 @@ export function getPreprodBuildsDisplay(
switch (display) {
case PreprodBuildsDisplay.DISTRIBUTION:
return PreprodBuildsDisplay.DISTRIBUTION;
case PreprodBuildsDisplay.SNAPSHOT:
return PreprodBuildsDisplay.SNAPSHOT;
case PreprodBuildsDisplay.SIZE:
default:
return PreprodBuildsDisplay.SIZE;
Expand Down
35 changes: 21 additions & 14 deletions static/app/components/preprod/preprodBuildsSearchControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ interface PreprodBuildsSearchControlsProps {
* are shown.
*/
allowedKeys?: string[];
/**
* Hide the display mode toggle
*/
hideDisplayToggle?: boolean;
/**
* Called on every keystroke (for controlled input with debounce)
*/
Expand All @@ -54,6 +58,7 @@ export function PreprodBuildsSearchControls({
display,
projects,
allowedKeys = MOBILE_BUILDS_ALLOWED_KEYS,
hideDisplayToggle,
onChange,
onSearch,
onDisplayChange,
Expand All @@ -74,20 +79,22 @@ export function PreprodBuildsSearchControls({
projects={projects}
/>
</Container>
<Container maxWidth="200px">
<CompactSelect
options={displaySelectOptions}
value={display}
onChange={option => onDisplayChange(option.value)}
trigger={triggerProps => (
<OverlayTrigger.Button
{...triggerProps}
prefix={t('Display')}
style={{width: '100%', zIndex: 1}}
/>
)}
/>
</Container>
{!hideDisplayToggle && (
<Container maxWidth="200px">
<CompactSelect
options={displaySelectOptions}
value={display}
onChange={option => onDisplayChange(option.value)}
trigger={triggerProps => (
<OverlayTrigger.Button
{...triggerProps}
prefix={t('Display')}
style={{width: '100%', zIndex: 1}}
/>
)}
/>
</Container>
)}
</Flex>
);
}
Loading
Loading