Uh oh!
There was an error while loading. Please reload this page.
feat(snapshots): add upload command - #1186
Conversation
Port `snapshots upload` from the legacy Rust CLI. Scans a folder for PNG/JPEG
screenshots, uploads each to Sentry's object store (skipping any already present
by content hash), and POSTs a manifest to create the snapshot for visual diffing.
Objectstore is live; until a native TS objectstore client ships we make the raw
calls the Rust `objectstore-client` crate makes:
- GET snapshots/upload-options → { objectstore: { url, scopes, authToken, expirationPolicy } }.
- HEAD {url}/v1/objects/preprod/{k=v;...}/{orgId}/{projectId}/{sha256} (dedup),
auth via `x-os-auth: Bearer <jwt>`.
- PUT the raw bytes with `x-sn-expiration`. Objects are keyed by the original
file's SHA-256, so dedup is compression-independent (images are already
compressed → uploaded raw).
- POST snapshots/ with the manifest (app_id, per-image width/height/content_hash
+ sidecar, flattened VCS, diff_threshold/selective/all_image_file_names).
New: src/lib/objectstore.ts (HEAD/PUT client), src/lib/snapshots/images.ts
(collect + validate, dimensions via image-size), preprod-artifacts
fetchSnapshotsUploadOptions/createPreprodSnapshot, src/commands/snapshots/upload.ts.
Adds image-size as a devDependency. Tests cover the objectstore client, image
collection/validation, the two API calls, and the command's dedup + manifest.
|
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.
Reviewed by Cursor Bugbot for commit 035242a. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Codecov Results 📊✅ Patch coverage is 82.63%. Project has 5247 uncovered lines. Files with missing lines (3)
Coverage diff@@ Coverage Diff @@## main #PR +/-##
==========================================
- Coverage 81.70% 81.69% -0.01%
==========================================
Files 410 413 +3
Lines 28467 28657 +190
Branches 18497 18599 +102
==========================================
+ Hits 23257 23410 +153- Misses 5210 5247 +37- Partials 1901 1918 +17Generated by Codecov Action |
Uh oh!
There was an error while loading. Please reload this page.
- M1: POST the create-snapshot manifest with Content-Encoding: zstd (matching
the legacy CLI's with_zstd_json_body) so large image suites don't risk a body
limit. Adds an opt-in `bodyEncoding: "zstd"` to apiRequestToRegion (additive;
falls back to plain JSON when the runtime lacks zstd).
- L2: ignore array/scalar JSON sidecars (only objects are usable metadata).
- L3: enforce the --all-image-file-names[/-file] conflict before the empty-folder
early return.
- N1: reject an empty --diff-threshold (Number("") was silently accepted as 0).
- Tests: assert the objectstore key is {orgId}/{projectId}/{sha256}; sidecar
width/height/content_hash override; --pr-number without a base SHA is rejected.Reduce the upload func's cognitive complexity below the Biome limit after the added flag-conflict check.
BYK
commented
Jul 3, 2026
Self-review (subagent-assisted) + fixesProtocol parity was verified exact against the Python objectstore client + Rust
Tests added for the flagged gaps: the objectstore key is Confirmed safe as-is by the review (no change needed): uploading images raw (no |
- Cursor (High): resolve the positional folder against the command's cwd before scanning — the walker requires an absolute path, so a relative `./screenshots` passed the stat check but would fail the walk. - Warden (High): add request timeouts to the objectstore HEAD (30s) and PUT (120s) via AbortSignal.timeout, so a stalled connection can't hang the CLI / a CI job forever. - Tests: relative-path resolution against cwd; HEAD/PUT carry an AbortSignal.
Uh oh!
There was an error while loading. Please reload this page.
## Summary #1186 ported `snapshots upload` from the legacy `getsentry/sentry-cli`, including its empty-directory early return. That return runs before `--all-image-file-names[-file]` is parsed, so selective runs with no changed images exit successfully without creating a snapshot. Resolve the full image list before the early return. When it is present, post an empty selective manifest and skip objectstore; plain empty uploads remain no-ops. Explicitly empty list flags are now validated instead of treated as absent. Closes#1397. ## Test plan - `pnpm exec vitest run test/commands/snapshots/upload.test.ts test/lib/api/preprod-artifacts.test.ts test/lib/snapshots/images.test.ts test/lib/objectstore.test.ts` — 55 passed - `pnpm run lint` - `pnpm run generate:schema && pnpm run typecheck`

Ports
snapshots uploadfrom the legacy Rust CLI, completing the snapshots trio (upload/download/diff). Scans a folder of screenshots, uploads them to Sentry's object store, and creates a snapshot for visual diffing. Sentry SaaS only.Objectstore approach
The object store is live; a native TS objectstore client is still pending, so — as agreed — we replicate the raw HTTP calls the Rust
objectstore-clientcrate makes (reverse-engineered from getsentry/objectstore):projects/{org}/{project}/preprodartifacts/snapshots/upload-options/→{ objectstore: { url, scopes, authToken, expirationPolicy } }.{url}/v1/objects/preprod/{scope}/{key}to dedup (404 = missing), wherescope = k=v;k=v(ordered) andkey = {orgId}/{projectId}/{sha256}. Auth is a pre-signed JWT sent asx-os-auth: Bearer <token>(notAuthorization).x-sn-expiration: <policy>. The key is the original file's SHA-256, so dedup is independent of upload compression — images are already-compressed formats, so they're uploaded raw (noContent-Encoding).projects/{org}/{project}/preprodartifacts/snapshots/with the manifest →{ artifactId, imageCount, snapshotUrl? }.HEAD/PUT run with bounded concurrency (8).
Behavior (parity with Rust)
<path>(a directory) +--app-id(required).image-size(header-only), SHA-256, and a companion<image>.jsonsidecar (its keys merge into the manifest; CLIwidth/height/content_hashwin).--selective/--all-image-file-names[-file]for subset uploads (the latter validates every uploaded image is in the list; both imply selective).--diff-threshold(0.0–1.0). Full git-metadata flags (auto-collected in CI, likebuild upload); a--pr-numberwithout a resolvable base SHA errors.vcsInfoToBody).Memory
Each image is read once (dimensions + hash) and released; only the missing ones are re-read for the PUT body — bounded to a few images at a time.
New files / deps
src/lib/objectstore.ts,src/lib/snapshots/images.ts,fetchSnapshotsUploadOptions/createPreprodSnapshotinpreprod-artifacts.ts,src/commands/snapshots/upload.ts. Addsimage-sizeas a devDependency (the analog of Rust'simagesize). The VCS flag block is inlined (matchingbuild upload); extracting a shared definition is a noted follow-up.Tests
Objectstore client (URL/auth/HEAD/PUT), image collection + validation (real pngjs fixtures), both API calls, and the command's dedup (HEAD skips existing → fewer PUTs) + manifest shape.
typecheck/lint/check:deps/check:fragmentspass; full suite 8332 passed.