@@ -21,11 +21,8 @@ use uuid::Uuid;
2121use tauri_plugin_global_shortcut:: { GlobalShortcutExt , ShortcutState } ;
2222
2323const GLOBAL_SHORTCUT_STORE_KEY : & str = "globalShortcut" ;
24- const APP_STARTED_TRACKED_DAY_KEY_PREFIX : & str = "analytics.app_started_day." ;
25-
26- fn app_started_day_key ( version : & str ) -> String {
27- format ! ( "{}{}" , APP_STARTED_TRACKED_DAY_KEY_PREFIX , version)
28- }
24+ const DAILY_ACTIVE_TRACKED_DAY_KEY : & str = "analytics.daily_active_day" ;
25+ const DAILY_ACTIVE_EVENT_NAME : & str = "app_started" ;
2926
3027fn today_utc_ymd ( ) -> String {
3128 let date = time:: OffsetDateTime :: now_utc ( ) . date ( ) ;
@@ -37,48 +34,82 @@ fn today_utc_ymd() -> String {
3734 )
3835}
3936
40- fn should_track_app_started ( last_tracked_day : Option < & str > , today : & str ) -> bool {
37+ fn should_track_daily_active ( last_tracked_day : Option < & str > , today : & str ) -> bool {
4138 match last_tracked_day {
4239 Some ( day) => day != today,
4340 None => true ,
4441 }
4542}
4643
4744#[ cfg( desktop) ]
48- fn track_app_started_once_per_day_per_version ( app : & tauri:: App ) {
45+ fn track_daily_active_if_needed ( app_handle : & tauri:: AppHandle ) {
4946 use tauri_plugin_store:: StoreExt ;
5047
51- let version = app. package_info ( ) . version . to_string ( ) ;
52- let key = app_started_day_key ( & version) ;
5348 let today = today_utc_ymd ( ) ;
5449
55- let store = match app . handle ( ) . store ( "settings.json" ) {
50+ let store = match app_handle . store ( "settings.json" ) {
5651 Ok ( store) => store,
5752 Err ( error) => {
58- log:: warn!( "Failed to access settings store for app_started gate: {}" , error) ;
53+ log:: warn!(
54+ "Failed to access settings store for daily analytics gate: {}" ,
55+ error
56+ ) ;
5957 return ;
6058 }
6159 } ;
6260
6361 let last_tracked_day = store
64- . get ( & key )
62+ . get ( DAILY_ACTIVE_TRACKED_DAY_KEY )
6563 . and_then ( |value| value. as_str ( ) . map ( |value| value. to_string ( ) ) ) ;
6664
67- if !should_track_app_started ( last_tracked_day. as_deref ( ) , & today) {
65+ if !should_track_daily_active ( last_tracked_day. as_deref ( ) , & today) {
6866 return ;
6967 }
7068
71- let _ = app. track_event ( "app_started" , None ) ;
69+ if let Err ( error) = app_handle. track_event ( DAILY_ACTIVE_EVENT_NAME , None ) {
70+ log:: warn!( "Failed to track daily analytics event: {}" , error) ;
71+ return ;
72+ }
7273
73- store. set ( & key, serde_json:: Value :: String ( today) ) ;
74+ store. set (
75+ DAILY_ACTIVE_TRACKED_DAY_KEY ,
76+ serde_json:: Value :: String ( today) ,
77+ ) ;
7478 if let Err ( error) = store. save ( ) {
75- log:: warn!( "Failed to save app_started tracked day: {}" , error) ;
79+ log:: warn!( "Failed to save daily analytics tracked day: {}" , error) ;
7680 }
7781}
7882
7983#[ cfg( not( desktop) ) ]
80- fn track_app_started_once_per_day_per_version ( app : & tauri:: App ) {
81- let _ = app. track_event ( "app_started" , None ) ;
84+ fn track_daily_active_if_needed ( app_handle : & tauri:: AppHandle ) {
85+ let _ = app_handle. track_event ( DAILY_ACTIVE_EVENT_NAME , None ) ;
86+ }
87+
88+ #[ cfg( desktop) ]
89+ fn seconds_until_next_utc_day ( now : time:: OffsetDateTime ) -> u64 {
90+ let now_time = now. time ( ) ;
91+ let seconds_since_midnight = u64:: from ( now_time. hour ( ) ) * 60 * 60
92+ + u64:: from ( now_time. minute ( ) ) * 60
93+ + u64:: from ( now_time. second ( ) ) ;
94+ let seconds_until_next_day = 86_400_u64 . saturating_sub ( seconds_since_midnight) ;
95+ if seconds_until_next_day == 0 {
96+ 86_400
97+ } else {
98+ seconds_until_next_day
99+ }
100+ }
101+
102+ #[ cfg( desktop) ]
103+ fn spawn_daily_active_rollover_tracker ( app_handle : tauri:: AppHandle ) {
104+ std:: thread:: spawn ( move || {
105+ loop {
106+ let sleep_for = std:: time:: Duration :: from_secs ( seconds_until_next_utc_day (
107+ time:: OffsetDateTime :: now_utc ( ) ,
108+ ) ) ;
109+ std:: thread:: sleep ( sleep_for) ;
110+ track_daily_active_if_needed ( & app_handle) ;
111+ }
112+ } ) ;
82113}
83114
84115#[ cfg( desktop) ]
@@ -89,7 +120,10 @@ fn managed_shortcut_slot() -> &'static Mutex<Option<String>> {
89120
90121/// Shared shortcut handler that toggles the panel when the shortcut is pressed.
91122#[ cfg( desktop) ]
92- fn handle_global_shortcut ( app : & tauri:: AppHandle , event : tauri_plugin_global_shortcut:: ShortcutEvent ) {
123+ fn handle_global_shortcut (
124+ app : & tauri:: AppHandle ,
125+ event : tauri_plugin_global_shortcut:: ShortcutEvent ,
126+ ) {
93127 if event. state == ShortcutState :: Pressed {
94128 log:: debug!( "Global shortcut triggered" ) ;
95129 panel:: toggle_panel ( app) ;
@@ -270,9 +304,19 @@ async fn start_probe_batch(
270304 if has_error {
271305 log:: warn!( "probe {} completed with error" , plugin_id) ;
272306 } else {
273- log:: info!( "probe {} completed ok ({} lines)" , plugin_id, output. lines. len( ) ) ;
307+ log:: info!(
308+ "probe {} completed ok ({} lines)" ,
309+ plugin_id,
310+ output. lines. len( )
311+ ) ;
274312 }
275- let _ = handle. emit ( "probe:result" , ProbeResult { batch_id : bid, output } ) ;
313+ let _ = handle. emit (
314+ "probe:result" ,
315+ ProbeResult {
316+ batch_id : bid,
317+ output,
318+ } ,
319+ ) ;
276320 }
277321 Err ( _) => {
278322 log:: error!( "probe {} panicked" , plugin_id) ;
@@ -311,7 +355,10 @@ fn get_log_path(app_handle: tauri::AppHandle) -> Result<String, String> {
311355/// Pass `null` to disable the shortcut, or a shortcut string like "CommandOrControl+Shift+U".
312356#[ cfg( desktop) ]
313357#[ tauri:: command]
314- fn update_global_shortcut ( app_handle : tauri:: AppHandle , shortcut : Option < String > ) -> Result < ( ) , String > {
358+ fn update_global_shortcut (
359+ app_handle : tauri:: AppHandle ,
360+ shortcut : Option < String > ,
361+ ) -> Result < ( ) , String > {
315362 let global_shortcut = app_handle. global_shortcut ( ) ;
316363 let normalized_shortcut = shortcut. and_then ( |value| {
317364 let trimmed = value. trim ( ) . to_string ( ) ;
@@ -338,7 +385,11 @@ fn update_global_shortcut(app_handle: tauri::AppHandle, shortcut: Option<String>
338385 * managed_shortcut = None ;
339386 }
340387 Err ( e) => {
341- log:: warn!( "Failed to unregister existing shortcut '{}': {}" , existing, e) ;
388+ log:: warn!(
389+ "Failed to unregister existing shortcut '{}': {}" ,
390+ existing,
391+ e
392+ ) ;
342393 }
343394 }
344395 }
@@ -462,7 +513,9 @@ pub fn run() {
462513 let version = app. package_info ( ) . version . to_string ( ) ;
463514 log:: info!( "OpenUsage v{} starting" , version) ;
464515
465- track_app_started_once_per_day_per_version ( app) ;
516+ track_daily_active_if_needed ( app. handle ( ) ) ;
517+ #[ cfg( desktop) ]
518+ spawn_daily_active_rollover_tracker ( app. handle ( ) . clone ( ) ) ;
466519
467520 let app_data_dir = app. path ( ) . app_data_dir ( ) . expect ( "no app data dir" ) ;
468521 let resource_dir = app. path ( ) . resource_dir ( ) . expect ( "no resource dir" ) ;
@@ -477,7 +530,8 @@ pub fn run() {
477530
478531 tray:: create ( app. handle ( ) ) ?;
479532
480- app. handle ( ) . plugin ( tauri_plugin_updater:: Builder :: new ( ) . build ( ) ) ?;
533+ app. handle ( )
534+ . plugin ( tauri_plugin_updater:: Builder :: new ( ) . build ( ) ) ?;
481535
482536 // Register global shortcut from stored settings
483537 #[ cfg( desktop) ]
@@ -498,7 +552,8 @@ pub fn run() {
498552 } ,
499553 ) {
500554 log:: warn!( "Failed to register initial global shortcut: {}" , e) ;
501- } else if let Ok ( mut managed_shortcut) = managed_shortcut_slot ( ) . lock ( )
555+ } else if let Ok ( mut managed_shortcut) =
556+ managed_shortcut_slot ( ) . lock ( )
502557 {
503558 * managed_shortcut = Some ( shortcut. to_string ( ) ) ;
504559 } else {
@@ -519,29 +574,41 @@ pub fn run() {
519574
520575#[ cfg( test) ]
521576mod tests {
522- use super :: { app_started_day_key, should_track_app_started} ;
577+ use super :: {
578+ DAILY_ACTIVE_TRACKED_DAY_KEY , seconds_until_next_utc_day, should_track_daily_active,
579+ } ;
580+ use time:: { Date , Month , PrimitiveDateTime , Time } ;
523581
524582 #[ test]
525583 fn should_track_when_no_previous_day ( ) {
526- assert ! ( should_track_app_started ( None , "2026-02-12" ) ) ;
584+ assert ! ( should_track_daily_active ( None , "2026-02-12" ) ) ;
527585 }
528586
529587 #[ test]
530588 fn should_not_track_when_same_day ( ) {
531- assert ! ( !should_track_app_started ( Some ( "2026-02-12" ) , "2026-02-12" ) ) ;
589+ assert ! ( !should_track_daily_active ( Some ( "2026-02-12" ) , "2026-02-12" ) ) ;
532590 }
533591
534592 #[ test]
535593 fn should_track_when_day_changes ( ) {
536- assert ! ( should_track_app_started( Some ( "2026-02-11" ) , "2026-02-12" ) ) ;
594+ assert ! ( should_track_daily_active( Some ( "2026-02-11" ) , "2026-02-12" ) ) ;
595+ }
596+
597+ #[ test]
598+ fn daily_active_key_is_not_version_scoped ( ) {
599+ assert_eq ! ( DAILY_ACTIVE_TRACKED_DAY_KEY , "analytics.daily_active_day" ) ;
600+ assert ! ( !DAILY_ACTIVE_TRACKED_DAY_KEY . contains( "0.6.2" ) ) ;
601+ assert ! ( !DAILY_ACTIVE_TRACKED_DAY_KEY . contains( "0.6.3" ) ) ;
537602 }
538603
539604 #[ test]
540- fn key_is_version_scoped ( ) {
541- let v1_key = app_started_day_key ( "0.6.2" ) ;
542- let v2_key = app_started_day_key ( "0.6.3" ) ;
543- assert_ne ! ( v1_key, v2_key) ;
544- assert ! ( v1_key. ends_with( "0.6.2" ) ) ;
545- assert ! ( v2_key. ends_with( "0.6.3" ) ) ;
605+ fn rollover_sleep_waits_for_next_utc_day_boundary ( ) {
606+ let now = PrimitiveDateTime :: new (
607+ Date :: from_calendar_date ( 2026 , Month :: February , 12 ) . unwrap ( ) ,
608+ Time :: from_hms ( 23 , 59 , 50 ) . unwrap ( ) ,
609+ )
610+ . assume_utc ( ) ;
611+
612+ assert_eq ! ( seconds_until_next_utc_day( now) , 10 ) ;
546613 }
547614}
0 commit comments