Skip to content

Commit ade06fd

Browse files
ref(preprod): Add endpoint to decide whether to reveal mobile builds or snapshot tabs in releases
1 parent bd79c6d commit ade06fd

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from __future__ import annotations
2+
3+
from rest_framework.request import Request
4+
from rest_framework.response import Response
5+
6+
from sentry import features
7+
from sentry.api.api_owners import ApiOwner
8+
from sentry.api.api_publish_status import ApiPublishStatus
9+
from sentry.api.base import cell_silo_endpoint
10+
from sentry.api.bases.organization import NoProjects, OrganizationEndpoint
11+
from sentry.models.organization import Organization
12+
from sentry.preprod.models import PreprodArtifact, PreprodArtifactSizeMetrics
13+
from sentry.preprod.snapshots.models import PreprodSnapshotMetrics
14+
15+
VALID_TYPES = {"size", "snapshots"}
16+
17+
18+
@cell_silo_endpoint
19+
class OrganizationPreprodHasDataEndpoint(OrganizationEndpoint):
20+
owner = ApiOwner.EMERGE_TOOLS
21+
publish_status = {
22+
"GET": ApiPublishStatus.EXPERIMENTAL,
23+
}
24+
25+
def get(self, request: Request, organization: Organization) -> Response:
26+
if not features.has(
27+
"organizations:preprod-frontend-routes", organization, actor=request.user
28+
):
29+
return Response(
30+
{"detail": "Feature organizations:preprod-frontend-routes is not enabled."},
31+
status=403,
32+
)
33+
34+
requested_types = set(request.GET.getlist("type"))
35+
valid_requested = requested_types & VALID_TYPES
36+
if not valid_requested:
37+
return Response(
38+
{"detail": f"type must include at least one of: {', '.join(sorted(VALID_TYPES))}"},
39+
status=400,
40+
)
41+
42+
try:
43+
params = self.get_filter_params(request, organization, date_filter_optional=True)
44+
except NoProjects:
45+
return Response({t: False for t in valid_requested})
46+
47+
artifact_qs = PreprodArtifact.objects.filter(
48+
project_id__in=params["project_id"],
49+
)
50+
51+
if params.get("start"):
52+
artifact_qs = artifact_qs.filter(date_added__gte=params["start"])
53+
if params.get("end"):
54+
artifact_qs = artifact_qs.filter(date_added__lte=params["end"])
55+
56+
result = {}
57+
58+
if "size" in valid_requested:
59+
result["size"] = PreprodArtifactSizeMetrics.objects.filter(
60+
preprod_artifact__in=artifact_qs
61+
).exists()
62+
63+
if "snapshots" in valid_requested:
64+
result["snapshots"] = PreprodSnapshotMetrics.objects.filter(
65+
preprod_artifact__in=artifact_qs
66+
).exists()
67+
68+
return Response(result)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
OrganizationPreprodSnapshotEndpoint,
3333
ProjectPreprodSnapshotEndpoint,
3434
)
35+
from .preprod_has_data import OrganizationPreprodHasDataEndpoint
3536
from .preprod_snapshot_recompare import PreprodSnapshotRecompareEndpoint
3637
from .project_installable_preprod_artifact_download import (
3738
ProjectInstallablePreprodArtifactDownloadEndpoint,
@@ -212,6 +213,11 @@
212213
ProjectPreprodDistributionEndpoint.as_view(),
213214
name="sentry-api-0-organization-preprod-artifact-distribution",
214215
),
216+
re_path(
217+
r"^(?P<organization_id_or_slug>[^/]+)/preprod/has-data/$",
218+
OrganizationPreprodHasDataEndpoint.as_view(),
219+
name="sentry-api-0-organization-preprod-has-data",
220+
),
215221
]
216222

217223
preprod_internal_urlpatterns = [
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import annotations
2+
3+
from datetime import timedelta
4+
5+
from django.utils import timezone
6+
7+
from sentry.preprod.models import PreprodArtifact
8+
from sentry.preprod.snapshots.models import PreprodSnapshotMetrics
9+
from sentry.testutils.cases import APITestCase
10+
11+
12+
class OrganizationPreprodHasDataEndpointTest(APITestCase):
13+
endpoint = "sentry-api-0-organization-preprod-has-data"
14+
15+
def setUp(self) -> None:
16+
super().setUp()
17+
self.login_as(user=self.user)
18+
19+
def test_returns_400_when_no_type_param(self) -> None:
20+
with self.feature("organizations:preprod-frontend-routes"):
21+
response = self.get_response(self.organization.slug)
22+
assert response.status_code == 400
23+
24+
def test_returns_400_when_invalid_type_param(self) -> None:
25+
with self.feature("organizations:preprod-frontend-routes"):
26+
response = self.get_response(self.organization.slug, type="invalid")
27+
assert response.status_code == 400
28+
29+
def test_returns_403_when_feature_flag_off(self) -> None:
30+
response = self.get_response(self.organization.slug, type="size")
31+
assert response.status_code == 403
32+
33+
def test_size_false_when_no_artifacts(self) -> None:
34+
with self.feature("organizations:preprod-frontend-routes"):
35+
response = self.get_success_response(self.organization.slug, type="size")
36+
assert response.data == {"size": False}
37+
38+
def test_snapshots_false_when_no_artifacts(self) -> None:
39+
with self.feature("organizations:preprod-frontend-routes"):
40+
response = self.get_success_response(self.organization.slug, type="snapshots")
41+
assert response.data == {"snapshots": False}
42+
43+
def test_size_true_when_size_metrics_exist(self) -> None:
44+
artifact = self.create_preprod_artifact(
45+
project=self.project,
46+
state=PreprodArtifact.ArtifactState.PROCESSED,
47+
)
48+
self.create_preprod_artifact_size_metrics(artifact)
49+
50+
with self.feature("organizations:preprod-frontend-routes"):
51+
response = self.get_success_response(self.organization.slug, type="size")
52+
assert response.data == {"size": True}
53+
54+
def test_snapshots_true_when_snapshot_metrics_exist(self) -> None:
55+
artifact = self.create_preprod_artifact(
56+
project=self.project,
57+
state=PreprodArtifact.ArtifactState.PROCESSED,
58+
)
59+
PreprodSnapshotMetrics.objects.create(preprod_artifact=artifact, image_count=5)
60+
61+
with self.feature("organizations:preprod-frontend-routes"):
62+
response = self.get_success_response(self.organization.slug, type="snapshots")
63+
assert response.data == {"snapshots": True}
64+
65+
def test_both_types_returned_together(self) -> None:
66+
artifact = self.create_preprod_artifact(
67+
project=self.project,
68+
state=PreprodArtifact.ArtifactState.PROCESSED,
69+
)
70+
self.create_preprod_artifact_size_metrics(artifact)
71+
PreprodSnapshotMetrics.objects.create(preprod_artifact=artifact, image_count=5)
72+
73+
with self.feature("organizations:preprod-frontend-routes"):
74+
response = self.get_success_response(self.organization.slug, type=["size", "snapshots"])
75+
assert response.data == {"size": True, "snapshots": True}
76+
77+
def test_respects_time_range(self) -> None:
78+
now = timezone.now()
79+
artifact = self.create_preprod_artifact(
80+
project=self.project,
81+
state=PreprodArtifact.ArtifactState.PROCESSED,
82+
date_added=now - timedelta(days=30),
83+
)
84+
self.create_preprod_artifact_size_metrics(artifact)
85+
86+
with self.feature("organizations:preprod-frontend-routes"):
87+
response = self.get_success_response(
88+
self.organization.slug,
89+
type="size",
90+
start=(now - timedelta(days=1)).isoformat(),
91+
end=now.isoformat(),
92+
)
93+
assert response.data == {"size": False}
94+
95+
def test_respects_project_filter(self) -> None:
96+
other_project = self.create_project(organization=self.organization)
97+
artifact = self.create_preprod_artifact(
98+
project=other_project,
99+
state=PreprodArtifact.ArtifactState.PROCESSED,
100+
)
101+
self.create_preprod_artifact_size_metrics(artifact)
102+
103+
with self.feature("organizations:preprod-frontend-routes"):
104+
response = self.get_success_response(
105+
self.organization.slug,
106+
type="size",
107+
project=[self.project.id],
108+
)
109+
assert response.data == {"size": False}
110+
111+
def test_no_cross_org_data_leak(self) -> None:
112+
other_org = self.create_organization(owner=self.create_user())
113+
other_project = self.create_project(organization=other_org)
114+
artifact = self.create_preprod_artifact(
115+
project=other_project,
116+
state=PreprodArtifact.ArtifactState.PROCESSED,
117+
)
118+
self.create_preprod_artifact_size_metrics(artifact)
119+
120+
with self.feature("organizations:preprod-frontend-routes"):
121+
response = self.get_success_response(self.organization.slug, type="size")
122+
assert response.data == {"size": False}

0 commit comments

Comments
 (0)