Skip to content
Open
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
9 changes: 8 additions & 1 deletion src/sentry/preprod/api/endpoints/builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
from sentry.preprod.api.models.project_preprod_build_details_models import (
transform_preprod_artifact_to_build_details,
)
from sentry.preprod.models import PreprodArtifact, PreprodArtifactQuerySet
from sentry.preprod.models import (
PreprodArtifact,
PreprodArtifactQuerySet,
PreprodArtifactSizeMetrics,
)

ERR_FEATURE_REQUIRED = "Feature {} is not enabled for the organization."

Expand Down Expand Up @@ -135,6 +139,9 @@ def queryset_for_query(
queryset = queryset.annotate_installable()
queryset = queryset.annotate_main_size_metrics()
queryset = queryset.annotate_platform_name()
queryset = queryset.exclude(
preprodartifactsizemetrics__state=PreprodArtifactSizeMetrics.SizeAnalysisState.NOT_RAN,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exclude filter leaks into quota/processing pipeline logic

High Severity

The NOT_RAN exclude added to queryset_for_query unintentionally affects the processing pipeline. queryset_for_query is also called by artifact_matches_query (used in quotas.pyshould_run_feature), meaning artifacts with any NOT_RAN size metric will fail quota/filter checks for all features, not just display. In project_preprod_artifact_update.py, when size analysis is skipped, a NOT_RAN metric is created before should_run_distribution is called—so distribution gets incorrectly skipped too. Similarly, the rerun-analysis endpoint would fail to re-process artifacts that already have NOT_RAN metrics.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK for now. I think the AI is slightly wrong, but this change is basically only for presentation layer list cases, so it should not affect any actual running of logic


search_filters = parse_search_query(query, config=search_config, get_field_type=get_field_type)
return apply_filters(queryset, search_filters, organization)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
transform_preprod_artifact_to_build_details,
)
from sentry.preprod.api.validators import PreprodListBuildsValidator
from sentry.preprod.models import PreprodArtifact
from sentry.preprod.models import PreprodArtifact, PreprodArtifactSizeMetrics
from sentry.preprod.utils import parse_release_version

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -91,6 +91,9 @@ def get(self, request: Request, organization: Organization) -> Response:
)
.prefetch_related("preprodartifactsizemetrics_set")
.filter(project_id__in=project_ids)
.exclude(
preprodartifactsizemetrics__state=PreprodArtifactSizeMetrics.SizeAnalysisState.NOT_RAN,
)
)

if start and end:
Expand Down
45 changes: 45 additions & 0 deletions tests/sentry/preprod/api/endpoints/test_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.urls import reverse
from django.utils.functional import cached_property

from sentry.preprod.models import PreprodArtifactSizeMetrics
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.datetime import before_now
from sentry.testutils.helpers.features import with_feature
Expand Down Expand Up @@ -845,6 +846,50 @@ def test_free_text_search_with_structured_filter(self) -> None:
assert len(data) == 1
assert data[0]["app_info"]["app_id"] == "com.example.android"

@with_feature("organizations:preprod-frontend-routes")
def test_excludes_builds_with_not_ran_size_metrics(self) -> None:
artifact = self.create_preprod_artifact(app_id="not_ran.app")
self.create_preprod_artifact_size_metrics(
artifact, state=PreprodArtifactSizeMetrics.SizeAnalysisState.NOT_RAN
)
response = self._request({})
self._assert_is_successful(response)
assert len(response.json()) == 0

@with_feature("organizations:preprod-frontend-routes")
def test_includes_builds_with_completed_size_metrics(self) -> None:
artifact = self.create_preprod_artifact(app_id="completed.app")
self.create_preprod_artifact_size_metrics(
artifact, state=PreprodArtifactSizeMetrics.SizeAnalysisState.COMPLETED
)
response = self._request({})
self._assert_is_successful(response)
assert len(response.json()) == 1
assert response.json()[0]["app_info"]["app_id"] == "completed.app"

@with_feature("organizations:preprod-frontend-routes")
def test_includes_builds_with_no_size_metrics(self) -> None:
self.create_preprod_artifact(app_id="no_metrics.app")
response = self._request({})
self._assert_is_successful(response)
assert len(response.json()) == 1
assert response.json()[0]["app_info"]["app_id"] == "no_metrics.app"

@with_feature("organizations:preprod-frontend-routes")
def test_excludes_build_with_mixed_metrics_including_not_ran(self) -> None:
artifact = self.create_preprod_artifact(app_id="mixed.app")
self.create_preprod_artifact_size_metrics(
artifact, state=PreprodArtifactSizeMetrics.SizeAnalysisState.COMPLETED
)
self.create_preprod_artifact_size_metrics(
artifact,
state=PreprodArtifactSizeMetrics.SizeAnalysisState.NOT_RAN,
metrics_type=PreprodArtifactSizeMetrics.MetricsArtifactType.WATCH_ARTIFACT,
)
response = self._request({})
self._assert_is_successful(response)
assert len(response.json()) == 0


class QuerysetForQueryTest(APITestCase):
"""Tests for the queryset_for_query, artifact_in_queryset, and artifact_matches_query functions."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from sentry import analytics
from sentry.preprod.analytics import PreprodArtifactApiListBuildsEvent
from sentry.preprod.models import PreprodArtifact
from sentry.preprod.models import PreprodArtifact, PreprodArtifactSizeMetrics
from sentry.testutils.cases import APITestCase


Expand Down Expand Up @@ -742,3 +742,43 @@ def test_list_builds_filter_project_and_other_criteria(self) -> None:
builds = response.json()["builds"]
assert len(builds) == 1
assert builds[0]["app_info"]["app_id"] == "com.example.app4"

def test_list_builds_excludes_not_ran_size_analysis(self) -> None:
self.create_preprod_artifact_size_metrics(
self.artifact1, state=PreprodArtifactSizeMetrics.SizeAnalysisState.NOT_RAN
)
response = self.client.get(
f"{self._get_url()}?project={self.project.id}&project={self.project2.id}",
format="json",
HTTP_AUTHORIZATION=f"Bearer {self.api_token.token}",
)
assert response.status_code == 200
builds = response.json()["builds"]
assert len(builds) == 3
app_ids = {b["app_info"]["app_id"] for b in builds}
assert "com.example.app" not in app_ids

def test_list_builds_includes_completed_size_analysis(self) -> None:
self.create_preprod_artifact_size_metrics(
self.artifact1, state=PreprodArtifactSizeMetrics.SizeAnalysisState.COMPLETED
)
response = self.client.get(
f"{self._get_url()}?project={self.project.id}&project={self.project2.id}",
format="json",
HTTP_AUTHORIZATION=f"Bearer {self.api_token.token}",
)
assert response.status_code == 200
builds = response.json()["builds"]
assert len(builds) == 4
app_ids = {b["app_info"]["app_id"] for b in builds}
assert "com.example.app" in app_ids

def test_list_builds_includes_no_size_metrics(self) -> None:
response = self.client.get(
f"{self._get_url()}?project={self.project.id}&project={self.project2.id}",
format="json",
HTTP_AUTHORIZATION=f"Bearer {self.api_token.token}",
)
assert response.status_code == 200
builds = response.json()["builds"]
assert len(builds) == 4
Loading