Pass the previous app version to the notification endpoint so the update notification can display all missed changelogs, not just the latest one.

This commit is contained in:
Gregory Schier
2025-10-18 07:13:52 -07:00
parent f5094c5a94
commit f8478677c5
5 changed files with 123 additions and 40 deletions
+52 -26
View File
@@ -1,48 +1,74 @@
use chrono::{NaiveDateTime, Utc};
use log::debug;
use std::sync::OnceLock;
use tauri::{AppHandle, Runtime}; use tauri::{AppHandle, Runtime};
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
const NAMESPACE: &str = "analytics"; const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches"; const NUM_LAUNCHES_KEY: &str = "num_launches";
const LAST_VERSION_KEY: &str = "last_tracked_version";
const PREV_VERSION_KEY: &str = "last_tracked_version_prev";
const VERSION_SINCE_KEY: &str = "last_tracked_version_since";
#[derive(Default, Debug)] #[derive(Default, Debug, Clone)]
pub struct LaunchEventInfo { pub struct LaunchEventInfo {
pub current_version: String, pub current_version: String,
pub previous_version: String, pub previous_version: String,
pub launched_after_update: bool, pub launched_after_update: bool,
pub version_since: NaiveDateTime,
pub user_since: NaiveDateTime,
pub num_launches: i32, pub num_launches: i32,
} }
pub async fn store_launch_history<R: Runtime>(app_handle: &AppHandle<R>) -> LaunchEventInfo { static LAUNCH_INFO: OnceLock<LaunchEventInfo> = OnceLock::new();
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default(); pub fn get_or_upsert_launch_info<R: Runtime>(app_handle: &AppHandle<R>) -> &LaunchEventInfo {
LAUNCH_INFO.get_or_init(|| {
let now = Utc::now().naive_utc();
let mut info = LaunchEventInfo {
version_since: app_handle.db().get_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, now),
current_version: app_handle.package_info().version.to_string(),
user_since: app_handle.db().get_settings().created_at,
num_launches: app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0) + 1,
info.num_launches = get_num_launches(app_handle).await + 1; // The rest will be set below
info.current_version = app_handle.package_info().version.to_string(); ..Default::default()
};
app_handle app_handle
.with_tx(|tx| { .with_tx(|tx| {
info.previous_version = // Load the previously tracked version
tx.get_key_value_string(NAMESPACE, last_tracked_version_key, ""); let curr_db = tx.get_key_value_str(NAMESPACE, LAST_VERSION_KEY, "");
let prev_db = tx.get_key_value_str(NAMESPACE, PREV_VERSION_KEY, "");
if !info.previous_version.is_empty() { // We just updated if the app version is different from the last tracked version we stored
info.launched_after_update = info.current_version != info.previous_version; if !curr_db.is_empty() && info.current_version != curr_db {
}; info.launched_after_update = true;
}
// Update key values // If we just updated, track the previous version as the "previous" current version
if info.launched_after_update {
info.previous_version = curr_db.clone();
info.version_since = now;
} else {
info.previous_version = prev_db.clone();
}
let source = &UpdateSource::Background; // Rotate stored versions: move previous into the "prev" slot before overwriting
let version = info.current_version.as_str(); let source = &UpdateSource::Background;
tx.set_key_value_string(NAMESPACE, last_tracked_version_key, version, source);
tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
Ok(())
})
.unwrap();
info tx.set_key_value_str(NAMESPACE, PREV_VERSION_KEY, &info.previous_version, source);
} tx.set_key_value_str(NAMESPACE, LAST_VERSION_KEY, &info.current_version, source);
tx.set_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, info.version_since, source);
pub async fn get_num_launches<R: Runtime>(app_handle: &AppHandle<R>) -> i32 { tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0)
Ok(())
})
.unwrap();
debug!("Initialized launch info");
info
})
} }
+1 -1
View File
@@ -1454,7 +1454,7 @@ pub fn run() {
let _ = window::create_main_window(app_handle, "/"); let _ = window::create_main_window(app_handle, "/");
let h = app_handle.clone(); let h = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let info = history::store_launch_history(&h).await; let info = history::get_or_upsert_launch_info(&h);
debug!("Launched Yaak {:?}", info); debug!("Launched Yaak {:?}", info);
}); });
+38 -9
View File
@@ -1,7 +1,7 @@
use std::time::SystemTime; use std::time::SystemTime;
use crate::error::Result; use crate::error::Result;
use crate::history::get_num_launches; use crate::history::get_or_upsert_launch_info;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use log::debug; use log::debug;
use reqwest::Method; use reqwest::Method;
@@ -79,7 +79,7 @@ impl YaakNotifier {
#[cfg(feature = "license")] #[cfg(feature = "license")]
let license_check = { let license_check = {
use yaak_license::{LicenseCheckStatus, check_license}; use yaak_license::{check_license, LicenseCheckStatus};
match check_license(window).await { match check_license(window).await {
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(), Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(), Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(),
@@ -91,17 +91,17 @@ impl YaakNotifier {
#[cfg(not(feature = "license"))] #[cfg(not(feature = "license"))]
let license_check = "disabled".to_string(); let license_check = "disabled".to_string();
let settings = window.db().get_settings(); let launch_info = get_or_upsert_launch_info(app_handle);
let num_launches = get_num_launches(app_handle).await;
let info = app_handle.package_info().clone();
let req = yaak_api_client(app_handle)? let req = yaak_api_client(app_handle)?
.request(Method::GET, "https://notify.yaak.app/notifications") .request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[ .query(&[
("version", info.version.to_string().as_str()), ("version", &launch_info.current_version),
("launches", num_launches.to_string().as_str()), ("version_prev", &launch_info.previous_version),
("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()), ("launches", &launch_info.num_launches.to_string()),
("installed", &launch_info.user_since.format("%Y-%m-%d").to_string()),
("license", &license_check), ("license", &license_check),
("platform", get_os()), ("updates", &get_updater_status(app_handle).to_string()),
("platform", &get_os().to_string()),
]); ]);
let resp = req.send().await?; let resp = req.send().await?;
if resp.status() != 200 { if resp.status() != 200 {
@@ -131,3 +131,32 @@ async fn get_kv<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<String>> {
Some(v) => Ok(serde_json::from_str(&v.value)?), Some(v) => Ok(serde_json::from_str(&v.value)?),
} }
} }
fn get_updater_status<R: Runtime>(app_handle: &AppHandle<R>) -> &'static str {
#[cfg(not(feature = "updater"))]
{
// Updater is not enabled as a Rust feature
return "missing";
}
#[cfg(all(feature = "updater", target_os = "linux"))]
{
let settings = app_handle.db().get_settings();
if !settings.autoupdate {
// Updates are explicitly disabled
"disabled"
} else if std::env::var("APPIMAGE").is_err() {
// Updates are enabled, but unsupported
"unsupported"
} else {
// Updates are enabled and supported
"enabled"
}
}
#[cfg(all(feature = "updater", not(target_os = "linux")))]
{
let settings = app_handle.db().get_settings();
if settings.autoupdate { "enabled" } else { "disabled" }
}
}
+2 -2
View File
@@ -88,7 +88,7 @@ pub async fn activate_license<R: Runtime>(
} }
let body: ActivateLicenseResponsePayload = response.json().await?; let body: ActivateLicenseResponsePayload = response.json().await?;
window.app_handle().db().set_key_value_string( window.app_handle().db().set_key_value_str(
KV_ACTIVATION_ID_KEY, KV_ACTIVATION_ID_KEY,
KV_NAMESPACE, KV_NAMESPACE,
body.activation_id.as_str(), body.activation_id.as_str(),
@@ -207,5 +207,5 @@ fn build_url(path: &str) -> String {
} }
pub async fn get_activation_id<R: Runtime>(app_handle: &AppHandle<R>) -> String { pub async fn get_activation_id<R: Runtime>(app_handle: &AppHandle<R>) -> String {
app_handle.db().get_key_value_string(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "") app_handle.db().get_key_value_str(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
} }
@@ -1,3 +1,4 @@
use chrono::NaiveDateTime;
use crate::db_context::DbContext; use crate::db_context::DbContext;
use crate::error::Result; use crate::error::Result;
use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo}; use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo};
@@ -22,7 +23,7 @@ impl<'a> DbContext<'a> {
Ok(items.map(|v| v.unwrap()).collect()) Ok(items.map(|v| v.unwrap()).collect())
} }
pub fn get_key_value_string(&self, namespace: &str, key: &str, default: &str) -> String { pub fn get_key_value_str(&self, namespace: &str, key: &str, default: &str) -> String {
match self.get_key_value_raw(namespace, key) { match self.get_key_value_raw(namespace, key) {
None => default.to_string(), None => default.to_string(),
Some(v) => { Some(v) => {
@@ -38,6 +39,22 @@ impl<'a> DbContext<'a> {
} }
} }
pub fn get_key_value_dte(&self, namespace: &str, key: &str, default: NaiveDateTime) -> NaiveDateTime {
match self.get_key_value_raw(namespace, key) {
None => default,
Some(v) => {
let result = serde_json::from_str(&v.value);
match result {
Ok(v) => v,
Err(e) => {
error!("Failed to parse date key value: {}", e);
default
}
}
}
}
}
pub fn get_key_value_int(&self, namespace: &str, key: &str, default: i32) -> i32 { pub fn get_key_value_int(&self, namespace: &str, key: &str, default: i32) -> i32 {
match self.get_key_value_raw(namespace, key) { match self.get_key_value_raw(namespace, key) {
None => default.clone(), None => default.clone(),
@@ -67,7 +84,18 @@ impl<'a> DbContext<'a> {
self.conn.resolve().query_row(sql.as_str(), &*params.as_params(), KeyValue::from_row).ok() self.conn.resolve().query_row(sql.as_str(), &*params.as_params(), KeyValue::from_row).ok()
} }
pub fn set_key_value_string( pub fn set_key_value_dte(
&self,
namespace: &str,
key: &str,
value: NaiveDateTime,
source: &UpdateSource,
) -> (KeyValue, bool) {
let encoded = serde_json::to_string(&value).unwrap();
self.set_key_value_raw(namespace, key, &encoded, source)
}
pub fn set_key_value_str(
&self, &self,
namespace: &str, namespace: &str,
key: &str, key: &str,