From 76f005940db57c38b479cee858abc0cfbd12ac28 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Wed, 29 Oct 2025 15:57:06 -0400 Subject: [PATCH] feat: add uuid to updater state, patch check request (#300) * feat: add uuid to updater state * add client_id to patch check request * cleanup * comments * cleanup * more comments * delete commented-out code * Update library/src/cache/updater_state.rs Co-authored-by: Eric Seidel * formatting --------- Co-authored-by: Eric Seidel --- Cargo.lock | 125 +++++++++++++++++++++++------ library/Cargo.toml | 1 + library/src/cache/updater_state.rs | 69 +++++++++++++++- library/src/events.rs | 5 ++ library/src/network.rs | 15 +++- library/src/updater.rs | 21 ++++- 6 files changed, 205 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8cc1457..2a3287a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.2" @@ -96,7 +102,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.1", "object", "rustc-demangle", ] @@ -267,9 +273,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -430,12 +436,12 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "flate2" -version = "1.0.28" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -577,6 +583,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gimli" version = "0.28.1" @@ -848,10 +866,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -929,6 +948,16 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.10" @@ -1191,7 +1220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", - "getrandom", + "getrandom 0.2.11", "rand 0.8.5", "ring", "rustc-hash", @@ -1227,6 +1256,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.4.6" @@ -1282,7 +1317,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.11", ] [[package]] @@ -1412,7 +1447,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.11", "libc", "spin", "untrusted", @@ -1487,6 +1522,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.16" @@ -1620,6 +1661,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "similar" version = "2.4.0" @@ -1996,6 +2043,7 @@ dependencies = [ "sha2", "simple_logger", "tempdir", + "uuid", "zip", ] @@ -2010,6 +2058,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.4" @@ -2032,24 +2091,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.89" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn 2.0.89", @@ -2070,9 +2140,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2080,9 +2150,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -2093,9 +2163,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" @@ -2299,6 +2372,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "zeroize" version = "1.8.1" diff --git a/library/Cargo.toml b/library/Cargo.toml index 20f98ff..2125187 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -49,6 +49,7 @@ serde_json = "1.0.93" serde_yaml = "0.9.19" # For computing hashes of patch files for validation. sha2 = "0.10.6" +uuid = { version = "1.18.1", features = ["v4"] } # For decompressing .apk files. zip = { version = "0.6.4", default-features = false, features = ["deflate"] } diff --git a/library/src/cache/updater_state.rs b/library/src/cache/updater_state.rs index 7067b93..078e093 100644 --- a/library/src/cache/updater_state.rs +++ b/library/src/cache/updater_state.rs @@ -44,6 +44,12 @@ pub struct UpdaterState { /// Written out to disk as a json file at STATE_FILE_NAME. #[derive(Debug, Deserialize, Serialize)] struct SerializedState { + /// The client ID for this device. This is assigned on the first launch of this app and persists + /// between release versions. This is only reset when the app is uninstalled. + /// Shorebird uses these per-install ids in order to provide you, the customer, + /// install-count analytics for your apps. Storage or use of this, and any other, + /// information is covered in our privacy policy: https://shorebird.dev/privacy/ + client_id: String, // Per-release state: /// The release version this cache corresponds to. /// If this does not match the release version we're booting from we will @@ -54,6 +60,10 @@ struct SerializedState { queued_events: Vec, } +fn generate_client_id() -> String { + uuid::Uuid::new_v4().to_string() +} + fn is_file_not_found(error: &anyhow::Error) -> bool { for cause in error.chain() { if let Some(io_error) = cause.downcast_ref::() { @@ -63,14 +73,27 @@ fn is_file_not_found(error: &anyhow::Error) -> bool { false } +/// Serialized updater state +impl UpdaterState { + pub fn client_id(&self) -> String { + self.serialized_state.client_id.clone() + } +} + /// Lifecycle methods for the updater state. impl UpdaterState { /// Creates a new `UpdaterState`. - fn new(cache_dir: PathBuf, release_version: String, patch_public_key: Option<&str>) -> Self { + fn new( + cache_dir: PathBuf, + release_version: String, + patch_public_key: Option<&str>, + client_id: String, + ) -> Self { Self { cache_dir: cache_dir.clone(), patch_manager: Box::new(PatchManager::new(cache_dir.clone(), patch_public_key)), serialized_state: SerializedState { + client_id: client_id, release_version, queued_events: Vec::new(), }, @@ -93,11 +116,13 @@ impl UpdaterState { storage_dir: &Path, release_version: &str, patch_public_key: Option<&str>, + client_id: String, ) -> Self { let mut state = Self::new( storage_dir.to_owned(), release_version.to_owned(), patch_public_key, + client_id, ); if let Err(e) = state.save() { shorebird_warn!("Error saving state {:?}, ignoring.", e); @@ -125,6 +150,7 @@ impl UpdaterState { storage_dir, release_version, patch_public_key, + loaded.client_id(), ); } loaded @@ -133,7 +159,12 @@ impl UpdaterState { if !is_file_not_found(&e) { shorebird_info!("No existing state file found: {:#}, creating new state.", e); } - Self::create_new_and_save(storage_dir, release_version, patch_public_key) + Self::create_new_and_save( + storage_dir, + release_version, + patch_public_key, + generate_client_id(), + ) } } } @@ -269,6 +300,7 @@ mod tests { cache_dir: tmp_dir.path().to_path_buf(), patch_manager: Box::new(patch_manager), serialized_state: SerializedState { + client_id: "123".to_string(), release_version: "1.0.0+1".to_string(), queued_events: Vec::new(), }, @@ -313,6 +345,38 @@ mod tests { assert!(super::is_file_not_found(&result.unwrap_err())); } + #[test] + fn creates_updater_state_with_client_id() { + let tmp_dir = TempDir::new("example").unwrap(); + let state = UpdaterState::load_or_new_on_error(tmp_dir.path(), "1.0.0+1", None); + let saved_state = UpdaterState::load_or_new_on_error(tmp_dir.path(), "1.0.0+1", None); + assert_eq!( + state.serialized_state.client_id, + saved_state.serialized_state.client_id + ); + } + + // A new UpdaterState is created when the release version is changed, but + // the client_id should remain the same. + #[test] + fn client_id_does_not_change_if_release_version_changes() { + let tmp_dir = TempDir::new("example").unwrap(); + + let state = test_state(&tmp_dir, PatchManager::manager_for_test(&tmp_dir)); + let original_loaded = UpdaterState::load_or_new_on_error( + &state.cache_dir, + &state.serialized_state.release_version, + None, + ); + + let new_loaded = UpdaterState::load_or_new_on_error(&state.cache_dir, "1.0.0+2", None); + + assert_eq!( + original_loaded.serialized_state.client_id, + new_loaded.serialized_state.client_id + ); + } + #[test] fn does_not_save_cache_dir() { let original_tmp_dir = TempDir::new("example").unwrap(); @@ -320,6 +384,7 @@ mod tests { cache_dir: original_tmp_dir.path().to_path_buf(), patch_manager: Box::new(PatchManager::manager_for_test(&original_tmp_dir)), serialized_state: SerializedState { + client_id: "123".to_string(), release_version: "1.0.0+1".to_string(), queued_events: Vec::new(), }, diff --git a/library/src/events.rs b/library/src/events.rs index 659f55c..60bb7e8 100644 --- a/library/src/events.rs +++ b/library/src/events.rs @@ -54,6 +54,9 @@ pub struct PatchEvent { /// The architecture we're running (e.g. "aarch64", "x86", "x86_64"). pub arch: String, + /// The unique ID of this device. + pub client_id: String, + /// The identifier of this event. #[serde(rename = "type")] pub identifier: EventType, @@ -81,11 +84,13 @@ impl PatchEvent { config: &UpdateConfig, event_type: EventType, patch_number: usize, + client_id: String, message: Option<&str>, ) -> PatchEvent { PatchEvent { app_id: config.app_id.clone(), arch: current_arch().to_string(), + client_id, identifier: event_type, patch_number, platform: current_platform().to_string(), diff --git a/library/src/network.rs b/library/src/network.rs index 41608d8..741df77 100644 --- a/library/src/network.rs +++ b/library/src/network.rs @@ -169,18 +169,22 @@ pub struct PatchCheckRequest { pub platform: String, /// Architecture we're running (e.g. "aarch64", "x86", "x86_64"). pub arch: String, + /// The unique ID of this device. This is a random UUID generated by Shorebird and _not_ the + /// device's UUID or any other identifier that has meaning outside of Shorebird. + pub client_id: String, // We specifically do not send a patch number as part of this request because we always want to // know what the latest available patch is. } impl PatchCheckRequest { - pub fn new(config: &UpdateConfig) -> PatchCheckRequest { + pub fn new(config: &UpdateConfig, client_id: &str) -> PatchCheckRequest { PatchCheckRequest { app_id: config.app_id.clone(), channel: config.channel.clone(), release_version: config.release_version.clone(), platform: current_platform().to_string(), arch: current_arch().to_string(), + client_id: client_id.to_string(), } } } @@ -274,6 +278,7 @@ mod tests { fn create_patch_install_event_request_serializes() { let event = PatchEvent { app_id: "app_id".to_string(), + client_id: "client_id".to_string(), arch: "arch".to_string(), patch_number: 1, platform: "platform".to_string(), @@ -286,7 +291,7 @@ mod tests { let json_string = serde_json::to_string(&request).unwrap(); assert_eq!( json_string, - r#"{"event":{"app_id":"app_id","arch":"arch","type":"__patch_install__","patch_number":1,"platform":"platform","release_version":"release_version","timestamp":1234,"message":null}}"# + r#"{"event":{"app_id":"app_id","arch":"arch","client_id":"client_id","type":"__patch_install__","patch_number":1,"platform":"platform","release_version":"release_version","timestamp":1234,"message":null}}"# ) } @@ -294,6 +299,7 @@ mod tests { fn create_patch_install_event_request_serializes_with_message() { let event = PatchEvent { app_id: "app_id".to_string(), + client_id: "client_id".to_string(), arch: "arch".to_string(), patch_number: 1, platform: "platform".to_string(), @@ -306,7 +312,7 @@ mod tests { let json_string = serde_json::to_string(&request).unwrap(); assert_eq!( json_string, - r#"{"event":{"app_id":"app_id","arch":"arch","type":"__patch_install__","patch_number":1,"platform":"platform","release_version":"release_version","timestamp":1234,"message":"hello"}}"# + r#"{"event":{"app_id":"app_id","arch":"arch","client_id":"client_id","type":"__patch_install__","patch_number":1,"platform":"platform","release_version":"release_version","timestamp":1234,"message":"hello"}}"# ) } @@ -324,6 +330,7 @@ mod tests { release_version: "".to_string(), platform: "".to_string(), arch: "".to_string(), + client_id: "".to_string(), }, ); assert!(result.is_err()); @@ -375,6 +382,7 @@ mod tests { fn handle_network_result_no_internet() { let event = PatchEvent { app_id: "app_id".to_string(), + client_id: "client_id".to_string(), arch: "arch".to_string(), patch_number: 2, platform: "platform".to_string(), @@ -404,6 +412,7 @@ mod tests { super::CreatePatchEventRequest { event: PatchEvent { app_id: "app_id".to_string(), + client_id: "client_id".to_string(), arch: "arch".to_string(), patch_number: 2, platform: "platform".to_string(), diff --git a/library/src/updater.rs b/library/src/updater.rs index c76c527..f96e69d 100644 --- a/library/src/updater.rs +++ b/library/src/updater.rs @@ -219,6 +219,7 @@ pub fn handle_prior_boot_failure_if_necessary() -> Result<(), InitError> { config, EventType::PatchInstallFailure, patch.number, + state.client_id(), Some( format!( "Patch {} was marked currently_booting in init", @@ -250,6 +251,8 @@ pub fn should_auto_update() -> anyhow::Result { /// Returns true if an update is available for download. Will return false if the update is already /// downloaded and ready to install. pub fn check_for_downloadable_update(channel: Option<&str>) -> anyhow::Result { + let client_id = with_state(|state| Ok(state.client_id()))?; + let (request, url, request_fn) = with_config(|config| { let mut config = config.clone(); @@ -259,7 +262,7 @@ pub fn check_for_downloadable_update(channel: Option<&str>) -> anyhow::Result) -> anyhow::Resul shorebird_error!("Failed to clear events: {:?}", err); } // Update our outer state with the new state. - Ok(PatchCheckRequest::new(&config)) + Ok(PatchCheckRequest::new(&config, &state.client_id())) })?; // Check for update. @@ -440,8 +443,15 @@ fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> anyhow::Resul patch.number ); + let client_id = state.client_id(); std::thread::spawn(move || { - let event = PatchEvent::new(&config, EventType::PatchDownload, patch.number, None); + let event = PatchEvent::new( + &config, + EventType::PatchDownload, + patch.number, + client_id, + None, + ); let report_result = crate::network::send_patch_event(event, &config); if let Err(err) = report_result { shorebird_error!("Failed to report patch download: {:?}", err); @@ -601,10 +611,12 @@ pub fn report_launch_failure() -> anyhow::Result<()> { if mark_result.is_err() { shorebird_error!("Failed to mark patch as bad: {:?}", mark_result); } + let client_id = state.client_id(); let event = PatchEvent::new( config, EventType::PatchInstallFailure, patch.number, + client_id, Some( format!( "Install failure reported from engine for patch {}", @@ -657,11 +669,13 @@ pub fn report_launch_success() -> anyhow::Result<()> { } let config_copy = config.clone(); + let client_id = state.client_id(); std::thread::spawn(move || { let event = PatchEvent::new( &config_copy, EventType::PatchInstallSuccess, booting_patch.number, + client_id, None, ); let report_result = crate::network::send_patch_event(event, &config_copy); @@ -1261,6 +1275,7 @@ mod tests { ); let fail_event = PatchEvent { app_id: config.app_id.clone(), + client_id: "client_id".to_string(), arch: current_arch().to_string(), identifier: EventType::PatchInstallFailure, patch_number: 1,