Skip to content

feat(preprod): Add snapshot image comparison task and endpoint logic#109151

Open
NicoHinderling wants to merge 5 commits intocreate-compare-task-v1-part3from
nh/add-endpoint-logic-to-trigger-task
Open

feat(preprod): Add snapshot image comparison task and endpoint logic#109151
NicoHinderling wants to merge 5 commits intocreate-compare-task-v1-part3from
nh/add-endpoint-logic-to-trigger-task

Conversation

@NicoHinderling
Copy link
Contributor

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_snapshots Celery task. This task:

  • Fetches manifests for both head and base artifacts
  • Matches images by file name across the two builds
  • Runs pixel-level comparison using odiff (via a new image_diff module)
  • Stores diff masks and a comparison manifest in object storage
  • Records results in a PreprodSnapshotComparison model

The 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 comparison
  • sentry.preprod.snapshots.tasks — Celery task for async snapshot comparison
  • snapshot-diff internal endpoint registered in URL patterns

Also adds odiff-bin as a dependency for the image diffing binary.

@github-actions github-actions bot added Scope: Frontend Automatically applied to PRs that change frontend components Scope: Backend Automatically applied to PRs that change backend components labels Feb 23, 2026
@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.

@NicoHinderling NicoHinderling changed the base branch from master to graphite-base/109151 February 24, 2026 00:55
@NicoHinderling NicoHinderling force-pushed the nh/add-endpoint-logic-to-trigger-task branch from f04c5f3 to 2b4c3f4 Compare February 24, 2026 00:56
@NicoHinderling NicoHinderling changed the base branch from graphite-base/109151 to create-compare-task-v1 February 24, 2026 00:56
Copy link
Contributor Author

NicoHinderling commented Feb 24, 2026

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.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@NicoHinderling NicoHinderling force-pushed the nh/add-endpoint-logic-to-trigger-task branch from 2b4c3f4 to e1d9787 Compare February 24, 2026 20:41
@NicoHinderling NicoHinderling force-pushed the nh/add-endpoint-logic-to-trigger-task branch from e1d9787 to 317a73a Compare February 24, 2026 21:08
@NicoHinderling NicoHinderling force-pushed the nh/add-endpoint-logic-to-trigger-task branch 2 times, most recently from 43cc0fc to 03eceb6 Compare February 24, 2026 21:20
@NicoHinderling NicoHinderling marked this pull request as ready for review February 24, 2026 21:21
@NicoHinderling NicoHinderling requested a review from a team as a code owner February 24, 2026 21:21
@NicoHinderling NicoHinderling force-pushed the nh/add-endpoint-logic-to-trigger-task branch from 1793aa1 to 2ea9010 Compare February 24, 2026 21:41
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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.


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"
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

@NicoHinderling NicoHinderling Feb 26, 2026

Choose a reason for hiding this comment

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

@rbro112 perhaps we can do non PNGs as a follow up task

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines +327 to 337
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,
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ignore for now

Comment on lines +273 to +283
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will consolidate these two cases into one and then handle showing them on the frontend in a later PR

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.

1 participant