diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 3a1e2384..ebbc4e94 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -34,6 +34,7 @@ from launchpad.artifacts.artifact_factory import ArtifactFactory from launchpad.constants import ( ArtifactType, + InstallableAppErrorCode, PreprodFeature, ProcessingErrorCode, ProcessingErrorMessage, @@ -331,9 +332,16 @@ def _do_distribution( with apk.raw_file() as f: self._sentry_client.upload_installable_app(organization_id, project_id, artifact_id, f) else: - # TODO(EME-422): Should call _update_artifact_error here once we - # support setting errors just for build. - logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id} (project: {project_id}, org: {organization_id})") + logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id}: unsupported artifact type") + try: + self._sentry_client.update_distribution_error( + org=organization_id, + artifact_id=artifact_id, + error_code=InstallableAppErrorCode.PROCESSING_ERROR.value, + error_message="unsupported artifact type", + ) + except Exception: + logger.exception(f"Failed to update distribution error for artifact {artifact_id}") def _do_size( self, diff --git a/src/launchpad/constants.py b/src/launchpad/constants.py index 4eb84c0f..8f449417 100644 --- a/src/launchpad/constants.py +++ b/src/launchpad/constants.py @@ -32,6 +32,14 @@ class PreprodFeature(Enum): BUILD_DISTRIBUTION = "build_distribution" +# Matches InstallableApp.ErrorCode in sentry +class InstallableAppErrorCode(Enum): + UNKNOWN = 0 + NO_QUOTA = 1 + SKIPPED = 2 + PROCESSING_ERROR = 3 + + # Health check threshold - consider unhealthy if file not touched in 60 seconds HEALTHCHECK_MAX_AGE_SECONDS = 60.0 diff --git a/src/launchpad/sentry_client.py b/src/launchpad/sentry_client.py index e2581ecc..24d03872 100644 --- a/src/launchpad/sentry_client.py +++ b/src/launchpad/sentry_client.py @@ -252,6 +252,24 @@ def update_artifact(self, org: str, project: str, artifact_id: str, data: Dict[s endpoint = f"/api/0/internal/{org}/{project}/files/preprodartifacts/{artifact_id}/update/" return self._make_json_request("PUT", endpoint, UpdateResponse, data=data) + def update_distribution_error(self, org: str, artifact_id: str, error_code: int, error_message: str) -> None: + """Report distribution error via the dedicated distribution endpoint.""" + endpoint = f"/api/0/organizations/{org}/preprodartifacts/{artifact_id}/distribution/" + url = self._build_url(endpoint) + body = json.dumps({"error_code": error_code, "error_message": error_message}).encode("utf-8") + + logger.debug(f"PUT {url}") + response = self.session.request( + method="PUT", + url=url, + data=body, + auth=self.auth, + timeout=30, + ) + + if response.status_code != 200: + raise SentryClientError(response=response) + def upload_size_analysis_file( self, org: str, diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index bdd18b2e..1d668100 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -6,7 +6,9 @@ ) from launchpad.artifact_processor import ArtifactProcessor +from launchpad.artifacts.artifact import Artifact from launchpad.constants import ( + InstallableAppErrorCode, ProcessingErrorCode, ProcessingErrorMessage, ) @@ -137,6 +139,25 @@ def test_processing_error_message_enum_values(self): assert ProcessingErrorMessage.SIZE_ANALYSIS_FAILED.value == "Failed to perform size analysis" assert ProcessingErrorMessage.UNKNOWN_ERROR.value == "An unknown error occurred" + def test_do_distribution_unknown_artifact_type_reports_error(self): + mock_sentry_client = Mock(spec=SentryClient) + mock_sentry_client.update_distribution_error.return_value = None + self.processor._sentry_client = mock_sentry_client + + unknown_artifact = Mock(spec=Artifact) + mock_info = Mock() + + self.processor._do_distribution( + "test-org-id", "test-project-id", "test-artifact-id", unknown_artifact, mock_info + ) + + mock_sentry_client.update_distribution_error.assert_called_once_with( + org="test-org-id", + artifact_id="test-artifact-id", + error_code=InstallableAppErrorCode.PROCESSING_ERROR.value, + error_message="unsupported artifact type", + ) + class TestArtifactProcessorMessageHandling: """Test message processing functionality in ArtifactProcessor."""