Skip to content

chore(launchpad): Return artifact URL upon creation #97558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 11, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from sentry.models.orgauthtoken import is_org_auth_token_auth, update_org_auth_token_last_used
from sentry.preprod.analytics import PreprodArtifactApiAssembleEvent
from sentry.preprod.tasks import assemble_preprod_artifact, create_preprod_artifact
from sentry.preprod.url_utils import get_preprod_artifact_url
from sentry.tasks.assemble import ChunkFileState
from sentry.types.ratelimit import RateLimit, RateLimitCategory

Expand Down Expand Up @@ -183,6 +184,12 @@ def post(self, request: Request, project) -> Response:
if is_org_auth_token_auth(request.auth):
update_org_auth_token_last_used(request.auth, [project.id])

artifact_url = get_preprod_artifact_url(project.organization_id, artifact_id)

return Response(
{"state": ChunkFileState.OK, "missingChunks": [], "artifactId": artifact_id}
{
"state": ChunkFileState.CREATED,
"missingChunks": [],
"artifactUrl": artifact_url,
}
)
13 changes: 13 additions & 0 deletions src/sentry/preprod/url_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

from sentry.models.organization import Organization


def get_preprod_artifact_url(organization_id: int, artifact_id: str) -> str:
"""
Build a region/customer-domain aware absolute URL for the preprod artifact UI.
"""
organization: Organization = Organization.objects.get_from_cache(id=organization_id)

path = f"/organizations/{organization.slug}/preprod/internal/{artifact_id}"
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Cross-Silo Query Violation in URL Construction

The get_preprod_artifact_url function queries the Organization model (Organization.objects.get_from_cache) from a region-silo endpoint to build a URL (using organization.slug and absolute_url). As Organization is a control-silo model, this violates silo boundaries, risking cross-silo database access errors (e.g., SiloLimitError) in multi-silo deployments. The URL should instead be constructed using data already available (e.g., project.organization) or a silo-safe service/mapping (e.g., OrganizationMapping or an organization_service) to avoid querying the control database from the region.

Fix in Cursor Fix in Web

return organization.absolute_url(path)
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,10 @@ def test_assemble_basic(
HTTP_AUTHORIZATION=f"Bearer {self.token.token}",
)
assert response.status_code == 200, response.content
assert response.data["state"] == ChunkFileState.OK
assert response.data["state"] == ChunkFileState.CREATED
assert set(response.data["missingChunks"]) == set()
assert response.data["artifactId"] == artifact_id
expected_url = f"/organizations/{self.organization.slug}/preprod/internal/{artifact_id}"
assert expected_url in response.data["artifactUrl"]

mock_create_preprod_artifact.assert_called_once_with(
org_id=self.organization.id,
Expand Down Expand Up @@ -440,9 +441,10 @@ def test_assemble_with_metadata(
HTTP_AUTHORIZATION=f"Bearer {self.token.token}",
)
assert response.status_code == 200, response.content
assert response.data["state"] == ChunkFileState.OK
assert response.data["state"] == ChunkFileState.CREATED
assert set(response.data["missingChunks"]) == set()
assert response.data["artifactId"] == artifact_id
expected_url = f"/organizations/{self.organization.slug}/preprod/internal/{artifact_id}"
assert expected_url in response.data["artifactUrl"]

mock_create_preprod_artifact.assert_called_once_with(
org_id=self.organization.id,
Expand Down Expand Up @@ -500,7 +502,7 @@ def test_assemble_with_missing_chunks(self) -> None:
)

assert response.status_code == 200, response.content
assert response.data["state"] == ChunkFileState.OK
assert response.data["state"] == ChunkFileState.CREATED

def test_assemble_response(self) -> None:
content = b"test response content"
Expand All @@ -518,7 +520,7 @@ def test_assemble_response(self) -> None:
)

assert response.status_code == 200, response.content
assert response.data["state"] == ChunkFileState.OK
assert response.data["state"] == ChunkFileState.CREATED

def test_assemble_with_pending_deletion_project(self) -> None:
self.project.status = ObjectStatus.PENDING_DELETION
Expand Down Expand Up @@ -637,7 +639,7 @@ def test_check_existing_assembly_status(self) -> None:

# Even if assembly status exists, endpoint doesn't check it
set_assemble_status(
AssembleTask.PREPROD_ARTIFACT, self.project.id, checksum, ChunkFileState.OK
AssembleTask.PREPROD_ARTIFACT, self.project.id, checksum, ChunkFileState.CREATED
)

response = self.client.post(
Expand Down Expand Up @@ -671,7 +673,7 @@ def test_integration_task_sets_status_api_can_read_it(self) -> None:

# Even if task sets status, this endpoint doesn't read it
set_assemble_status(
AssembleTask.PREPROD_ARTIFACT, self.project.id, total_checksum, ChunkFileState.OK
AssembleTask.PREPROD_ARTIFACT, self.project.id, total_checksum, ChunkFileState.CREATED
)

response = self.client.post(
Expand Down
Loading