From 778c473631a12b01a52d59fb555cfac763fe7fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Tue, 28 Jul 2026 12:52:07 +0200 Subject: [PATCH] added telemetry call on app resume with 15 minute cooldown --- .../telemetry_heartbeat.dart | 8 +++--- lib/app.dart | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart b/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart index 5fb367f..8a8b2a5 100644 --- a/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart +++ b/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart @@ -12,8 +12,9 @@ import 'telemetry_device_id.dart'; /// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) — /// upserts the stable install id, platform, app version and device info. Sent -/// once on app start and again once push registration completes that session -/// (so a fresh registration isn't under-reported until the next launch). +/// on app start, again once push registration completes that session (so a +/// fresh registration isn't under-reported until the next launch), and on +/// resume after the app spent more than 15 minutes in the background. /// Bearer-authenticated via the shared dio interceptor. Replaces the legacy /// mhsl.eu `server/userIndex/update` call. class TelemetryHeartbeat extends MarianumConnectQuery { @@ -21,7 +22,8 @@ class TelemetryHeartbeat extends MarianumConnectQuery { /// Fire-and-forget: schedules a heartbeat and swallows any error, so a failed /// send never disrupts app start. Used from the app shell's initState and - /// re-emitted once push registration completes (see `_MainState._syncPush`). + /// lifecycle handler, and re-emitted once push registration completes (see + /// `_MainState._syncPush`). static void report({required bool notificationsEnabled}) { unawaited( TelemetryHeartbeat() diff --git a/lib/app.dart b/lib/app.dart index 8ce0b4f..deb94ac 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -46,9 +46,11 @@ class _AppState extends State with WidgetsBindingObserver { int _knownTotalTabs = 1; int _lastTabIndex = 0; bool _userOnLastTab = false; + DateTime? _lastTelemetryAt; static const Duration _chatListActiveInterval = Duration(seconds: 15); static const Duration _chatListIdleInterval = Duration(seconds: 60); + static const Duration _telemetryInterval = Duration(minutes: 15); void _onTabControllerChanged() { final newIndex = Main.bottomNavigator.index; @@ -73,10 +75,29 @@ class _AppState extends State with WidgetsBindingObserver { if (talkIsActive) bloc.refresh(); } + // Wall-clock throttle rather than Debouncer.throttle: a Timer does not tick + // reliably while the app is suspended, so the window would still be open on + // the resume it is supposed to let through. + void _reportTelemetry() { + if (!mounted) return; + final now = DateTime.now(); + final last = _lastTelemetryAt; + if (last != null && now.difference(last) < _telemetryInterval) return; + _lastTelemetryAt = now; + TelemetryHeartbeat.report( + notificationsEnabled: context + .read() + .val() + .notificationSettings + .enabled, + ); + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { log('AppLifecycle: $state'); if (state == AppLifecycleState.resumed) { + _reportTelemetry(); Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () { if (!mounted) return; log('Refreshing due to LifecycleChange'); @@ -178,10 +199,7 @@ class _AppState extends State with WidgetsBindingObserver { if (mounted) setState(() {}); }); - TelemetryHeartbeat.report( - notificationsEnabled: - context.read().val().notificationSettings.enabled, - ); + _reportTelemetry(); // A refreshed FCM token invalidates the existing push subscription — the // NC device identifier stays stable, so we simply re-register (NC first,