Skip to content

Commit c1625f1

Browse files
hodl
1 parent b275801 commit c1625f1

File tree

16 files changed

+789
-11
lines changed

16 files changed

+789
-11
lines changed

.env.local

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SENTRY_ORG=emerge-tools
2+
SENTRY_PROJECT=hackernews-ios
3+
SENTRY_AUTH_TOKEN=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

.sentryclirc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
; this stub prevents global .sentryclirc from being loaded
1+
[auth]
2+
token=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

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ version = "2.46.0"
66
edition = "2021"
77
rust-version = "1.86"
88

9+
[lib]
10+
name = "sentry_cli"
11+
path = "src/lib.rs"
12+
13+
[[bin]]
14+
name = "test_full_upload"
15+
path = "test_full_upload.rs"
16+
17+
[[bin]]
18+
name = "test_mobile_app_upload"
19+
path = "test_mobile_app_upload.rs"
20+
921
[dependencies]
1022
anylog = "0.6.3"
1123
anyhow = { version = "1.0.69", features = ["backtrace"] }

README_TEST.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Sentry CLI Upload Test
2+
3+
This is a test program that reuses the existing sentry-cli codebase to test the upload process against a local Sentry service.
4+
5+
## Building and Running
6+
7+
1. **Build the test binary:**
8+
```bash
9+
cargo build --bin test_sentry_upload
10+
```
11+
12+
2. **Run the test:**
13+
```bash
14+
cargo run --bin test_sentry_upload
15+
```
16+
17+
Or run the compiled binary directly:
18+
```bash
19+
./target/debug/test_sentry_upload
20+
```
21+
22+
## What it tests
23+
24+
The test program performs the following tests against your local Sentry service:
25+
26+
1. **Authentication Test** - Verifies the auth token works
27+
2. **Chunk Upload Options Test** - Checks `/organizations/{org}/chunk-upload/` endpoint
28+
3. **Artifact Bundle Assembly Test** - Tests `/organizations/{org}/artifactbundle/assemble/` endpoint
29+
4. **Sourcemap Upload Test** - Performs actual file upload using the existing upload logic
30+
31+
## Configuration
32+
33+
The test is configured for:
34+
- **Base URL:** `http://localhost:8000`
35+
- **Organization:** `sentry`
36+
- **Project:** `internal`
37+
- **Auth Token:** Your provided token
38+
- **Test File:** `/Users/nicolashinderling/emerge-dev/emerge/web/.next/server/edge-instrumentation.js.map`
39+
40+
You can modify these values in the `main()` function of `test_sentry_upload.rs`.
41+
42+
## Output
43+
44+
The test will show:
45+
- ✅/❌ Status indicators for each test
46+
- Server capabilities (which ChunkUploadCapability features are supported)
47+
- Upload method used (artifact bundle vs chunked vs legacy)
48+
- Detailed error messages if anything fails
49+
50+
## API Endpoints Tested
51+
52+
1. **GET** `/api/0/organizations/{org}/chunk-upload/`
53+
- Discovers server capabilities
54+
- Returns chunk upload configuration
55+
56+
2. **POST** `/api/0/organizations/{org}/artifactbundle/assemble/`
57+
- Assembles uploaded chunks into artifact bundles
58+
- Tests the artifact bundle flow
59+
60+
The test reuses the exact same code paths as the real `sentry-cli sourcemaps upload` command, so it's a comprehensive test of the upload functionality.

src/api/data_types/chunking/artifact.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use serde::{Deserialize, Serialize};
22
use sha1_smol::Digest;
3+
use std::collections::HashMap;
34

45
use super::ChunkedFileState;
56

@@ -13,16 +14,85 @@ pub struct ChunkedArtifactRequest<'a> {
1314
pub version: Option<&'a str>,
1415
#[serde(skip_serializing_if = "Option::is_none")]
1516
pub dist: Option<&'a str>,
17+
#[serde(skip_serializing_if = "Option::is_none")]
18+
pub filename: Option<String>,
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
pub project_id: Option<String>,
1621
}
1722

1823
#[derive(Debug, Deserialize)]
1924
#[serde(rename_all = "camelCase")]
20-
pub struct AssembleArtifactsResponse {
25+
pub struct ChunkedArtifactResponse {
2126
pub state: ChunkedFileState,
2227
pub missing_chunks: Vec<Digest>,
2328
pub detail: Option<String>,
2429
}
2530

31+
#[derive(Debug, Serialize)]
32+
#[serde(transparent)]
33+
pub struct AssembleArtifactsRequest<'a>(HashMap<Digest, ChunkedArtifactRequest<'a>>);
34+
35+
impl<'a, T> FromIterator<T> for AssembleArtifactsRequest<'a>
36+
where
37+
T: Into<ChunkedArtifactRequest<'a>>,
38+
{
39+
fn from_iter<I>(iter: I) -> Self
40+
where
41+
I: IntoIterator<Item = T>,
42+
{
43+
Self(
44+
iter.into_iter()
45+
.map(|obj| obj.into())
46+
.map(|r| (r.checksum, r))
47+
.collect(),
48+
)
49+
}
50+
}
51+
52+
pub type AssembleArtifactsResponse = ChunkedArtifactResponse;
53+
54+
#[derive(Debug, Serialize)]
55+
pub struct ChunkedPreprodArtifactRequest<'a> {
56+
pub checksum: Digest,
57+
pub chunks: &'a [Digest],
58+
// Optional metadata fields that the server supports
59+
#[serde(skip_serializing_if = "Option::is_none")]
60+
pub build_version: Option<&'a str>,
61+
#[serde(skip_serializing_if = "Option::is_none")]
62+
pub build_number: Option<i32>,
63+
#[serde(skip_serializing_if = "Option::is_none")]
64+
pub build_configuration: Option<&'a str>,
65+
#[serde(skip_serializing_if = "Option::is_none")]
66+
pub date_built: Option<&'a str>,
67+
#[serde(skip_serializing_if = "Option::is_none")]
68+
pub extras: Option<serde_json::Value>,
69+
}
70+
71+
impl<'a> ChunkedPreprodArtifactRequest<'a> {
72+
/// Create a new ChunkedPreprodArtifactRequest with the required fields.
73+
pub fn new(checksum: Digest, chunks: &'a [Digest]) -> Self {
74+
Self {
75+
checksum,
76+
chunks,
77+
build_version: None,
78+
build_number: None,
79+
build_configuration: None,
80+
date_built: None,
81+
extras: None,
82+
}
83+
}
84+
}
85+
86+
#[derive(Debug, Deserialize)]
87+
#[serde(rename_all = "camelCase")]
88+
pub struct ChunkedPreprodArtifactResponse {
89+
pub state: ChunkedFileState,
90+
pub missing_chunks: Vec<Digest>,
91+
pub detail: Option<String>,
92+
}
93+
94+
pub type AssemblePreprodArtifactsResponse = HashMap<Digest, ChunkedPreprodArtifactResponse>;
95+
2696
fn version_is_empty(version: &Option<&str>) -> bool {
2797
match version {
2898
Some(v) => v.is_empty(),

src/api/data_types/chunking/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ mod file_state;
88
mod hash_algorithm;
99
mod upload;
1010

11-
pub use self::artifact::{AssembleArtifactsResponse, ChunkedArtifactRequest};
11+
pub use self::artifact::{
12+
AssembleArtifactsRequest, AssembleArtifactsResponse, ChunkedArtifactRequest, ChunkedArtifactResponse,
13+
ChunkedPreprodArtifactRequest, ChunkedPreprodArtifactResponse,
14+
};
1215
pub use self::compression::ChunkCompression;
1316
pub use self::dif::{AssembleDifsRequest, AssembleDifsResponse, ChunkedDifRequest};
1417
pub use self::file_state::ChunkedFileState;

src/api/data_types/chunking/upload/capability.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ pub enum ChunkUploadCapability {
1111
/// Chunked upload of standalone artifact bundles
1212
ArtifactBundles,
1313

14+
/// Chunked upload of preprocessed artifact bundles (@nico: unclear if this new enum is actually needed)
15+
PreprocessedArtifactBundles,
16+
1417
/// Like `ArtifactBundles`, but with deduplicated chunk
1518
/// upload.
1619
ArtifactBundlesV2,
@@ -30,6 +33,9 @@ pub enum ChunkUploadCapability {
3033
/// Upload of il2cpp line mappings
3134
Il2Cpp,
3235

36+
/// Upload of preprod artifacts (mobile app archives, etc.)
37+
PreprodArtifacts,
38+
3339
/// Any other unsupported capability (ignored)
3440
Unknown,
3541
}
@@ -43,12 +49,14 @@ impl<'de> Deserialize<'de> for ChunkUploadCapability {
4349
"debug_files" => ChunkUploadCapability::DebugFiles,
4450
"release_files" => ChunkUploadCapability::ReleaseFiles,
4551
"artifact_bundles" => ChunkUploadCapability::ArtifactBundles,
52+
"preprocessed_artifact_bundles" => ChunkUploadCapability::PreprocessedArtifactBundles,
4653
"artifact_bundles_v2" => ChunkUploadCapability::ArtifactBundlesV2,
4754
"pdbs" => ChunkUploadCapability::Pdbs,
4855
"portablepdbs" => ChunkUploadCapability::PortablePdbs,
4956
"sources" => ChunkUploadCapability::Sources,
5057
"bcsymbolmaps" => ChunkUploadCapability::BcSymbolmap,
5158
"il2cpp" => ChunkUploadCapability::Il2Cpp,
59+
"preprod_artifacts" => ChunkUploadCapability::PreprodArtifacts,
5260
_ => ChunkUploadCapability::Unknown,
5361
})
5462
}

src/api/data_types/chunking/upload/options.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ pub struct ChunkServerOptions {
1515
pub max_file_size: u64,
1616
#[serde(default)]
1717
pub max_wait: u64,
18-
#[expect(dead_code)]
1918
pub hash_algorithm: ChunkHashAlgorithm,
2019
pub chunk_size: u64,
2120
pub concurrency: u8,

src/api/mod.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,8 @@ impl<'a> AuthenticatedApi<'a> {
988988
projects: &[],
989989
version: None,
990990
dist: None,
991+
filename: None,
992+
project_id: None,
991993
})?
992994
.send()?
993995
.convert_rnf(ApiErrorKind::ReleaseNotFound)
@@ -1011,11 +1013,32 @@ impl<'a> AuthenticatedApi<'a> {
10111013
projects,
10121014
version,
10131015
dist,
1016+
filename: None,
1017+
project_id: None,
10141018
})?
10151019
.send()?
10161020
.convert_rnf(ApiErrorKind::ReleaseNotFound)
10171021
}
10181022

1023+
/// Request preprod artifact assembling and processing from chunks.
1024+
pub fn assemble_preprod_artifact(
1025+
&self,
1026+
org: &str,
1027+
project: &str,
1028+
request: &ChunkedPreprodArtifactRequest<'_>,
1029+
) -> ApiResult<ChunkedPreprodArtifactResponse> {
1030+
let url = format!(
1031+
"/projects/{}/{}/files/preprodartifacts/assemble/",
1032+
PathArg(org),
1033+
PathArg(project)
1034+
);
1035+
1036+
self.request(Method::Post, &url)?
1037+
.with_json_body(request)?
1038+
.send()?
1039+
.convert_rnf(ApiErrorKind::ProjectNotFound)
1040+
}
1041+
10191042
pub fn associate_proguard_mappings(
10201043
&self,
10211044
org: &str,
@@ -1929,7 +1952,6 @@ pub struct AuthDetails {
19291952
#[derive(Deserialize, Debug)]
19301953
pub struct User {
19311954
pub email: String,
1932-
#[expect(dead_code)]
19331955
pub id: String,
19341956
}
19351957

@@ -2011,7 +2033,6 @@ pub struct UpdatedRelease {
20112033
#[derive(Debug, Deserialize)]
20122034
pub struct ReleaseInfo {
20132035
pub version: String,
2014-
#[expect(dead_code)]
20152036
pub url: Option<String>,
20162037
#[serde(rename = "dateCreated")]
20172038
pub date_created: DateTime<Utc>,
@@ -2077,7 +2098,6 @@ pub struct DebugInfoData {
20772098
#[serde(default, rename = "type")]
20782099
pub kind: Option<ObjectKind>,
20792100
#[serde(default)]
2080-
#[expect(dead_code)]
20812101
pub features: Vec<String>,
20822102
}
20832103

0 commit comments

Comments
 (0)