mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-24 10:21:15 +01:00
Launch analytics events, changelog, better filter styles
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::JsonValue;
|
||||
use tauri::{async_runtime, AppHandle, Manager};
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::is_dev;
|
||||
use crate::{is_dev, models};
|
||||
|
||||
// serializable
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -19,6 +22,8 @@ pub enum AnalyticsResource {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsAction {
|
||||
Launch,
|
||||
LaunchFirst,
|
||||
LaunchUpdate,
|
||||
Create,
|
||||
Update,
|
||||
Upsert,
|
||||
@@ -28,6 +33,24 @@ pub enum AnalyticsAction {
|
||||
Duplicate,
|
||||
}
|
||||
|
||||
impl AnalyticsAction {
|
||||
pub fn from_str(s: &str) -> Option<AnalyticsAction> {
|
||||
match s {
|
||||
"launch" => Some(AnalyticsAction::Launch),
|
||||
"launch_first" => Some(AnalyticsAction::LaunchFirst),
|
||||
"launch_update" => Some(AnalyticsAction::LaunchUpdate),
|
||||
"create" => Some(AnalyticsAction::Create),
|
||||
"update" => Some(AnalyticsAction::Update),
|
||||
"upsert" => Some(AnalyticsAction::Upsert),
|
||||
"delete" => Some(AnalyticsAction::Delete),
|
||||
"delete_many" => Some(AnalyticsAction::DeleteMany),
|
||||
"send" => Some(AnalyticsAction::Send),
|
||||
"duplicate" => Some(AnalyticsAction::Duplicate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||
match resource {
|
||||
AnalyticsResource::App => "app",
|
||||
@@ -42,6 +65,8 @@ fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||
fn action_name(action: AnalyticsAction) -> &'static str {
|
||||
match action {
|
||||
AnalyticsAction::Launch => "launch",
|
||||
AnalyticsAction::LaunchFirst => "launch_first",
|
||||
AnalyticsAction::LaunchUpdate => "launch_update",
|
||||
AnalyticsAction::Create => "create",
|
||||
AnalyticsAction::Update => "update",
|
||||
AnalyticsAction::Upsert => "upsert",
|
||||
@@ -52,15 +77,70 @@ fn action_name(action: AnalyticsAction) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_event_blocking(
|
||||
app_handle: &AppHandle,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
async_runtime::block_on(async move {
|
||||
track_event(app_handle, resource, action, attributes).await;
|
||||
});
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LaunchEventInfo {
|
||||
pub current_version: String,
|
||||
pub previous_version: String,
|
||||
pub launched_after_update: bool,
|
||||
pub num_launches: i32,
|
||||
}
|
||||
|
||||
pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo {
|
||||
let namespace = "analytics";
|
||||
let last_tracked_version_key = "last_tracked_version";
|
||||
let db_instance: State<'_, Mutex<Pool<Sqlite>>> = app_handle.state();
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let mut info = LaunchEventInfo::default();
|
||||
|
||||
info.num_launches = models::get_key_value_int(namespace, "num_launches", 0, pool).await + 1;
|
||||
info.previous_version =
|
||||
models::get_key_value_string(namespace, last_tracked_version_key, "", pool).await;
|
||||
info.current_version = app_handle.package_info().version.to_string();
|
||||
|
||||
if info.previous_version.is_empty() {
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::LaunchFirst,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
info.launched_after_update = info.current_version != info.previous_version;
|
||||
if info.launched_after_update {
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::LaunchUpdate,
|
||||
Some(json!({ "num_launches": info.num_launches })),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
// Track a launch event in all cases
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
Some(json!({ "num_launches": info.num_launches })),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
// Update key values
|
||||
|
||||
models::set_key_value_string(
|
||||
namespace,
|
||||
last_tracked_version_key,
|
||||
info.current_version.as_str(),
|
||||
pool,
|
||||
)
|
||||
.await;
|
||||
models::set_key_value_int(namespace, "num_launches", info.num_launches, pool).await;
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub async fn track_event(
|
||||
@@ -79,7 +159,7 @@ pub async fn track_event(
|
||||
};
|
||||
let base_url = match is_dev() {
|
||||
true => "http://localhost:7194",
|
||||
false => "https://t.yaak.app"
|
||||
false => "https://t.yaak.app",
|
||||
};
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
@@ -96,13 +176,17 @@ pub async fn track_event(
|
||||
.get(format!("{base_url}/t/e"))
|
||||
.query(¶ms);
|
||||
|
||||
// Disable analytics actual sending in dev
|
||||
if is_dev() {
|
||||
debug!("track: {} {}", event, attributes_json);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = req.send().await {
|
||||
warn!(
|
||||
"Error sending analytics event: {} {} {:?}",
|
||||
e, event, params
|
||||
);
|
||||
} else {
|
||||
debug!("Send event: {}: {:?}", event, params);
|
||||
"Error sending analytics event: {} {} {} {:?}",
|
||||
e, event, attributes_json, params,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,24 +9,25 @@ extern crate objc;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env::current_dir;
|
||||
use std::fs::{create_dir_all, File, read_to_string};
|
||||
use std::fs::{create_dir_all, read_to_string, File};
|
||||
use std::process::exit;
|
||||
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::{debug, info, warn};
|
||||
use log::{debug, error, info, warn};
|
||||
use rand::random;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
use sqlx::migrate::Migrator;
|
||||
use sqlx::types::Json;
|
||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri_plugin_log::{fern, LogTarget};
|
||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
use window_ext::TrafficLightWindowExt;
|
||||
@@ -84,7 +85,15 @@ async fn send_ephemeral_request(
|
||||
let response = models::HttpResponse::new();
|
||||
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
|
||||
request.id = "".to_string();
|
||||
send_http_request(request, &response, &environment_id2, &app_handle, pool, None).await
|
||||
send_http_request(
|
||||
request,
|
||||
&response,
|
||||
&environment_id2,
|
||||
&app_handle,
|
||||
pool,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -243,8 +252,15 @@ async fn send_request(
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
send_http_request(req, &response2, &environment_id2, &app_handle2, &pool2, download_path).await
|
||||
if let Err(e) = send_http_request(
|
||||
req,
|
||||
&response2,
|
||||
&environment_id2,
|
||||
&app_handle2,
|
||||
&pool2,
|
||||
download_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
response_err(&response2, e, &app_handle2, &pool2)
|
||||
.await
|
||||
@@ -274,11 +290,25 @@ async fn response_err(
|
||||
#[tauri::command]
|
||||
async fn track_event(
|
||||
window: Window<Wry>,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
resource: &str,
|
||||
action: &str,
|
||||
attributes: Option<Value>,
|
||||
) -> Result<(), String> {
|
||||
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
|
||||
let action_type = AnalyticsAction::from_str(action);
|
||||
match (action_type, action_type) {
|
||||
(Some(t), Some(t)) => {
|
||||
analytics::track_event(
|
||||
&window.app_handle(),
|
||||
resource,
|
||||
action_type,
|
||||
attributes,
|
||||
)
|
||||
.await;
|
||||
},
|
||||
_ => {
|
||||
error!("Invalid action type: {}", action);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -298,7 +328,7 @@ async fn get_key_value(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<Option<models::KeyValue>, ()> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let result = models::get_key_value(namespace, key, pool).await;
|
||||
let result = models::get_key_value_raw(namespace, key, pool).await;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -311,7 +341,7 @@ async fn set_key_value(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::KeyValue, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let (key_value, created) = models::set_key_value(namespace, key, value, pool).await;
|
||||
let (key_value, created) = models::set_key_value_raw(namespace, key, value, pool).await;
|
||||
|
||||
if created {
|
||||
emit_and_return(&window, "created_model", key_value)
|
||||
@@ -558,13 +588,9 @@ async fn list_environments(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_settings(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
async fn get_settings(db_instance: State<'_, Mutex<Pool<Sqlite>>>) -> Result<models::Settings, ()> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::get_or_create_settings(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
Ok(models::get_or_create_settings(pool).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -848,15 +874,21 @@ fn main() {
|
||||
},
|
||||
RunEvent::Ready => {
|
||||
let w = create_window(app_handle, None);
|
||||
w.restore_state(StateFlags::all())
|
||||
.expect("Failed to restore window state");
|
||||
if let Err(e) = w.restore_state(StateFlags::all()) {
|
||||
error!("Failed to restore window state {}", e);
|
||||
}
|
||||
|
||||
analytics::track_event_blocking(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
None,
|
||||
);
|
||||
let h = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let info = analytics::track_launch_event(&h).await;
|
||||
info!("Launched Yaak {:?}", info);
|
||||
|
||||
// Wait for window render and give a chance for the user to notice
|
||||
if info.launched_after_update && info.num_launches > 1 {
|
||||
sleep(std::time::Duration::from_secs(5)).await;
|
||||
let _ = w.emit("show_changelog", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
RunEvent::WindowEvent {
|
||||
label: _label,
|
||||
@@ -996,9 +1028,6 @@ fn emit_side_effect<S: Serialize + Clone>(app_handle: &AppHandle<Wry>, event: &s
|
||||
}
|
||||
|
||||
async fn get_update_mode(pool: &Pool<Sqlite>) -> UpdateMode {
|
||||
let mode = models::get_key_value_string("app", "update_mode", pool).await;
|
||||
match mode {
|
||||
Some(mode) => update_mode_from_str(&mode),
|
||||
None => UpdateMode::Stable,
|
||||
}
|
||||
let settings = models::get_or_create_settings(pool).await;
|
||||
update_mode_from_str(settings.update_channel.as_str())
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::fs;
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use tauri::AppHandle;
|
||||
|
||||
fn default_true() -> bool {
|
||||
@@ -21,6 +21,7 @@ pub struct Settings {
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub theme: String,
|
||||
pub appearance: String,
|
||||
pub update_channel: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@@ -179,13 +180,75 @@ pub struct KeyValue {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub async fn set_key_value(
|
||||
pub async fn set_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> (KeyValue, bool) {
|
||||
let existing = get_key_value(namespace, key, pool).await;
|
||||
let encoded = serde_json::to_string(value);
|
||||
set_key_value_raw(namespace, key, &encoded.unwrap(), pool).await
|
||||
}
|
||||
|
||||
pub async fn set_key_value_int(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: i32,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> (KeyValue, bool) {
|
||||
let encoded = serde_json::to_string(&value);
|
||||
set_key_value_raw(namespace, key, &encoded.unwrap(), pool).await
|
||||
}
|
||||
|
||||
pub async fn get_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
default: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> String {
|
||||
match get_key_value_raw(namespace, key, pool).await {
|
||||
None => default.to_string(),
|
||||
Some(v) => {
|
||||
let result = serde_json::from_str(&v.value);
|
||||
match result {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Failed to parse string key value: {}", e);
|
||||
default.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_key_value_int(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
default: i32,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> i32 {
|
||||
match get_key_value_raw(namespace, key, pool).await {
|
||||
None => default.clone(),
|
||||
Some(v) => {
|
||||
let result = serde_json::from_str(&v.value);
|
||||
match result {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Failed to parse int key value: {}", e);
|
||||
default.clone()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_key_value_raw(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> (KeyValue, bool) {
|
||||
let existing = get_key_value_raw(namespace, key, pool).await;
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO key_values (namespace, key, value)
|
||||
@@ -201,13 +264,13 @@ pub async fn set_key_value(
|
||||
.await
|
||||
.expect("Failed to insert key value");
|
||||
|
||||
let kv = get_key_value(namespace, key, pool)
|
||||
let kv = get_key_value_raw(namespace, key, pool)
|
||||
.await
|
||||
.expect("Failed to get key value");
|
||||
(kv, existing.is_none())
|
||||
}
|
||||
|
||||
pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<KeyValue> {
|
||||
pub async fn get_key_value_raw(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<KeyValue> {
|
||||
sqlx::query_as!(
|
||||
KeyValue,
|
||||
r#"
|
||||
@@ -223,22 +286,6 @@ pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> O
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn get_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Option<String> {
|
||||
let kv = get_key_value(namespace, key, pool).await?;
|
||||
let result = serde_json::from_str(&kv.value);
|
||||
match result {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
println!("Failed to parse key value: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Workspace,
|
||||
@@ -346,7 +393,8 @@ async fn get_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
created_at,
|
||||
updated_at,
|
||||
theme,
|
||||
appearance
|
||||
appearance,
|
||||
update_channel
|
||||
FROM settings
|
||||
WHERE id = 'default'
|
||||
"#,
|
||||
@@ -355,10 +403,9 @@ async fn get_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
let existing = get_settings(pool).await;
|
||||
if let Ok(s) = existing {
|
||||
Ok(s)
|
||||
pub async fn get_or_create_settings(pool: &Pool<Sqlite>) -> Settings {
|
||||
if let Ok(settings) = get_settings(pool).await {
|
||||
settings
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -367,8 +414,8 @@ pub async fn get_or_create_settings(pool: &Pool<Sqlite>) -> Result<Settings, sql
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
.await.expect("Failed to insert settings");
|
||||
get_settings(pool).await.expect("Failed to get settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,11 +427,13 @@ pub async fn update_settings(
|
||||
r#"
|
||||
UPDATE settings SET (
|
||||
theme,
|
||||
appearance
|
||||
) = (?, ?) WHERE id = 'default';
|
||||
appearance,
|
||||
update_channel
|
||||
) = (?, ?, ?) WHERE id = 'default';
|
||||
"#,
|
||||
settings.theme,
|
||||
settings.appearance,
|
||||
settings.update_channel
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
@@ -30,13 +30,16 @@ impl YaakUpdater {
|
||||
app_handle: &AppHandle<Wry>,
|
||||
mode: UpdateMode,
|
||||
) -> Result<(), updater::Error> {
|
||||
if is_dev() {
|
||||
info!("Skipping update check because we are in dev mode");
|
||||
self.last_update_check = SystemTime::now();
|
||||
|
||||
let update_mode = get_update_mode_str(mode);
|
||||
let enabled = !is_dev();
|
||||
info!("Checking for updates mode={} enabled={}", update_mode, enabled);
|
||||
|
||||
if !enabled {
|
||||
return Ok(());
|
||||
}
|
||||
self.last_update_check = SystemTime::now();
|
||||
let update_mode = get_update_mode_str(mode);
|
||||
info!("Checking for updates mode={}", update_mode);
|
||||
|
||||
match app_handle
|
||||
.updater()
|
||||
.header("X-Update-Mode", update_mode)?
|
||||
@@ -62,7 +65,7 @@ impl YaakUpdater {
|
||||
if dialog::blocking::ask(
|
||||
None::<&Window>,
|
||||
"Update Installed",
|
||||
format!("Would you like to restart the app?",),
|
||||
"Would you like to restart the app?",
|
||||
) {
|
||||
h.restart();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user