Skip to content

Commit 34d044d

Browse files
committed
Calculate & automatically report more default parameters
1 parent 0036d04 commit 34d044d

File tree

2 files changed

+115
-33
lines changed

2 files changed

+115
-33
lines changed

Sources/TelemetryDeck/Helpers/SessionManager.swift

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import UIKit
66
import AppKit
77
#endif
88

9-
// TODO: add automatic sending of session length, first install date, distinct days etc. as default parameters
10-
119
final 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

Sources/TelemetryDeck/Signals/Signal.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,16 @@ public struct DefaultSignalPayload: Encodable {
9999
"TelemetryDeck.UserPreference.language": Self.preferredLanguage,
100100
"TelemetryDeck.UserPreference.layoutDirection": Self.layoutDirection,
101101
"TelemetryDeck.UserPreference.region": Self.region,
102+
103+
// Pirate Metrics
104+
"TelemetryDeck.Acquisition.firstSessionDate": SessionManager.shared.firstSessionDate,
105+
"TelemetryDeck.Retention.averageSessionSeconds": "\(SessionManager.shared.averageSessionSeconds)",
106+
"TelemetryDeck.Retention.distinctDaysUsed": "\(SessionManager.shared.distinctDaysUsed.count)",
107+
"TelemetryDeck.Retention.totalSessionsCount": "\(SessionManager.shared.totalSessionsCount)",
102108
]
103109

104110
parameters.merge(self.accessibilityParameters, uniquingKeysWith: { $1 })
111+
parameters.merge(self.calendarParameters, uniquingKeysWith: { $1 })
105112

106113
if let extensionIdentifier = Self.extensionIdentifier {
107114
// deprecated name
@@ -111,13 +118,55 @@ public struct DefaultSignalPayload: Encodable {
111118
parameters["TelemetryDeck.RunContext.extensionIdentifier"] = extensionIdentifier
112119
}
113120

121+
if let previousSessionSeconds = SessionManager.shared.previousSessionSeconds {
122+
parameters["TelemetryDeck.Retention.previousSessionSeconds"] = "\(previousSessionSeconds)"
123+
}
124+
114125
return parameters
115126
}
116127
}
117128

118129
// MARK: - Helpers
119130

120131
extension DefaultSignalPayload {
132+
static var calendarParameters: [String: String] {
133+
let calendar = Calendar(identifier: .gregorian)
134+
let now = Date()
135+
136+
// Get components for all the metrics we need
137+
let components = calendar.dateComponents(
138+
[.day, .weekday, .weekOfYear, .month, .hour, .quarter, .yearForWeekOfYear],
139+
from: now
140+
)
141+
142+
// Calculate day of year
143+
let dayOfYear = calendar.ordinality(of: .day, in: .year, for: now) ?? -1
144+
145+
// Convert Sunday=1..Saturday=7 to Monday=1..Sunday=7
146+
let dayOfWeek = components.weekday.map { $0 == 1 ? 7 : $0 - 1 } ?? -1
147+
148+
// Weekend is now days 6 (Saturday) and 7 (Sunday)
149+
let isWeekend = dayOfWeek >= 6
150+
151+
return [
152+
// Day-based metrics
153+
"TelemetryDeck.Calendar.dayOfMonth": "\(components.day ?? -1)",
154+
"TelemetryDeck.Calendar.dayOfWeek": "\(dayOfWeek)", // 1 = Monday, 7 = Sunday
155+
"TelemetryDeck.Calendar.dayOfYear": "\(dayOfYear)",
156+
157+
// Week-based metrics
158+
"TelemetryDeck.Calendar.weekOfYear": "\(components.weekOfYear ?? -1)",
159+
"TelemetryDeck.Calendar.isWeekend": "\(isWeekend)",
160+
161+
// Month and quarter
162+
"TelemetryDeck.Calendar.monthOfYear": "\(components.month ?? -1)",
163+
"TelemetryDeck.Calendar.quarterOfYear": "\(components.quarter ?? -1)",
164+
165+
// Hours in 1-24 format
166+
"TelemetryDeck.Calendar.hourOfDay": "\((components.hour ?? -1) + 1)"
167+
]
168+
}
169+
121170
@MainActor
122171
static var accessibilityParameters: [String: String] {
123172
var a11yParams: [String: String] = [:]

0 commit comments

Comments
 (0)