Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
1D63DEA526E950D400F46FA5 /* SupportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D63DEA426E950D400F46FA5 /* SupportManager.swift */; };
1D6B1B6726866D89009AC446 /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */; };
1D70C40126EC0F9D00C62570 /* SupportManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */; };
A7E1C0B22F8A4D2E00C0FFEE /* AnalyticsServicesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E1C0B12F8A4D2E00C0FFEE /* AnalyticsServicesManagerTests.swift */; };
1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D80313C24746274002810DF /* AlertStoreTests.swift */; };
1D82E6A025377C6B009131FB /* TrustedTimeChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */; };
1D8D55BC252274650044DBB6 /* BolusEntryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8D55BB252274650044DBB6 /* BolusEntryViewModelTests.swift */; };
Expand Down Expand Up @@ -635,7 +636,9 @@
C1ED6C7E2E7C811A002F91C2 /* SetPresetUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ED6C7C2E7C8113002F91C2 /* SetPresetUserInfo.swift */; };
C1ED6C802E7C9C86002F91C2 /* PendingPresetReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ED6C7F2E7C9C7A002F91C2 /* PendingPresetReminder.swift */; };
C1ED6C822E7D8E3D002F91C2 /* AcknowledgeAlertUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ED6C812E7D8E36002F91C2 /* AcknowledgeAlertUserInfo.swift */; };
B5C0FFEE2F8A4D2E00A1B2C2 /* WatchUsageSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C0FFEE2F8A4D2E00A1B2C1 /* WatchUsageSummary.swift */; };
C1ED6C832E7D8E3D002F91C2 /* AcknowledgeAlertUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ED6C812E7D8E36002F91C2 /* AcknowledgeAlertUserInfo.swift */; };
B5C0FFEE2F8A4D2E00A1B2C3 /* WatchUsageSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C0FFEE2F8A4D2E00A1B2C1 /* WatchUsageSummary.swift */; };
C1EE9E812A38D0FB0064784A /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = C1EE9E802A38D0FB0064784A /* BuildDetails.plist */; };
C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */; };
C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; };
Expand Down Expand Up @@ -898,6 +901,7 @@
1D63DEA426E950D400F46FA5 /* SupportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportManager.swift; sourceTree = "<group>"; };
1D6B1B6626866D89009AC446 /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportManagerTests.swift; sourceTree = "<group>"; };
A7E1C0B12F8A4D2E00C0FFEE /* AnalyticsServicesManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsServicesManagerTests.swift; sourceTree = "<group>"; };
1D80313C24746274002810DF /* AlertStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStoreTests.swift; sourceTree = "<group>"; };
1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedTimeChecker.swift; sourceTree = "<group>"; };
1D8D55BB252274650044DBB6 /* BolusEntryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusEntryViewModelTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1463,6 +1467,7 @@
C1ED6C7C2E7C8113002F91C2 /* SetPresetUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetPresetUserInfo.swift; sourceTree = "<group>"; };
C1ED6C7F2E7C9C7A002F91C2 /* PendingPresetReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingPresetReminder.swift; sourceTree = "<group>"; };
C1ED6C812E7D8E36002F91C2 /* AcknowledgeAlertUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgeAlertUserInfo.swift; sourceTree = "<group>"; };
B5C0FFEE2F8A4D2E00A1B2C1 /* WatchUsageSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchUsageSummary.swift; sourceTree = "<group>"; };
C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = "<group>"; };
C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1746,6 +1751,7 @@
E9B3552C293592B40076AB04 /* MealDetectionManagerTests.swift */,
C1DA43562B1A70BE00CBD33F /* SettingsManagerTests.swift */,
1D70C40026EC0F9D00C62570 /* SupportManagerTests.swift */,
A7E1C0B12F8A4D2E00C0FFEE /* AnalyticsServicesManagerTests.swift */,
C129BF492B2791EE00DF15CB /* TemporaryPresetsManagerTests.swift */,
A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */,
);
Expand Down Expand Up @@ -2875,6 +2881,7 @@
children = (
C1DCEDF32E999D5A001A7BB0 /* LastManualBolus.swift */,
C1ED6C812E7D8E36002F91C2 /* AcknowledgeAlertUserInfo.swift */,
B5C0FFEE2F8A4D2E00A1B2C1 /* WatchUsageSummary.swift */,
A9347F3024E7521800C99C34 /* CarbBackfillRequestUserInfo.swift */,
4F11D3BF20DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift */,
E9B08020253BBDE900BAD8F8 /* IntentExtensionInfo.swift */,
Expand Down Expand Up @@ -3994,6 +4001,7 @@
buildActionMask = 2147483647;
files = (
C1ED6C822E7D8E3D002F91C2 /* AcknowledgeAlertUserInfo.swift in Sources */,
B5C0FFEE2F8A4D2E00A1B2C2 /* WatchUsageSummary.swift in Sources */,
E9C00EF324C6222400628F35 /* LoopSettings.swift in Sources */,
C1ED6C6D2E7C6EC1002F91C2 /* NotificationActionSelection.swift in Sources */,
C1ED6C772E7C6F58002F91C2 /* WatchPredictedGlucose.swift in Sources */,
Expand Down Expand Up @@ -4030,6 +4038,7 @@
buildActionMask = 2147483647;
files = (
C1ED6C832E7D8E3D002F91C2 /* AcknowledgeAlertUserInfo.swift in Sources */,
B5C0FFEE2F8A4D2E00A1B2C3 /* WatchUsageSummary.swift in Sources */,
C1ED6C652E7C6E2F002F91C2 /* CarbBackfillRequestUserInfo.swift in Sources */,
C1ED6C6C2E7C6EC1002F91C2 /* NotificationActionSelection.swift in Sources */,
E9C00EF224C6221B00628F35 /* LoopSettings.swift in Sources */,
Expand Down Expand Up @@ -4083,6 +4092,7 @@
1DA7A84424477698008257F0 /* InAppModalAlertSchedulerTests.swift in Sources */,
C1DA43532B19310A00CBD33F /* LoopControlMock.swift in Sources */,
1D70C40126EC0F9D00C62570 /* SupportManagerTests.swift in Sources */,
A7E1C0B22F8A4D2E00C0FFEE /* AnalyticsServicesManagerTests.swift in Sources */,
E93E86A824DDCC4400FF40C8 /* MockDoseStore.swift in Sources */,
B4D4534128E5CA7900F1A8D9 /* AlertMuterTests.swift in Sources */,
E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Loop/Managers/Alerts/AlertManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ extension AlertManager: AlertManagerResponder {

func acknowledgeAlert(identifier: Alert.Identifier) async throws {
self.log.default("acknowledgeAlert called for identifier %{public}@", String(describing: identifier))
analyticsServicesManager.didAcknowledgeAlert()
if let responder = responders[identifier.managerIdentifier]?.value {
do {
try await responder.acknowledgeAlert(alertIdentifier: identifier.alertIdentifier)
Expand Down
132 changes: 132 additions & 0 deletions Loop/Managers/AnalyticsServicesManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,92 @@ final class AnalyticsServicesManager {

// MARK: - Screens

@MainActor
func didDisplayBolusScreen() {
logEvent("Bolus Screen")
updateDailySummary { $0.usage.screenCounts["bolus", default: 0] += 1 }
}

@MainActor
func didDisplayCarbEntryScreen() {
logEvent("Carb Entry Screen")
updateDailySummary { $0.usage.screenCounts["carbEntry", default: 0] += 1 }
}

@MainActor
func didDisplaySettingsScreen() {
logEvent("Settings Screen")
updateDailySummary { $0.usage.screenCounts["settings", default: 0] += 1 }
}

@MainActor
func didDisplayStatusScreen() {
logEvent("Status Screen")
updateDailySummary { $0.usage.screenCounts["status", default: 0] += 1 }
}

// MARK: - Foreground Use

private var foregroundStart: Date?

private var foregroundTouchCount = 0

@MainActor
func didEnterForeground(at date: Date = Date()) {
foregroundStart = date
foregroundTouchCount = 0
}

@MainActor
func didEnterBackground(at date: Date = Date()) {
guard let foregroundStart else {
return
}
self.foregroundStart = nil
let duration = max(0, date.timeIntervalSince(foregroundStart))
let touchCount = foregroundTouchCount
updateDailySummary({ $0.addForegroundSession(duration: duration, touchCount: touchCount) }, now: date)
}

@MainActor
func didReceiveTouch() {
foregroundTouchCount += 1
}

@MainActor
func didOpenFromNotification() {
updateDailySummary { $0.usage.notificationOpenCount += 1 }
}

@MainActor
func didOpenFromLink() {
updateDailySummary { $0.usage.linkOpenCount += 1 }
}

@MainActor
func didAcknowledgeAlert() {
updateDailySummary { $0.usage.alertAcknowledgementCount += 1 }
}

// MARK: - Watch Use

@MainActor
func didRequestWatchContext() {
updateDailySummary { $0.usage.watchContextRequestCount += 1 }
}

@MainActor
func didAcknowledgeAlertOnWatch() {
updateDailySummary { $0.usage.watchAlertAcknowledgementCount += 1 }
}

@MainActor
func didStartPresetOnWatch() {
updateDailySummary { $0.usage.watchPresetCount += 1 }
}

func didReceiveWatchUsage(_ summary: WatchUsageSummary) {
logEvent("Watch daily summary", withProperties: summary.properties, outOfSession: true)
}

// MARK: - Config Events
Expand Down Expand Up @@ -182,16 +254,20 @@ final class AnalyticsServicesManager {
logEvent("CGM Added", withProperties: ["identifier" : identifier])
}

@MainActor
func didAddCarbs(source: String, isFavoriteFood: Bool = false) {
logEvent("Carb entry created", withProperties: ["source" : source, "isFavoriteFood": isFavoriteFood], outOfSession: source != "Phone")
updateDailySummary { $0.usage.carbEntryCounts[source, default: 0] += 1 }
}

func didRetryBolus() {
logEvent("Bolus Retry")
}

@MainActor
func didBolus(source: String) {
logEvent("Bolus set", withProperties: ["source" : source], outOfSession: source != "Phone")
updateDailySummary { $0.usage.bolusCounts[source, default: 0] += 1 }
}

// MARK: - Daily Summary
Expand Down Expand Up @@ -284,6 +360,7 @@ final class AnalyticsServicesManager {
logEvent("Alert Displayed", withProperties: ["identifier": identifier, "interruptionLevel": interruptionLevel.rawValue, "scheduled": scheduled], outOfSession: scheduled)
}

@MainActor
func didEnactOverride(kind: String, duration: TemporaryScheduleOverride.Duration, activityType: String? = nil) {
var properties: [String: Any] = ["kind": kind]

Expand All @@ -297,6 +374,7 @@ final class AnalyticsServicesManager {
properties["activityType"] = activityType

logEvent("Override Enacted", withProperties: properties)
updateDailySummary { $0.usage.overrideCounts[kind, default: 0] += 1 }
}

func didEndOverride(kind: String) {
Expand Down Expand Up @@ -369,6 +447,21 @@ struct AnalyticsDailySummary: Codable, Equatable {
var backfillCount = 0
var maxBackfillAge: TimeInterval = 0
var state: AnalyticsState?
private var storedUsage: AnalyticsUsage?

init(day: Date) {
self.day = day
}

var usage: AnalyticsUsage {
get { storedUsage ?? AnalyticsUsage() }
set { storedUsage = newValue }
}

mutating func addForegroundSession(duration: TimeInterval, touchCount: Int) {
usage.foreground.add(duration: duration)
usage.touchCount += touchCount
}

var properties: [AnyHashable: Any] {
var properties: [AnyHashable: Any] = [
Expand All @@ -390,6 +483,45 @@ struct AnalyticsDailySummary: Codable, Equatable {
properties["glucoseBasedPartialApplication"] = state.glucoseBasedPartialApplication
properties["guardAndRunway"] = state.guardAndRunway
}
properties.merge(usage.properties) { current, _ in current }
return properties
}
}

struct AnalyticsUsage: Codable, Equatable {
var foreground = ForegroundSessions()
var touchCount = 0
var notificationOpenCount = 0
var linkOpenCount = 0
var alertAcknowledgementCount = 0
var screenCounts: [String: Int] = [:]
var bolusCounts: [String: Int] = [:]
var carbEntryCounts: [String: Int] = [:]
var overrideCounts: [String: Int] = [:]
var watchContextRequestCount = 0
var watchAlertAcknowledgementCount = 0
var watchPresetCount = 0

var properties: [AnyHashable: Any] {
var properties: [AnyHashable: Any] = [
"touchCount": touchCount,
"notificationOpenCount": notificationOpenCount,
"linkOpenCount": linkOpenCount,
"alertAcknowledgementCount": alertAcknowledgementCount,
"screenCounts": screenCounts,
"bolusCount": bolusCounts.values.reduce(0, +),
"bolusCounts": bolusCounts,
"carbEntryCount": carbEntryCounts.values.reduce(0, +),
"carbEntryCounts": carbEntryCounts,
"overrideCount": overrideCounts.values.reduce(0, +),
"overrideCounts": overrideCounts,
"watchContextRequestCount": watchContextRequestCount,
"watchAlertAcknowledgementCount": watchAlertAcknowledgementCount,
"watchPresetCount": watchPresetCount
]
for (key, value) in foreground.properties(prefix: "foreground") {
properties[key] = value
}
return properties
}
}
Expand Down
17 changes: 16 additions & 1 deletion Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,18 @@ class LoopAppManager: NSObject {
WidgetCenter.shared.reloadAllTimelines()
}

func willEnterForeground() {
analyticsServicesManager.didEnterForeground()
}

func didEnterBackground() {
analyticsServicesManager.didEnterBackground()
}

func didReceiveTouch() {
analyticsServicesManager.didReceiveTouch()
}

// MARK: - Remote Notification

func remoteNotificationRegistrationDidFinish(_ result: Swift.Result<Data,Error>) {
Expand Down Expand Up @@ -741,7 +753,9 @@ class LoopAppManager: NSObject {
guard let deeplink = Deeplink(url: url) else {
return false
}


analyticsServicesManager.didOpenFromLink()

switch deeplink {
case let .carbEntry(carbEntryLink):
if let carbEntryLink {
Expand Down Expand Up @@ -981,6 +995,7 @@ extension LoopAppManager: UNUserNotificationCenterDelegate {
}

case UNNotificationDefaultActionIdentifier:
analyticsServicesManager.didOpenFromNotification()
guard response.notification.request.identifier == LoopNotificationCategory.missedMeal.rawValue else {
break
}
Expand Down
1 change: 1 addition & 0 deletions Loop/Managers/TemporaryPresetsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import os.log
import LoopCore
import LoopAlgorithm

@MainActor
protocol PresetActivationObserver: AnyObject {
func presetActivated(context: TemporaryScheduleOverride.Context, duration: TemporaryScheduleOverride.Duration)
func presetDeactivated(context: TemporaryScheduleOverride.Context)
Expand Down
14 changes: 13 additions & 1 deletion Loop/Managers/WatchDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ final class WatchDataManager: NSObject {
if let userInfo = SetPresetUserInfo(rawValue: message) {
if let presetIdentifier = userInfo.presetIdentifier {
temporaryPresetsManager.startPreset(withIdentifier: presetIdentifier)
analyticsServicesManager?.didStartPresetOnWatch()
} else {
temporaryPresetsManager.clearOverride()
}
Expand All @@ -502,6 +503,7 @@ final class WatchDataManager: NSObject {
log.default("Acknowledge alert from watch: %{public}@", String(describing: message))
if let userInfo = AcknowledgeAlertUserInfo(rawValue: message) {
let id = Alert.Identifier(managerIdentifier: userInfo.managerIdentifier, alertIdentifier: userInfo.alertIdentifier)
analyticsServicesManager?.didAcknowledgeAlertOnWatch()
try? await alertManager.acknowledgeAlert(identifier: id)
}
return [:]
Expand Down Expand Up @@ -530,6 +532,7 @@ final class WatchDataManager: NSObject {
return [:]
}
case WatchContextRequestUserInfo.name?:
analyticsServicesManager?.didRequestWatchContext()
return await createWatchContext(reason: "contextRequest").rawValue
case NotificationActionSelection.name?:
if let selection = NotificationActionSelection(rawValue: message) {
Expand Down Expand Up @@ -561,7 +564,16 @@ extension WatchDataManager: WCSessionDelegate {
}

nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
assertionFailure("We currently don't expect any userInfo messages transferred from the watch side")
guard let summary = WatchUsageSummaryUserInfo(rawValue: userInfo)?.summary else {
let name = String(describing: userInfo["name"])
Task { @MainActor in
self.log.error("Unexpected userInfo from watch: %{public}@", name)
}
return
}
Task { @MainActor in
self.analyticsServicesManager?.didReceiveWatchUsage(summary)
}
}

nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
Expand Down
Loading