Skip to content

Commit 1a0e6ef

Browse files
authored
feat(snapshots): Upload image metadata json data as part of snapshots job (#3163)
### Description First-class supported platforms (web library, iOS/Android in future) will be outputting a "sidecar" json file with each image containing custom metadata set by the framework and provided by the user. This will contain things like the "display name" among other items. This updates our snapshots command to process these sidecar files if present and upload them as part of the image manifest data in the POST body.
1 parent f3e45e8 commit 1a0e6ef

File tree

3 files changed

+101
-10
lines changed

3 files changed

+101
-10
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Experimental Feature 🧑‍🔬 (internal-only)
6+
7+
- Pipe snapshot sidecar metadata into upload as part of `sentry-cli build snapshots` command ([#3163](https://github.com/getsentry/sentry-cli/pull/3163)).
8+
39
## 3.3.0
410

511
### New Features
@@ -120,6 +126,7 @@ The following changes only apply when using `sentry-cli` via the npm package [`@
120126
- Drop support for Node.js <18. The minimum required Node.js version is now 18.0.0 ([#2985](https://github.com/getsentry/sentry-cli/issues/2985)).
121127
- The type export `SentryCliReleases` has been removed.
122128
- The JavaScript wrapper now uses named exports instead of default exports ([#2989](https://github.com/getsentry/sentry-cli/pull/2989)). You need to update your imports:
129+
123130
```js
124131
// Old (default import)
125132
const SentryCli = require('@sentry/cli');
@@ -129,6 +136,7 @@ The following changes only apply when using `sentry-cli` via the npm package [`@
129136
```
130137

131138
For ESM imports:
139+
132140
```js
133141
// Old
134142
import SentryCli from '@sentry/cli';
@@ -137,7 +145,6 @@ The following changes only apply when using `sentry-cli` via the npm package [`@
137145
import { SentryCli } from '@sentry/cli';
138146
```
139147

140-
141148
### Improvements
142149

143150
- The `sentry-cli upload-proguard` command now uses chunked uploading by default ([#2918](https://github.com/getsentry/sentry-cli/pull/2918)). Users who previously set the `SENTRY_EXPERIMENTAL_PROGUARD_CHUNK_UPLOAD` environment variable to opt into this behavior no longer need to set the variable.
@@ -164,6 +171,7 @@ The following changes only apply when using `sentry-cli` via the npm package [`@
164171
- The `sentry-cli build upload` command now automatically detects the correct branch or tag reference in non-PR GitHub Actions workflows ([#2976](https://github.com/getsentry/sentry-cli/pull/2976)). Previously, `--head-ref` was only auto-detected for pull request workflows. Now it works for push, release, and other workflow types by using the `GITHUB_REF_NAME` environment variable.
165172

166173
### Fixes
174+
167175
- Fixed a bug where the `sentry-cli sourcemaps inject` command could inject JavaScript code into certain incorrectly formatted source map files, corrupting their JSON structure ([#3003](https://github.com/getsentry/sentry-cli/pull/3003)).
168176

169177
## 2.58.2

src/api/data_types/snapshots.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
use std::collections::HashMap;
44

55
use serde::{Deserialize, Serialize};
6+
use serde_json::Value;
7+
8+
const IMAGE_FILE_NAME_FIELD: &str = "image_file_name";
9+
const WIDTH_FIELD: &str = "width";
10+
const HEIGHT_FIELD: &str = "height";
611

712
/// Response from the create snapshot endpoint.
813
#[derive(Debug, Deserialize)]
@@ -23,9 +28,59 @@ pub struct SnapshotsManifest {
2328

2429
// Keep in sync with https://github.com/getsentry/sentry/blob/master/src/sentry/preprod/snapshots/manifest.py
2530
/// Metadata for a single image in a snapshot manifest.
31+
///
32+
/// CLI-managed fields (`image_file_name`, `width`, `height`) override any
33+
/// identically named fields provided by user sidecar metadata.
2634
#[derive(Debug, Serialize)]
2735
pub struct ImageMetadata {
28-
pub image_file_name: String,
29-
pub width: u32,
30-
pub height: u32,
36+
#[serde(flatten)]
37+
data: HashMap<String, Value>,
38+
}
39+
40+
impl ImageMetadata {
41+
pub fn new(
42+
image_file_name: String,
43+
width: u32,
44+
height: u32,
45+
mut extra: HashMap<String, Value>,
46+
) -> Self {
47+
extra.insert(
48+
IMAGE_FILE_NAME_FIELD.to_owned(),
49+
Value::String(image_file_name),
50+
);
51+
extra.insert(WIDTH_FIELD.to_owned(), Value::from(width));
52+
extra.insert(HEIGHT_FIELD.to_owned(), Value::from(height));
53+
54+
Self { data: extra }
55+
}
56+
}
57+
58+
#[cfg(test)]
59+
mod tests {
60+
use super::*;
61+
62+
use serde_json::json;
63+
64+
#[test]
65+
fn cli_managed_fields_override_sidecar_fields() {
66+
let extra = serde_json::from_value(json!({
67+
(IMAGE_FILE_NAME_FIELD): "from-sidecar.png",
68+
(WIDTH_FIELD): 1,
69+
(HEIGHT_FIELD): 2,
70+
"custom": "keep-me"
71+
}))
72+
.unwrap();
73+
74+
let metadata = ImageMetadata::new("from-cli.png".to_owned(), 100, 200, extra);
75+
let serialized = serde_json::to_value(metadata).unwrap();
76+
77+
let expected = json!({
78+
(IMAGE_FILE_NAME_FIELD): "from-cli.png",
79+
(WIDTH_FIELD): 100,
80+
(HEIGHT_FIELD): 200,
81+
"custom": "keep-me"
82+
});
83+
84+
assert_eq!(serialized, expected);
85+
}
3186
}

src/commands/build/snapshots.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::HashMap;
2-
use std::fs;
2+
use std::fs::{self, File};
3+
use std::io::BufReader;
34
use std::path::{Path, PathBuf};
45
use std::str::FromStr as _;
56

@@ -10,6 +11,7 @@ use itertools::Itertools as _;
1011
use log::{debug, info, warn};
1112
use objectstore_client::{ClientBuilder, ExpirationPolicy, Usecase};
1213
use secrecy::ExposeSecret as _;
14+
use serde_json::Value;
1315
use sha2::{Digest as _, Sha256};
1416
use walkdir::WalkDir;
1517

@@ -105,6 +107,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
105107
style(images.len()).yellow(),
106108
if images.len() == 1 { "file" } else { "files" }
107109
);
110+
108111
let manifest_entries = upload_images(images, &org, &project)?;
109112

110113
// Build manifest from discovered images
@@ -231,6 +234,29 @@ fn is_image_file(path: &Path) -> bool {
231234
.unwrap_or(false)
232235
}
233236

237+
/// Reads the companion JSON sidecar for an image, if it exists.
238+
///
239+
/// For an image at `path/to/button.png`, looks for `path/to/button.json`.
240+
/// Returns a map of all key-value pairs from the JSON file.
241+
fn read_sidecar_metadata(image_path: &Path) -> Result<HashMap<String, Value>> {
242+
let sidecar_path = image_path.with_extension("json");
243+
if !sidecar_path.is_file() {
244+
return Ok(HashMap::new());
245+
}
246+
247+
debug!("Reading sidecar metadata: {}", sidecar_path.display());
248+
249+
let sidecar_file = File::open(&sidecar_path)
250+
.with_context(|| format!("Failed to open sidecar file {}", sidecar_path.display()))?;
251+
252+
serde_json::from_reader(BufReader::new(sidecar_file)).with_context(|| {
253+
format!(
254+
"Failed to read sidecar file {} as JSON",
255+
sidecar_path.display()
256+
)
257+
})
258+
}
259+
234260
fn upload_images(
235261
images: Vec<ImageInfo>,
236262
org: &str,
@@ -290,13 +316,15 @@ fn upload_images(
290316
.unwrap_or_default()
291317
.to_string_lossy()
292318
.into_owned();
319+
320+
let extra = read_sidecar_metadata(&image.path).unwrap_or_else(|err| {
321+
warn!("Error reading sidecar metadata, ignoring it instead: {err:#}");
322+
HashMap::new()
323+
});
324+
293325
manifest_entries.insert(
294326
hash,
295-
ImageMetadata {
296-
image_file_name,
297-
width: image.width,
298-
height: image.height,
299-
},
327+
ImageMetadata::new(image_file_name, image.width, image.height, extra),
300328
);
301329
}
302330

0 commit comments

Comments
 (0)