feat: send current_patch_number on patch check (#343)

* feat: send current patch_number on patch check

* refactor: rename patch_number → current_patch_number

Renaming the field on PatchCheckRequest so it doesn't collide with the
legacy `patch_number` field old pre-#189 updaters still send. That
legacy field is what triggers the server's short-circuit response
(patchAvailable: false, no rolled_back_patch_numbers), which is still
load-bearing for ~0.7% of patch-check traffic coming from Flutter
≤ 3.22.2 clients. Using a distinct field name keeps our new analytics
signal from accidentally engaging that path.

Pairs with:
- shorebirdtech/shorebird#3702 (protocol field)
- shorebirdtech/_shorebird#2059 (server consumes this field)

* docs: rephrase current_patch_number doc comment

Drops the 'analytics' framing in favor of describing the field as
superseding patch_number for newer clients. patch_number remains for
compatibility with legacy clients that rely on the short-circuit path.

* fix: adapt to renamed currently_booting_patch and &str client_id
This commit is contained in:
Mac
2026-05-04 17:16:30 -06:00
committed by GitHub
parent 34509fca3c
commit b97c7919bd
2 changed files with 27 additions and 6 deletions
+15 -3
View File
@@ -247,12 +247,22 @@ pub struct PatchCheckRequest {
/// 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.
/// The number of the patch currently running on the device, if any.
///
/// Supersedes the legacy `patch_number` field. `patch_number` is
/// retained server-side for compatibility with older clients that still
/// rely on its short-circuit response path; newer updaters populate
/// this field instead.
#[serde(skip_serializing_if = "Option::is_none")]
pub current_patch_number: Option<usize>,
}
impl PatchCheckRequest {
pub fn new(config: &UpdateConfig, client_id: &str) -> PatchCheckRequest {
pub fn new(
config: &UpdateConfig,
client_id: &str,
current_patch_number: Option<usize>,
) -> PatchCheckRequest {
PatchCheckRequest {
app_id: config.app_id.clone(),
channel: config.channel.clone(),
@@ -260,6 +270,7 @@ impl PatchCheckRequest {
platform: current_platform().to_string(),
arch: current_arch().to_string(),
client_id: client_id.to_string(),
current_patch_number,
}
}
}
@@ -404,6 +415,7 @@ mod tests {
platform: "".to_string(),
arch: "".to_string(),
client_id: "".to_string(),
current_patch_number: None,
},
);
assert!(result.is_err());
+12 -3
View File
@@ -273,7 +273,12 @@ 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 (client_id, current_patch_number) = with_state(|state| {
Ok((
state.client_id().to_string(),
state.currently_booting_patch().map(|p| p.number),
))
})?;
let (request, url, request_fn) = with_config(|config| {
let mut config = config.clone();
@@ -283,7 +288,7 @@ pub fn check_for_downloadable_update(channel: Option<&str>) -> anyhow::Result<bo
}
Ok((
PatchCheckRequest::new(&config, &client_id),
PatchCheckRequest::new(&config, &client_id, current_patch_number),
patches_check_url(&config.base_url),
config.network_hooks.patch_check_request_fn,
))
@@ -400,7 +405,11 @@ 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, &state.client_id()))
Ok(PatchCheckRequest::new(
&config,
&state.client_id(),
state.currently_booting_patch().map(|p| p.number),
))
})?;
// Check for update.