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 <eric@shorebird.dev>

* formatting

---------

Co-authored-by: Eric Seidel <eric@shorebird.dev>
This commit is contained in:
Bryan Oltman
2025-10-29 15:57:06 -04:00
committed by GitHub
parent 8bfe1bac47
commit 76f005940d
6 changed files with 205 additions and 31 deletions
Generated
+102 -23
View File
@@ -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"
+1
View File
@@ -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"] }
+67 -2
View File
@@ -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<PatchEvent>,
}
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::<std::io::Error>() {
@@ -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(),
},
+5
View File
@@ -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(),
+12 -3
View File
@@ -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(),
+18 -3
View File
@@ -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<bool> {
/// 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<bool> {
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<bo
}
Ok((
PatchCheckRequest::new(&config),
PatchCheckRequest::new(&config, &client_id),
patches_check_url(&config.base_url),
config.network_hooks.patch_check_request_fn,
))
@@ -383,7 +386,7 @@ fn update_internal(_: &UpdaterLockState, channel: Option<&str>) -> 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,