@@ -6,8 +6,6 @@ import UIKit
66import AppKit
77#endif
88
9- // TODO: add automatic sending of session length, first install date, distinct days etc. as default parameters
10-
119final class SessionManager : @unchecked Sendable {
1210 private struct StoredSession : Codable {
1311 let startedAt : Date
@@ -22,8 +20,10 @@ final class SessionManager: @unchecked Sendable {
2220
2321 static let shared = SessionManager ( )
2422
25- private static let sessionsKey = " sessions "
26- private static let firstInstallDateKey = " firstInstallDate "
23+ private static let recentSessionsKey = " recentSessions "
24+ private static let deletedSessionsCountKey = " deletedSessionsCount "
25+
26+ private static let firstSessionDateKey = " firstSessionDate "
2727 private static let distinctDaysUsedKey = " distinctDaysUsed "
2828
2929 private static let decoder : JSONDecoder = {
@@ -43,7 +43,51 @@ final class SessionManager: @unchecked Sendable {
4343 return encoder
4444 } ( )
4545
46- private var sessions : [ StoredSession ]
46+ private var recentSessions : [ StoredSession ]
47+
48+ private var deletedSessionsCount : Int {
49+ get { TelemetryDeck . customDefaults? . integer ( forKey: Self . deletedSessionsCountKey) ?? 0 }
50+ set {
51+ self . persistenceQueue. async {
52+ TelemetryDeck . customDefaults? . set ( newValue, forKey: Self . deletedSessionsCountKey)
53+ }
54+ }
55+ }
56+
57+ var totalSessionsCount : Int {
58+ self . recentSessions. count + self . deletedSessionsCount
59+ }
60+
61+ var averageSessionSeconds : Int {
62+ let completedSessions = self . recentSessions. dropLast ( )
63+ let totalCompletedSessionSeconds = completedSessions. map ( \. durationInSeconds) . reduce ( into: 0 ) { $0 + $1 }
64+ return totalCompletedSessionSeconds / completedSessions. count
65+ }
66+
67+ var previousSessionSeconds : Int ? {
68+ self . recentSessions. dropLast ( ) . last? . durationInSeconds
69+ }
70+
71+ var firstSessionDate : String {
72+ get {
73+ TelemetryDeck . customDefaults? . string ( forKey: Self . firstSessionDateKey)
74+ ?? ISO8601DateFormatter . string ( from: Date ( ) , timeZone: . current, formatOptions: [ . withFullDate] )
75+ }
76+ set {
77+ self . persistenceQueue. async {
78+ TelemetryDeck . customDefaults? . set ( newValue, forKey: Self . firstSessionDateKey)
79+ }
80+ }
81+ }
82+
83+ var distinctDaysUsed : [ String ] {
84+ get { TelemetryDeck . customDefaults? . stringArray ( forKey: Self . distinctDaysUsedKey) ?? [ ] }
85+ set {
86+ self . persistenceQueue. async {
87+ TelemetryDeck . customDefaults? . set ( newValue, forKey: Self . distinctDaysUsedKey)
88+ }
89+ }
90+ }
4791
4892 private var currentSessionStartedAt : Date = . distantPast
4993 private var currentSessionDuration : TimeInterval = . zero
@@ -55,14 +99,17 @@ final class SessionManager: @unchecked Sendable {
5599
56100 private init ( ) {
57101 if
58- let existingSessionData = TelemetryDeck . customDefaults? . data ( forKey: Self . sessionsKey ) ,
102+ let existingSessionData = TelemetryDeck . customDefaults? . data ( forKey: Self . recentSessionsKey ) ,
59103 let existingSessions = try ? Self . decoder. decode ( [ StoredSession ] . self, from: existingSessionData)
60104 {
61105 // upon app start, clean up any sessions older than 90 days to keep dict small
62106 let cutoffDate = Date ( ) . addingTimeInterval ( - ( 90 * 24 * 60 * 60 ) )
63- self . sessions = existingSessions. filter { $0. startedAt > cutoffDate }
107+ self . recentSessions = existingSessions. filter { $0. startedAt > cutoffDate }
108+
109+ // Update deleted sessions count
110+ self . deletedSessionsCount += existingSessions. count - self . recentSessions. count
64111 } else {
65- self . sessions = [ ]
112+ self . recentSessions = [ ]
66113 }
67114
68115 self . updateDistinctDaysUsed ( )
@@ -73,19 +120,17 @@ final class SessionManager: @unchecked Sendable {
73120 // stop automatic duration counting of previous session
74121 self . stopSessionTimer ( )
75122
76- // if the sessions are empty, this must be the first start after installing the app
77- if self . sessions . isEmpty {
123+ // if the recent sessions are empty, this must be the first start after installing the app
124+ if self . recentSessions . isEmpty {
78125 // this ensures we only use the date, not the time –> e.g. "2025-01-31"
79126 let todayFormatted = ISO8601DateFormatter . string ( from: Date ( ) , timeZone: . current, formatOptions: [ . withFullDate] )
80127
128+ self . firstSessionDate = todayFormatted
129+
81130 TelemetryDeck . internalSignal (
82131 " TelemetryDeck.Acquisition.newInstallDetected " ,
83132 parameters: [ " TelemetryDeck.Acquisition.firstSessionDate " : todayFormatted]
84133 )
85-
86- self . persistenceQueue. async {
87- TelemetryDeck . customDefaults? . set ( todayFormatted, forKey: Self . firstInstallDateKey)
88- }
89134 }
90135
91136 // start a new session
@@ -124,17 +169,17 @@ final class SessionManager: @unchecked Sendable {
124169 guard self . currentSessionDuration >= 1.0 else { return }
125170
126171 // Add or update the current session
127- if let existingSessionIndex = self . sessions . lastIndex ( where: { $0. startedAt == self . currentSessionStartedAt } ) {
128- self . sessions [ existingSessionIndex] . durationInSeconds = Int ( self . currentSessionDuration)
172+ if let existingSessionIndex = self . recentSessions . lastIndex ( where: { $0. startedAt == self . currentSessionStartedAt } ) {
173+ self . recentSessions [ existingSessionIndex] . durationInSeconds = Int ( self . currentSessionDuration)
129174 } else {
130175 let newSession = StoredSession ( startedAt: self . currentSessionStartedAt, durationInSeconds: Int ( self . currentSessionDuration) )
131- self . sessions . append ( newSession)
176+ self . recentSessions . append ( newSession)
132177 }
133178
134179 // Save changes to UserDefaults without blocking Main thread
135180 self . persistenceQueue. async {
136- if let updatedSessionData = try ? Self . encoder. encode ( self . sessions ) {
137- TelemetryDeck . customDefaults? . set ( updatedSessionData, forKey: Self . sessionsKey )
181+ if let updatedSessionData = try ? Self . encoder. encode ( self . recentSessions ) {
182+ TelemetryDeck . customDefaults? . set ( updatedSessionData, forKey: Self . recentSessionsKey )
138183 }
139184 }
140185 }
@@ -160,22 +205,10 @@ final class SessionManager: @unchecked Sendable {
160205 private func updateDistinctDaysUsed( ) {
161206 let todayFormatted = ISO8601DateFormatter . string ( from: Date ( ) , timeZone: . current, formatOptions: [ . withFullDate] )
162207
163- var distinctDays : [ String ] = [ ]
164- if
165- let existinDaysData = TelemetryDeck . customDefaults? . data ( forKey: Self . distinctDaysUsedKey) ,
166- let existingDays = try ? JSONDecoder ( ) . decode ( [ String ] . self, from: existinDaysData)
167- {
168- distinctDays = existingDays
169- }
170-
208+ var distinctDays = self . distinctDaysUsed
171209 if distinctDays. last != todayFormatted {
172210 distinctDays. append ( todayFormatted)
173-
174- self . persistenceQueue. async {
175- if let updatedDistinctDaysData = try ? JSONEncoder ( ) . encode ( distinctDays) {
176- TelemetryDeck . customDefaults? . set ( updatedDistinctDaysData, forKey: Self . distinctDaysUsedKey)
177- }
178- }
211+ self . distinctDaysUsed = distinctDays
179212 }
180213 }
181214
0 commit comments