From 4a2ec29cddb0d67f572499f461396841be120fc5 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 16 Jun 2026 15:26:12 -0700 Subject: [PATCH 1/3] feat(snapshots): Compress preprod snapshot manifest with zstd The preprod snapshot manifest can be large; send it as a zstd-compressed JSON body so create_preprod_snapshot transfers fewer bytes over the wire. Adds a with_zstd_json_body request builder that serializes the payload to JSON, compresses it with zstd, and sets the Content-Encoding: zstd header. --- Cargo.lock | 1 + Cargo.toml | 1 + src/api/mod.rs | 21 ++++++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3073b9181f..7fa7281372 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3702,6 +3702,7 @@ dependencies = [ "whoami", "windows-sys 0.59.0", "zip 2.4.2", + "zstd", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 87e4a8f3aa..99092a8382 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ walkdir = "2.3.2" which = "4.4.0" whoami = "1.5.2" zip = "2.4.2" +zstd = "0.13.3" data-encoding = "2.3.3" magic_string = "0.3.4" chrono-tz = "0.8.4" diff --git a/src/api/mod.rs b/src/api/mod.rs index b52d4cc4e0..e7ca1c0097 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1020,7 +1020,9 @@ impl AuthenticatedApi<'_> { PathArg(org), PathArg(project) ); - self.post(&path, body) + self.request(Method::Post, &path)? + .with_zstd_json_body(body)? + .send() } /// Fetches upload options for snapshots. @@ -1360,6 +1362,23 @@ impl ApiRequest { Ok(self) } + pub fn with_zstd_json_body(mut self, body: &S) -> ApiResult { + let mut body_bytes: Vec = vec![]; + serde_json::to_writer(&mut body_bytes, &body) + .map_err(|err| ApiError::with_source(ApiErrorKind::CannotSerializeAsJson, err))?; + let compressed = zstd::encode_all(body_bytes.as_slice(), 0) + .map_err(|err| ApiError::with_source(ApiErrorKind::CompressionFailed, err))?; + debug!( + "zstd json body: {} bytes compressed to {} bytes", + body_bytes.len(), + compressed.len() + ); + self.body = Some(compressed); + self.headers.append("Content-Type: application/json")?; + self.headers.append("Content-Encoding: zstd")?; + Ok(self) + } + pub fn with_body(mut self, body: Vec) -> Self { self.body = Some(body); self From 597d3031e65b08467568cdd7de86da69efd48aa6 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 16 Jun 2026 15:51:59 -0700 Subject: [PATCH 2/3] docs: Add changelog entry for zstd snapshot manifest compression --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce4bb3765..9c4e2c3875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- (snapshots) Compress preprod snapshot manifest with zstd ([#3336](https://github.com/getsentry/sentry-cli/pull/3336)) + ## 3.5.1 ### Internal Changes 🔧 From ec50433a3e62b60bc5f0ed7b0688e66eb51d0411 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 22 Jun 2026 13:46:36 -0700 Subject: [PATCH 3/3] perf(api): Stream zstd json body to avoid extra allocation Write serde_json output directly into the zstd encoder instead of serializing the entire body into an intermediate uncompressed buffer first. This removes a full-size allocation of the uncompressed JSON, which matters for the large preprod snapshot manifests this is used for. --- src/api/mod.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index e7ca1c0097..24a3741844 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -42,6 +42,7 @@ use symbolic::common::DebugId; use symbolic::debuginfo::ObjectKind; use url::Url; use uuid::Uuid; +use zstd::Encoder as ZstdEncoder; use crate::api::errors::{ProjectRenamedError, RetryError}; use crate::config::{Auth, Config}; @@ -1363,16 +1364,23 @@ impl ApiRequest { } pub fn with_zstd_json_body(mut self, body: &S) -> ApiResult { - let mut body_bytes: Vec = vec![]; - serde_json::to_writer(&mut body_bytes, &body) - .map_err(|err| ApiError::with_source(ApiErrorKind::CannotSerializeAsJson, err))?; - let compressed = zstd::encode_all(body_bytes.as_slice(), 0) + let mut encoder = ZstdEncoder::new(Vec::new(), 0) .map_err(|err| ApiError::with_source(ApiErrorKind::CompressionFailed, err))?; - debug!( - "zstd json body: {} bytes compressed to {} bytes", - body_bytes.len(), - compressed.len() - ); + + serde_json::to_writer(&mut encoder, &body).map_err(|err| { + let kind = if err.is_io() { + ApiErrorKind::CompressionFailed + } else { + ApiErrorKind::CannotSerializeAsJson + }; + ApiError::with_source(kind, err) + })?; + + let compressed = encoder + .finish() + .map_err(|err| ApiError::with_source(ApiErrorKind::CompressionFailed, err))?; + + debug!("zstd json body: {} bytes compressed", compressed.len()); self.body = Some(compressed); self.headers.append("Content-Type: application/json")?; self.headers.append("Content-Encoding: zstd")?;