浏览代码

Fix Telemetry daily send for continous pings

Deniz Cengiz 1 天之前
父节点
当前提交
512b4d46d8
共有 2 个文件被更改,包括 53 次插入4 次删除
  1. 22 4
      Trio/Sources/Application/AppDelegate.swift
  2. 31 0
      Trio/Sources/Services/Telemetry/TelemetryClient.swift

+ 22 - 4
Trio/Sources/Application/AppDelegate.swift

@@ -20,21 +20,39 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(crashReportingEnabled)
         Crashlytics.crashlytics().setCustomValue(Bundle.main.appDevVersion ?? "unknown", forKey: "app_dev_version")
 
-        // Telemetry: record this cold launch into the sliding 7-day window. If
-        // consent is set and the build SHA changed since the last successful
-        // send, fire an immediate ping — the 24h scheduler can't notice a
-        // build update on its own. Then arm the recurring 24h timer.
+        // Telemetry: record this cold launch into the sliding 7-day window,
+        // then drive cadence via three layered triggers — listed below in
+        // priority of reliability:
+        //
+        //   1. SHA-change ping: build updated since last send. Awaited so
+        //      the lastSentAt stamp is fresh before the overdue check.
+        //   2. checkAndSendIfOverdue: covers the regular cold launch on the
+        //      same build when >24h has passed since the last successful
+        //      send. Together with the foreground-transition hook below
+        //      (`applicationWillEnterForeground`), this keeps daily pings
+        //      flowing on iOS.
+        //   3. scheduleRecurring: best-effort fallback for the rare case
+        //      where the app stays foregrounded for a full 24h.
         TelemetryClient.shared.recordColdLaunch()
         Task.detached {
             if TelemetryClient.shared.buildShaChangedSinceLastSend() {
                 await TelemetryClient.shared.maybeSend()
             }
             TelemetryClient.shared.scheduleRecurring()
+            TelemetryClient.shared.checkAndSendIfOverdue()
         }
 
         return true
     }
 
+    /// Foreground-transition entry point for telemetry cadence. Re-evaluates
+    /// the overdue window every time the user brings Trio to the foreground,
+    /// since `scheduleRecurring`'s GCD timer doesn't fire while suspended.
+    /// No-op if a send already landed within the last 24h.
+    func applicationWillEnterForeground(_: UIApplication) {
+        TelemetryClient.shared.checkAndSendIfOverdue()
+    }
+
     func application(
         _: UIApplication,
         didReceiveRemoteNotification userInfo: [AnyHashable: Any],

+ 31 - 0
Trio/Sources/Services/Telemetry/TelemetryClient.swift

@@ -104,6 +104,12 @@ final class TelemetryClient: Injectable {
     /// Arms (or re-arms) the 24h send timer. Idempotent. Bails out without
     /// scheduling if the user hasn't decided on consent yet or has opted out
     /// — there's nothing for the timer to do.
+    ///
+    /// Best-effort fallback only. GCD timers don't advance while the app is
+    /// suspended, so on iOS this effectively means "fires only if the app
+    /// stays foregrounded for 24h." The reliable cadence driver is
+    /// `checkAndSendIfOverdue()` called on every foreground transition and
+    /// cold launch.
     func scheduleRecurring() {
         guard PropertyPersistentFlags.shared.telemetryConsentDecisionMade == true,
               PropertyPersistentFlags.shared.telemetryEnabled == true
@@ -124,6 +130,31 @@ final class TelemetryClient: Injectable {
         }
     }
 
+    /// If consent is set and we haven't successfully sent within the last 24h
+    /// (or have never sent), fire a send. Called on foreground transitions
+    /// and from the cold-launch path so daily cadence is kept.
+    ///
+    /// Mirrors the pattern used by LoopFollow's `TaskScheduler.checkTasksNow()`:
+    /// wall-clock comparison against `telemetryLastSentAt`, fire-and-forget
+    /// if overdue. Safe to call repeatedly — if a send already fired within
+    /// the window, this is a no-op.
+    func checkAndSendIfOverdue() {
+        guard PropertyPersistentFlags.shared.telemetryConsentDecisionMade == true,
+              PropertyPersistentFlags.shared.telemetryEnabled == true
+        else {
+            return
+        }
+
+        let lastSent = PropertyPersistentFlags.shared.telemetryLastSentAt
+        let overdue: Bool = {
+            guard let lastSent else { return true }
+            return Date().timeIntervalSince(lastSent) >= Self.dailyInterval
+        }()
+        guard overdue else { return }
+
+        Task.detached { await self.maybeSend() }
+    }
+
     /// Single entry point for all sends (scheduler tick, consent-yes, startup
     /// SHA-change). Gated on consent + opt-in. *When* to send is the caller's
     /// decision — startup handles the SHA-change shortcut, the timer handles