From 54451d2499790840c859a97d4d42ddaeebafc950 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 23 Apr 2026 15:56:23 +0200 Subject: [PATCH 1/4] feat(artifacts): Emit granular installable app error codes (EME-883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sentry's InstallableAppErrorCode enum was extended with granular values for the specific distribution-failure reasons launchpad produces (INVALID_CODE_SIGNATURE, SIMULATOR_BUILD, UNSUPPORTED_ARTIFACT_TYPE). Mirror those values in the launchpad-side enum and emit them directly from _do_distribution instead of packing the reason into the free-form error_message string on top of SKIPPED / PROCESSING_ERROR. Depends on getsentry/sentry#113440 — that PR must be deployed first so Sentry's PUT endpoint accepts the new codes. --- src/launchpad/artifact_processor.py | 6 ++--- src/launchpad/constants.py | 5 ++++ .../unit/artifacts/test_artifact_processor.py | 24 +++++++++---------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index a6403e56..2f8affe5 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -334,13 +334,13 @@ def _do_distribution( if not apple_info.is_code_signature_valid: logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: invalid code signature") self._update_distribution_error( - organization_id, artifact_id, InstallableAppErrorCode.SKIPPED, "invalid_signature" + organization_id, artifact_id, InstallableAppErrorCode.INVALID_CODE_SIGNATURE, "" ) return if apple_info.is_simulator: logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: simulator build") self._update_distribution_error( - organization_id, artifact_id, InstallableAppErrorCode.SKIPPED, "simulator" + organization_id, artifact_id, InstallableAppErrorCode.SIMULATOR_BUILD, "" ) return with tempfile.TemporaryDirectory() as temp_dir_str: @@ -368,7 +368,7 @@ def _do_distribution( else: logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id}: unsupported artifact type") self._update_distribution_error( - organization_id, artifact_id, InstallableAppErrorCode.PROCESSING_ERROR, "unsupported artifact type" + organization_id, artifact_id, InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE, "" ) def _do_size( diff --git a/src/launchpad/constants.py b/src/launchpad/constants.py index 7b53d05a..cf1fcc1a 100644 --- a/src/launchpad/constants.py +++ b/src/launchpad/constants.py @@ -35,6 +35,11 @@ class InstallableAppErrorCode(Enum): NO_QUOTA = 1 SKIPPED = 2 PROCESSING_ERROR = 3 + DISTRIBUTION_DISABLED = 4 + DISTRIBUTION_FILTERED = 5 + INVALID_CODE_SIGNATURE = 6 + SIMULATOR_BUILD = 7 + UNSUPPORTED_ARTIFACT_TYPE = 8 # Health check threshold - consider unhealthy if file not touched in 60 seconds diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index 376717b8..3259e757 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -155,15 +155,15 @@ def test_do_distribution_unknown_artifact_type_reports_error(self): org="test-org-id", artifact_id="test-artifact-id", data={ - "error_code": InstallableAppErrorCode.PROCESSING_ERROR.value, - "error_message": "unsupported artifact type", + "error_code": InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE.value, + "error_message": "", }, ) mock_statsd.increment.assert_called_once_with( "distribution.processing.error", tags=[ - f"error_code:{InstallableAppErrorCode.PROCESSING_ERROR.value}", - "error_message:unsupported artifact type", + f"error_code:{InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE.value}", + "error_message:", "organization_id:test-org-id", ], ) @@ -186,15 +186,15 @@ def test_do_distribution_invalid_code_signature_reports_skip(self): org="test-org-id", artifact_id="test-artifact-id", data={ - "error_code": InstallableAppErrorCode.SKIPPED.value, - "error_message": "invalid_signature", + "error_code": InstallableAppErrorCode.INVALID_CODE_SIGNATURE.value, + "error_message": "", }, ) mock_statsd.increment.assert_called_once_with( "distribution.processing.error", tags=[ - f"error_code:{InstallableAppErrorCode.SKIPPED.value}", - "error_message:invalid_signature", + f"error_code:{InstallableAppErrorCode.INVALID_CODE_SIGNATURE.value}", + "error_message:", "organization_id:test-org-id", ], ) @@ -218,15 +218,15 @@ def test_do_distribution_simulator_build_reports_skip(self): org="test-org-id", artifact_id="test-artifact-id", data={ - "error_code": InstallableAppErrorCode.SKIPPED.value, - "error_message": "simulator", + "error_code": InstallableAppErrorCode.SIMULATOR_BUILD.value, + "error_message": "", }, ) mock_statsd.increment.assert_called_once_with( "distribution.processing.error", tags=[ - f"error_code:{InstallableAppErrorCode.SKIPPED.value}", - "error_message:simulator", + f"error_code:{InstallableAppErrorCode.SIMULATOR_BUILD.value}", + "error_message:", "organization_id:test-org-id", ], ) From a96cba5ca05b8c3857c0ccf46c51ddebf713a39a Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 23 Apr 2026 16:02:40 +0200 Subject: [PATCH 2/4] feat(artifacts): Send human-readable distribution error messages Replace empty error_message strings with a human-readable sentence at each emission site. Sentry renders these verbatim on the install page as the description under the error title (which is derived from error_code). Keeps copy next to the condition it describes rather than centralising it in Sentry and losing per-site context. --- src/launchpad/artifact_processor.py | 15 ++++++++++++--- tests/unit/artifacts/test_artifact_processor.py | 12 ++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 2f8affe5..b74269ec 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -334,13 +334,19 @@ def _do_distribution( if not apple_info.is_code_signature_valid: logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: invalid code signature") self._update_distribution_error( - organization_id, artifact_id, InstallableAppErrorCode.INVALID_CODE_SIGNATURE, "" + organization_id, + artifact_id, + InstallableAppErrorCode.INVALID_CODE_SIGNATURE, + "The build's code signature could not be verified.", ) return if apple_info.is_simulator: logger.warning(f"BUILD_DISTRIBUTION skipped for {artifact_id}: simulator build") self._update_distribution_error( - organization_id, artifact_id, InstallableAppErrorCode.SIMULATOR_BUILD, "" + organization_id, + artifact_id, + InstallableAppErrorCode.SIMULATOR_BUILD, + "Simulator builds cannot be distributed.", ) return with tempfile.TemporaryDirectory() as temp_dir_str: @@ -368,7 +374,10 @@ def _do_distribution( else: logger.error(f"BUILD_DISTRIBUTION failed for {artifact_id}: unsupported artifact type") self._update_distribution_error( - organization_id, artifact_id, InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE, "" + organization_id, + artifact_id, + InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE, + "This artifact type is not supported for distribution.", ) def _do_size( diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index 3259e757..8e300bd2 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -156,14 +156,14 @@ def test_do_distribution_unknown_artifact_type_reports_error(self): artifact_id="test-artifact-id", data={ "error_code": InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE.value, - "error_message": "", + "error_message": "This artifact type is not supported for distribution.", }, ) mock_statsd.increment.assert_called_once_with( "distribution.processing.error", tags=[ f"error_code:{InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE.value}", - "error_message:", + "error_message:This artifact type is not supported for distribution.", "organization_id:test-org-id", ], ) @@ -187,14 +187,14 @@ def test_do_distribution_invalid_code_signature_reports_skip(self): artifact_id="test-artifact-id", data={ "error_code": InstallableAppErrorCode.INVALID_CODE_SIGNATURE.value, - "error_message": "", + "error_message": "The build's code signature could not be verified.", }, ) mock_statsd.increment.assert_called_once_with( "distribution.processing.error", tags=[ f"error_code:{InstallableAppErrorCode.INVALID_CODE_SIGNATURE.value}", - "error_message:", + "error_message:The build's code signature could not be verified.", "organization_id:test-org-id", ], ) @@ -219,14 +219,14 @@ def test_do_distribution_simulator_build_reports_skip(self): artifact_id="test-artifact-id", data={ "error_code": InstallableAppErrorCode.SIMULATOR_BUILD.value, - "error_message": "", + "error_message": "Simulator builds cannot be distributed.", }, ) mock_statsd.increment.assert_called_once_with( "distribution.processing.error", tags=[ f"error_code:{InstallableAppErrorCode.SIMULATOR_BUILD.value}", - "error_message:", + "error_message:Simulator builds cannot be distributed.", "organization_id:test-org-id", ], ) From 988133fc1d63c144b2bb4bb6aadc031479cd504e Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 24 Apr 2026 15:53:04 +0200 Subject: [PATCH 3/4] ref(artifacts): Drop error_message statsd tag and stale comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit error_code already covers the dimension you'd pivot on in dashboards, and error_message is currently 1:1 mapped to error_code so the tag only duplicates information. Keeping it sets a trap: the day someone interpolates a variable (artifact ID, version, path) into a message, metric cardinality explodes silently. The log line and PUT body still carry the message for human context. Also drop two stale comments on the enum classes pointing at Sentry — the pointer was vague ("Django model") or used an old class name. --- src/launchpad/artifact_processor.py | 1 - src/launchpad/constants.py | 4 ---- tests/unit/artifacts/test_artifact_processor.py | 3 --- 3 files changed, 8 deletions(-) diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index b74269ec..9bfd791a 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -474,7 +474,6 @@ def _update_distribution_error( "distribution.processing.error", tags=[ f"error_code:{error_code.value}", - f"error_message:{error_message}", f"organization_id:{organization_id}", ], ) diff --git a/src/launchpad/constants.py b/src/launchpad/constants.py index cf1fcc1a..a479e1a8 100644 --- a/src/launchpad/constants.py +++ b/src/launchpad/constants.py @@ -3,10 +3,7 @@ from enum import Enum -# Error code constants (matching the Django model) class ProcessingErrorCode(Enum): - """Error codes for artifact processing (matching the Django model).""" - UNKNOWN = 0 UPLOAD_TIMEOUT = 1 ARTIFACT_PROCESSING_TIMEOUT = 2 @@ -29,7 +26,6 @@ class PreprodFeature(Enum): BUILD_DISTRIBUTION = "build_distribution" -# Matches InstallableApp.ErrorCode in sentry class InstallableAppErrorCode(Enum): UNKNOWN = 0 NO_QUOTA = 1 diff --git a/tests/unit/artifacts/test_artifact_processor.py b/tests/unit/artifacts/test_artifact_processor.py index 8e300bd2..4276d371 100644 --- a/tests/unit/artifacts/test_artifact_processor.py +++ b/tests/unit/artifacts/test_artifact_processor.py @@ -163,7 +163,6 @@ def test_do_distribution_unknown_artifact_type_reports_error(self): "distribution.processing.error", tags=[ f"error_code:{InstallableAppErrorCode.UNSUPPORTED_ARTIFACT_TYPE.value}", - "error_message:This artifact type is not supported for distribution.", "organization_id:test-org-id", ], ) @@ -194,7 +193,6 @@ def test_do_distribution_invalid_code_signature_reports_skip(self): "distribution.processing.error", tags=[ f"error_code:{InstallableAppErrorCode.INVALID_CODE_SIGNATURE.value}", - "error_message:The build's code signature could not be verified.", "organization_id:test-org-id", ], ) @@ -226,7 +224,6 @@ def test_do_distribution_simulator_build_reports_skip(self): "distribution.processing.error", tags=[ f"error_code:{InstallableAppErrorCode.SIMULATOR_BUILD.value}", - "error_message:Simulator builds cannot be distributed.", "organization_id:test-org-id", ], ) From 83eb09a4f4b651acab29e1e06f25189bfb6cbf61 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 24 Apr 2026 15:57:06 +0200 Subject: [PATCH 4/4] ref(constants): Restore enum sync comments with correct class names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Earlier I dropped these comments because the class names they pointed at were stale. The comments' load-bearing purpose — flagging that these enums mirror Sentry and a drift will fail pydantic validation on the distribution PUT — was worth keeping. Restore them with accurate references. --- src/launchpad/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/launchpad/constants.py b/src/launchpad/constants.py index a479e1a8..bbb8be1a 100644 --- a/src/launchpad/constants.py +++ b/src/launchpad/constants.py @@ -3,6 +3,7 @@ from enum import Enum +# Must stay in sync with PreprodArtifact.ErrorCode in sentry. class ProcessingErrorCode(Enum): UNKNOWN = 0 UPLOAD_TIMEOUT = 1 @@ -26,6 +27,9 @@ class PreprodFeature(Enum): BUILD_DISTRIBUTION = "build_distribution" +# Must stay in sync with PreprodArtifact.InstallableAppErrorCode in sentry. +# Adding a value here without adding it on the sentry side will make the +# distribution PUT 400 on pydantic validation. class InstallableAppErrorCode(Enum): UNKNOWN = 0 NO_QUOTA = 1