From c685bb23540ea952da4f0559a4d019a615178882 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 13 Oct 2026 11:17:27 -0500 Subject: [PATCH] Count backfilled glucose in the daily analytics summary The daily summary replaces cgmFetchCount, which only counted readings fetched by polling and missed CGMs that push readings, with counts of CGM readings actually stored: glucoseCount, backfillCount, and maxBackfillAgeMinutes (the longest gap between a backfilled reading's time and when Loop received it). --- Loop/Managers/AnalyticsServicesManager.swift | 23 ++++++++++++++++---- Loop/Managers/DeviceDataManager.swift | 8 +++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Loop/Managers/AnalyticsServicesManager.swift b/Loop/Managers/AnalyticsServicesManager.swift index 3a68ee2e9..fdbef3dc7 100644 --- a/Loop/Managers/AnalyticsServicesManager.swift +++ b/Loop/Managers/AnalyticsServicesManager.swift @@ -187,8 +187,19 @@ final class AnalyticsServicesManager { // MARK: - Daily Summary @MainActor - func didFetchNewCGMData() { - updateDailySummary { $0.cgmFetchCount += 1 } + func didStoreGlucose(_ samples: [StoredGlucoseSample]) { + updateDailySummary { summary in + for sample in samples where !sample.wasUserEntered { + summary.glucoseCount += 1 + guard sample.isBackfill else { + continue + } + summary.backfillCount += 1 + if let receivedDate = sample.receivedDate { + summary.maxBackfillAge = max(summary.maxBackfillAge, receivedDate.timeIntervalSince(sample.startDate)) + } + } + } } @MainActor @@ -321,7 +332,9 @@ struct AnalyticsDailySummary: Codable, Equatable { var totalLoopDuration: TimeInterval = 0 var maxLoopDuration: TimeInterval = 0 var loopErrorCounts: [String: Int] = [:] - var cgmFetchCount = 0 + var glucoseCount = 0 + var backfillCount = 0 + var maxBackfillAge: TimeInterval = 0 var state: AnalyticsState? var properties: [AnyHashable: Any] { @@ -333,7 +346,9 @@ struct AnalyticsDailySummary: Codable, Equatable { "maxLoopDuration": maxLoopDuration, "loopErrorCount": loopErrorCounts.values.reduce(0, +), "loopErrorCounts": loopErrorCounts, - "cgmFetchCount": cgmFetchCount + "glucoseCount": glucoseCount, + "backfillCount": backfillCount, + "maxBackfillAgeMinutes": Int(maxBackfillAge.minutes) ] if let state { properties["closedLoop"] = state.closedLoop diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 3e994138b..49625bd26 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -471,7 +471,9 @@ final class DeviceDataManager { switch readingResult { case .newData(let values): do { - storedNewGlucose = try await !glucoseStore.addGlucoseSamples(values).isEmpty + let storedSamples = try await glucoseStore.addGlucoseSamples(values) + storedNewGlucose = !storedSamples.isEmpty + analyticsServicesManager.didStoreGlucose(storedSamples) } catch { log.error("Unable to store glucose: %{public}@", String(describing: error)) } @@ -631,10 +633,6 @@ final class DeviceDataManager { let result = await cgmManager.fetchNewDataIfNeeded() - if case .newData = result { - self.analyticsServicesManager.didFetchNewCGMData() - } - let storedNewGlucose = await self.processCGMReadingResult(cgmManager, readingResult: result) // A heartbeat/refresh triggers a CGM check; only loop if that check produced a new value