fix(artifacts): Report distribution errors for unsupported and skipped builds (EME-422)#567
Conversation
Add a dedicated endpoint (`PUT
.../files/preprodartifacts/{id}/distribution/`) for launchpad to report
distribution processing errors back to the monolith. This mirrors the
existing `ProjectPreprodSizeEndpoint` pattern.
When launchpad encounters a distribution failure (unsupported artifact
type, invalid code signature, simulator build), it needs a way to set
`installable_app_error_code` and `installable_app_error_message` on the
artifact so the frontend can display the reason. Previously, the only
option was the general `update` endpoint which marks the entire artifact
as failed — but distribution errors shouldn't affect the artifact's
overall state.
Follow-up to #109062. The launchpad side that calls this endpoint is in
getsentry/launchpad#567.
Refs EME-842, EME-422
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Add a dedicated endpoint (`PUT
.../files/preprodartifacts/{id}/distribution/`) for launchpad to report
distribution processing errors back to the monolith. This mirrors the
existing `ProjectPreprodSizeEndpoint` pattern.
When launchpad encounters a distribution failure (unsupported artifact
type, invalid code signature, simulator build), it needs a way to set
`installable_app_error_code` and `installable_app_error_message` on the
artifact so the frontend can display the reason. Previously, the only
option was the general `update` endpoint which marks the entire artifact
as failed — but distribution errors shouldn't affect the artifact's
overall state.
Follow-up to #109062. The launchpad side that calls this endpoint is in
getsentry/launchpad#567.
Refs EME-842, EME-422
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
5eaf5cd to
fe8817e
Compare
📲 Install BuildsAndroid
|
Sentry Build Distribution
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unused
project_idparameter creates monitoring blind spot- Added StatsD metrics emission to _update_distribution_skip following the pattern of sibling methods to track distribution skip events with project_id, organization_id, and skip_reason tags.
Or push these changes by commenting:
@cursor push cbb5a57a4d
Preview (cbb5a57a4d)
diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py
--- a/src/launchpad/artifact_processor.py
+++ b/src/launchpad/artifact_processor.py
@@ -433,6 +433,15 @@
skip_reason: str,
) -> None:
"""Report distribution skip via the dedicated distribution endpoint."""
+ self._statsd.increment(
+ "artifact.distribution.skipped",
+ tags=[
+ f"skip_reason:{skip_reason}",
+ f"project_id:{project_id}",
+ f"organization_id:{organization_id}",
+ ],
+ )
+
try:
self._sentry_client.update_distribution_error(
org=organization_id,5911136 to
10c7af7
Compare
…on (EME-422) Replace the TODO in _do_distribution() with a call to _update_artifact_error() so that unsupported artifact types are properly reported back to Sentry instead of silently failing.
When a ZippedXCArchive has an invalid code signature or is a simulator build, _do_distribution now reports the specific error via _update_artifact_error instead of silently doing nothing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The update_artifact endpoint silently ignored distribution_state and distribution_skip_reason fields. Use the dedicated distribution endpoint that accepts error_code and error_message instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…EME-422) Unsupported artifact types are genuine errors, not intentional skips. Report them with PROCESSING_ERROR instead of SKIPPED so they surface correctly to users. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ns (EME-422) Catch all exceptions (not just SentryClientError) when reporting distribution errors/skips. Network errors like ConnectionError and Timeout from requests aren't subclasses of SentryClientError, so they would propagate uncaught and crash the pipeline. These are best-effort notifications that should never block artifact processing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert the iOS-specific changes that reported skip reasons for invalid code signatures and simulator builds. Keep the unsupported artifact type error reporting via the distribution endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When Sentry requests BUILD_DISTRIBUTION for an iOS artifact that has an invalid code signature or is a simulator build, Launchpad was silently doing nothing. This left the artifact stuck in a "processing" state in Sentry with no timeout to recover. Report SKIPPED via the distribution endpoint so Sentry can resolve the state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10c7af7 to
ec4a615
Compare
| error_code=InstallableAppErrorCode.SKIPPED.value, | ||
| error_message=skip_reason, | ||
| ) | ||
| except Exception: |
There was a problem hiding this comment.
Maybe this should more closely mirror _update_artifact_error above - that way it could cover both the skip and the error cases - and report the skipped / distro errors to _statsd
There was a problem hiding this comment.
good point, added statsd and made the method look more similar to the above
| ) | ||
|
|
||
| if response.status_code != 200: | ||
| raise SentryClientError(response=response) |
There was a problem hiding this comment.
Can this not use _make_json_request?
| 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: |
There was a problem hiding this comment.
I would be tempted to make this like update_artifact above (both naming and implementation wise)
There was a problem hiding this comment.
Good idea, updated. I had to change the _update_resonse_model method so that we don't return a ResponseModel since this is fire and forget.
Replace _report_distribution_skip with _update_distribution_error that mirrors _update_artifact_error — covers both skip and error cases with statsd metrics, structured logging, and SentryClientError handling. Also refactor SentryClient: rename update_distribution_error to update_distribution with a generic data dict, and make _make_json_request model param optional so it can handle fire-and-forget PUT requests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| except SentryClientError: | ||
| logger.exception(f"Failed to update distribution error for artifact {artifact_id}") |
There was a problem hiding this comment.
Bug: Network errors (ConnectionError, Timeout) from session.request() are not caught, causing them to propagate and be misreported as hard processing failures.
Severity: MEDIUM
Suggested Fix
Widen the exception handler in _update_distribution_error to catch all exceptions: change except SentryClientError: to except Exception:. This matches the PR's stated design intent and the comment in commit 3aa27629 which explicitly addressed this issue.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/launchpad/artifact_processor.py#L482-L483
Potential issue: In `_update_distribution_error`, only `SentryClientError` is caught
(line 482), but `session.request()` in `_make_json_request` can directly raise
network-level exceptions like `ConnectionError` or `Timeout` that are NOT subclasses of
`SentryClientError`. These propagate through `_update_distribution_error` →
`_do_distribution` → `process_artifact` → `process_message`, where the outer `except
Exception` logs them as "Processing failed" and increments `artifact.processing.failed`.
The PR's stated intent (as noted in commit `3aa27629`) is that these notifications are
"best-effort" and "should never block artifact processing", but the current
implementation violates this intent. A network transient error during skip reporting
would be misattributed as a hard failure.
Did we get this right? 👍 / 👎 to inform future reviews.
…l (EME-422) Keep _make_json_request signature strict by introducing an EmptyResponse model for endpoints that don't return meaningful data, rather than making the model parameter optional. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| def update_distribution(self, org: str, artifact_id: str, data: Dict[str, Any]) -> EmptyResponse: | ||
| """Update preprod artifact distribution.""" | ||
| endpoint = f"/api/0/organizations/{org}/preprodartifacts/{artifact_id}/distribution/" | ||
| return self._make_json_request("PUT", endpoint, EmptyResponse, data=data) |
There was a problem hiding this comment.
Bug: _make_json_request only handles HTTP 200, but the distribution endpoint may return 204 No Content, causing SentryClientError on success.
Severity: HIGH
Suggested Fix
Either: (1) update _make_json_request to accept both 200 and 204 success codes (returning an EmptyResponse() for 204), or (2) confirm the Sentry distribution endpoint always returns 200 with a JSON body. If it returns 204, handle it before calling model.model_validate(j).
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/launchpad/sentry_client.py#L259-L262
Potential issue: The `_make_json_request` method (used by `update_distribution`) only
treats HTTP 200 as success — any other status code triggers `raise
SentryClientError(response=response)`. The `EmptyResponse` model was introduced
specifically because the distribution endpoint doesn't return meaningful data,
suggesting its response body may be empty. A REST PUT endpoint that doesn't return a
body conventionally returns `204 No Content`. If Sentry's
`/api/0/organizations/{org}/preprodartifacts/{artifact_id}/distribution/` returns 204,
`_make_json_request` raises `SentryClientError`, which is caught-and-logged in
`_update_distribution_error`, meaning the skip/error notification silently fails every
time — defeating the purpose of this fix. Even if a 200 is returned but with an empty
body, `response.json()` would raise `JSONDecodeError`, also resulting in
`SentryClientError`.
…sponse (EME-422) Replace EmptyResponse with DistributionResponse that models the actual backend response (artifactId). Avoids misleading naming that could lead to silent failures if the endpoint response changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Summary
When Sentry requests
BUILD_DISTRIBUTION, Launchpad now reports back viathe distribution endpoint in all cases where it can't fulfill the request:
PROCESSING_ERRORviaupdate_distribution_errorSKIPPEDwith reasoninvalid_signatureSKIPPEDwith reasonsimulatorPreviously, these cases either silently did nothing or only logged an error,
leaving the artifact stuck in a "processing" state in Sentry with no timeout
to recover.
Fixes EME-422
🤖 Generated with Claude Code