feat(preprod): Add snapshot image comparison task and endpoint logic#109151
feat(preprod): Add snapshot image comparison task and endpoint logic#109151NicoHinderling wants to merge 5 commits intocreate-compare-task-v1-part3from
Conversation
|
🚨 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 |
f04c5f3 to
2b4c3f4
Compare
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
2b4c3f4 to
e1d9787
Compare
eb716d6 to
6ced8f1
Compare
6ced8f1 to
66b9902
Compare
e1d9787 to
317a73a
Compare
2eb8356 to
9f93bed
Compare
43cc0fc to
03eceb6
Compare
1793aa1 to
2ea9010
Compare
b3074bd to
09db923
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable autofix in the Cursor dashboard.
fb22dd0 to
a223ea2
Compare
e4f097b to
8d97888
Compare
8d97888 to
4ae7db4
Compare
a223ea2 to
51a1671
Compare
4ae7db4 to
f3c5e2a
Compare
51a1671 to
8075590
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable autofix in the Cursor dashboard.
f3c5e2a to
d750ab3
Compare
8075590 to
e13638e
Compare
|
|
||
| safe_name = name.replace("\\", "/").strip("/") | ||
| stem = safe_name.rsplit(".", 1)[0] if "." in safe_name else safe_name | ||
| diff_path = safe_name.removesuffix(".png") + ".png" |
There was a problem hiding this comment.
Bug: The use of removesuffix(".png") incorrectly handles non-PNG image files, leading to malformed file paths for diff masks like image.jpg.png.
Severity: HIGH
Suggested Fix
Revert to the previous, more robust implementation of stripping the file extension, such as using safe_name.rsplit(".", 1)[0], to correctly handle any image file extension before appending .png.
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/snapshots/tasks.py#L340
Potential issue: The logic for generating the diff mask path was changed from
`safe_name.rsplit(".", 1)[0]` to `safe_name.removesuffix(".png")`. This new
implementation only handles files with a `.png` extension. Since the file upload process
does not validate or enforce a specific image format, a user can upload images with
other extensions like `.jpg` or `.jpeg`. In such cases, the new code will generate an
incorrect path, such as `image.jpg.png` instead of `image.png`. This causes the diff
mask to be stored with an incorrect key in object storage, and the path returned to the
frontend will be broken, making the diff image inaccessible.
There was a problem hiding this comment.
@rbro112 perhaps we can do non PNGs as a follow up task
There was a problem hiding this comment.
e13638e to
7c7e142
Compare
d750ab3 to
e1ec0e3
Compare
| result = response.dict() | ||
| result["org_id"] = str(project.organization_id) | ||
| result["project_id"] = str(project.id) | ||
| result["comparison_type"] = comparison_type | ||
| if comparison_state is not None: | ||
| result["comparison_state"] = comparison_state | ||
|
|
||
| return result | ||
|
|
||
| return self.paginate( | ||
| request=request, |
There was a problem hiding this comment.
Bug: The endpoint returns a paginated images list but includes unpaginated changed, added, and removed lists, leading to an inconsistent API response.
Severity: MEDIUM
Suggested Fix
Filter the changed, added, removed, and unchanged lists within the on_results callback to only include items that are present in the paginated images list. This will ensure all data in the response is consistent with the current page.
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/preprod_artifact_snapshot.py#L309-L337
Potential issue: In the `PreprodArtifactSnapshotEndpoint`, the pagination logic
incorrectly combines paginated and unpaginated data. The `self.paginate` method
correctly slices the `image_list` to create a paginated list of `images` for the current
page. However, the `on_results` callback, which receives this paginated list, constructs
the final API response using the full, unpaginated `changed`, `added`, `removed`, and
`unchanged` lists. These lists are calculated from the complete dataset before
pagination is applied. This results in an inconsistent API response where the `images`
field is paginated, but the change-categorized lists contain all items, not just those
relevant to the current page.
There was a problem hiding this comment.
ignore for now
7c7e142 to
cddf7ff
Compare
e1ec0e3 to
fa0d220
Compare
| SnapshotImageResponse( | ||
| key=img_data.get("base_hash", ""), | ||
| display_name=name, | ||
| image_file_name=name, | ||
| width=img_data.get("before_width", 0), | ||
| height=img_data.get("before_height", 0), | ||
| ) | ||
| ) | ||
| elif status == "unchanged": | ||
| if head_img: | ||
| unchanged.append(head_img) |
There was a problem hiding this comment.
Bug: The snapshot comparison endpoint silently ignores images with "errored" or "skipped" statuses, returning partial results without any indication of data loss to the user.
Severity: MEDIUM
Suggested Fix
Update the API response model and database model to track counts for errored and skipped images. The GET endpoint should populate these fields from the comparison manifest and return them to the frontend. This will ensure the user is aware when a comparison is incomplete.
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/preprod_artifact_snapshot.py#L241-L283
Potential issue: The snapshot comparison task can generate images with `"errored"` or
`"skipped"` statuses, for example, when an image fails to process or exceeds a diff
limit. However, the GET endpoint that serves these comparison results only handles
`"changed"`, `"added"`, `"removed"`, and `"unchanged"` statuses. Images with `"errored"`
or `"skipped"` statuses are silently ignored. This causes the API to return a success
status with partial data, giving the user a false impression that the comparison was
complete when data was actually lost.
There was a problem hiding this comment.
I will consolidate these two cases into one and then handle showing them on the frontend in a later PR
fa0d220 to
eab1cfe
Compare

Adds the ability to automatically compare snapshot images between two preprod builds using pixel-level diffing.
When a snapshot is uploaded with VCS info (base_sha, base_repo_name, base_ref), the endpoint now looks for a matching base artifact and triggers a
compare_snapshotsCelery task. This task:image_diffmodule)PreprodSnapshotComparisonmodelThe GET endpoint is updated to return categorized image lists (changed, added, removed, unchanged) along with diff metadata when comparison data is available.
New modules:
sentry.preprod.snapshots.image_diff— wraps the odiff binary in server mode for efficient batch image comparisonsentry.preprod.snapshots.tasks— Celery task for async snapshot comparisonsnapshot-diffinternal endpoint registered in URL patternsAlso adds
odiff-binas a dependency for the image diffing binary.