Skip to content

Conversation

@rbro112
Copy link
Member

@rbro112 rbro112 commented Oct 24, 2025

Adds a new preprod /files/images/<image_id> endpoint which allows our frontend to download images from an ID with objectstore. First to be used with app icons coming soon.

Tested fully E2E locally and all works well. To work, this is reliant on landing getsentry/launchpad#430 once we get a published version of the objectstore client, but since nobody will be consuming this endpoint until we land the stacked PR (#102118) this is safe to go ahead and merge.

Copy link
Member Author

rbro112 commented Oct 24, 2025

@rbro112 rbro112 changed the title Add app-icon download endpoint feat(preprod): Add app-icon download endpoint Oct 28, 2025
@rbro112 rbro112 marked this pull request as ready for review October 28, 2025 19:35
@rbro112 rbro112 requested a review from a team as a code owner October 28, 2025 19:35
@rbro112 rbro112 changed the title feat(preprod): Add app-icon download endpoint feat(preprod): Add preprod images download endpoint Oct 30, 2025
@rbro112 rbro112 force-pushed the ryan/add_api_to_serve_preprod_app_icons branch from c5561d3 to 6835a33 Compare October 30, 2025 18:28
@analytics.eventclass("preprod_artifact.api.size_analysis_download")
class PreprodArtifactApiSizeAnalysisDownloadEvent(analytics.Event):
@analytics.eventclass("preprod_artifact.api.get_build_details")
class PreprodArtifactApiGetBuildDetailsEvent(analytics.Event):
Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't change these, I just reordered a few of these to match the .register order below

@codecov
Copy link

codecov bot commented Oct 30, 2025

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
29948 2 29946 251
View the top 2 failed test(s) by shortest run time
tests.sentry.preprod.api.endpoints.test_preprod_artifact_image.ProjectPreprodArtifactImageTest::test_successful_image_retrieval_heic
Stack Traces | 3.33s run time
#x1B[1m#x1B[.../api/endpoints/test_preprod_artifact_image.py#x1B[0m:81: in test_successful_image_retrieval_heic
    client.put(BytesIO(heic_data), key=f"{self.org.id}/{self.project.id}/test-image-123")
#x1B[1m#x1B[31m.venv/lib/python3.13.../site-packages/objectstore_client/client.py#x1B[0m:263: in put
    response = self._pool.request(
#x1B[1m#x1B[31m.venv/lib/python3.13....../site-packages/urllib3/_request_methods.py#x1B[0m:144: in request
    return self.request_encode_body(
#x1B[1m#x1B[31m.venv/lib/python3.13....../site-packages/urllib3/_request_methods.py#x1B[0m:279: in request_encode_body
    return self.urlopen(method, url, **extra_kw)
#x1B[1m#x1B[31m.venv/lib/python3.13....../site-packages/urllib3/connectionpool.py#x1B[0m:873: in urlopen
    return self.urlopen(
#x1B[1m#x1B[31m.venv/lib/python3.13....../site-packages/urllib3/connectionpool.py#x1B[0m:763: in urlopen
    body_pos = set_file_position(body, body_pos)
#x1B[1m#x1B[31m.venv/lib/python3.13.../urllib3/util/request.py#x1B[0m:142: in set_file_position
    rewind_body(body, pos)
#x1B[1m#x1B[31m.venv/lib/python3.13.../urllib3/util/request.py#x1B[0m:179: in rewind_body
    raise ValueError(
#x1B[1m#x1B[31mE   ValueError: body_pos must be of type integer, instead it was <class 'int'>.#x1B[0m
tests.snuba.api.endpoints.test_organization_events_stats_ourlogs.OrganizationEventsStatsOurlogsEndpointTest::test_homepage_query
Stack Traces | 14.7s run time
#x1B[1m#x1B[31mtests/conftest.py#x1B[0m:56: in unclosed_files
    assert _open_files() == fds
#x1B[1m#x1B[31mE   AssertionError: assert frozenset({'/.../cacert.pem'}) == frozenset({'/dev/null'})#x1B[0m
#x1B[1m#x1B[31mE     #x1B[0m
#x1B[1m#x1B[31mE     Extra items in the left set:#x1B[0m
#x1B[1m#x1B[31mE     '.../sentry/sentry/.venv/lib/python3.13....../site-packages/certifi/cacert.pem'#x1B[0m
#x1B[1m#x1B[31mE     #x1B[0m
#x1B[1m#x1B[31mE     Full diff:#x1B[0m
#x1B[1m#x1B[31mE       frozenset({#x1B[0m
#x1B[1m#x1B[31mE           '/dev/null',#x1B[0m
#x1B[1m#x1B[31mE     +     '.../sentry/sentry/.venv/lib/python3.13....../site-packages/certifi/cacert.pem',#x1B[0m
#x1B[1m#x1B[31mE       })#x1B[0m

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

logger = logging.getLogger(__name__)


def detect_image_content_type(image_data: bytes) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

From Jan's comment below it sounds like eventually this can be set on write and we wouldn't have to do it here on read, but this seems ok for now to unblock testing

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, before merge I'll leverage a custom metadata field and we can use the first class one as soon as Jan & team land that.

Copy link
Member Author

Choose a reason for hiding this comment

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

platform: Platform | None = None
is_installable: bool
build_configuration: str | None = None
app_icon_id: str | None = None
Copy link
Member Author

Choose a reason for hiding this comment

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

This is related to app icon, not the generic download endpoint, but should be fine to merge as the frontend does not yet use this until #102118 lands

return _ATTACHMENTS_CLIENT.session(_ATTACHMENTS_USECASE, org=org, project=project)


def get_preprod_session(org: int, project: int) -> Session:
Copy link
Member Author

Choose a reason for hiding this comment

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

@jan-auer saw this practice already used for attachments, is this how you all would recommend using a global session?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah. Although you might as well create a dict mapping Usecase -> Client | None to store the usecase and the client, as I can see us adding more of these over time.

@noahsmartin noahsmartin force-pushed the ryan/add_api_to_serve_preprod_app_icons branch from 4f0e1e9 to 24d9e1d Compare November 20, 2025 21:05
"image_id": image_id,
},
)
return HttpResponse({"error": "Internal server error"}, status=500)
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Invalid dictionary passed to HttpResponse without JSON serialization

The error response passes a dictionary directly to HttpResponse instead of serializing it to JSON. HttpResponse expects string or bytes content, and will convert the dict to its Python string representation (e.g., "{'error': '...'}") instead of valid JSON. Use json.dumps() to serialize the dictionary first.

Fix in Cursor Fix in Web

"image_id": image_id,
},
)
return HttpResponse({"error": "Internal server error"}, status=500)
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Invalid dictionary passed to HttpResponse without JSON serialization

The error response passes a dictionary directly to HttpResponse instead of serializing it to JSON. HttpResponse expects string or bytes content, and will convert the dict to its Python string representation (e.g., "{'error': '...'}") instead of valid JSON. Use json.dumps() to serialize the dictionary first, or use Response from rest_framework.

Fix in Cursor Fix in Web

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Nov 20, 2025
@github-actions
Copy link
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

@noahsmartin noahsmartin force-pushed the ryan/add_api_to_serve_preprod_app_icons branch from 5d540ea to f7a9713 Compare November 25, 2025 14:23
"image_id": image_id,
},
)
return HttpResponse({"error": "Internal server error"}, status=500)
Copy link

Choose a reason for hiding this comment

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

Bug: The HttpResponse constructor at line 55 receives a dictionary, which it does not handle correctly, leading to a TypeError or malformed response.
Severity: HIGH | Confidence: High

🔍 Detailed Analysis

The HttpResponse constructor at line 55 is called with a Python dictionary {'error': 'Internal server error'} as its first argument. Django's HttpResponse expects content to be bytes or a string, not a dictionary. This will cause a TypeError when an exception occurs in the try block (line 39), preventing a graceful 500 error response and instead crashing the server.

💡 Suggested Fix

Serialize the dictionary to JSON: HttpResponse(json.dumps({'error': 'Internal server error'}), content_type='application/json', status=500) or return plain text: HttpResponse('Internal server error', status=500).

🤖 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/sentry/preprod/api/endpoints/project_preprod_artifact_image.py#L55

Potential issue: The `HttpResponse` constructor at line 55 is called with a Python
dictionary `{'error': 'Internal server error'}` as its first argument. Django's
`HttpResponse` expects content to be bytes or a string, not a dictionary. This will
cause a `TypeError` when an exception occurs in the `try` block (line 39), preventing a
graceful 500 error response and instead crashing the server.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 3502411

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants