Skip to content

Commit

Permalink
Calculate & automatically report more default parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeehut committed Jan 31, 2025
1 parent c35b9bd commit 060e013
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 33 deletions.
99 changes: 66 additions & 33 deletions Sources/TelemetryDeck/Helpers/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import UIKit
import AppKit
#endif

// TODO: add automatic sending of session length, first install date, distinct days etc. as default parameters

final class SessionManager: @unchecked Sendable {
private struct StoredSession: Codable {
let startedAt: Date
Expand All @@ -22,8 +20,10 @@ final class SessionManager: @unchecked Sendable {

static let shared = SessionManager()

private static let sessionsKey = "sessions"
private static let firstInstallDateKey = "firstInstallDate"
private static let recentSessionsKey = "recentSessions"
private static let deletedSessionsCountKey = "deletedSessionsCount"

private static let firstSessionDateKey = "firstSessionDate"
private static let distinctDaysUsedKey = "distinctDaysUsed"

private static let decoder: JSONDecoder = {
Expand All @@ -43,7 +43,51 @@ final class SessionManager: @unchecked Sendable {
return encoder
}()

private var sessions: [StoredSession]
private var recentSessions: [StoredSession]

private var deletedSessionsCount: Int {
get { TelemetryDeck.customDefaults?.integer(forKey: Self.deletedSessionsCountKey) ?? 0 }
set {
self.persistenceQueue.async {
TelemetryDeck.customDefaults?.set(newValue, forKey: Self.deletedSessionsCountKey)
}
}
}

var totalSessionsCount: Int {
self.recentSessions.count + self.deletedSessionsCount
}

var averageSessionSeconds: Int {
let completedSessions = self.recentSessions.dropLast()
let totalCompletedSessionSeconds = completedSessions.map(\.durationInSeconds).reduce(into: 0) { $0 + $1 }
return totalCompletedSessionSeconds / completedSessions.count
}

var previousSessionSeconds: Int? {
self.recentSessions.dropLast().last?.durationInSeconds
}

var firstSessionDate: String {
get {
TelemetryDeck.customDefaults?.string(forKey: Self.firstSessionDateKey)
?? ISO8601DateFormatter.string(from: Date(), timeZone: .current, formatOptions: [.withFullDate])
}
set {
self.persistenceQueue.async {
TelemetryDeck.customDefaults?.set(newValue, forKey: Self.firstSessionDateKey)
}
}
}

var distinctDaysUsed: [String] {
get { TelemetryDeck.customDefaults?.stringArray(forKey: Self.distinctDaysUsedKey) ?? [] }
set {
self.persistenceQueue.async {
TelemetryDeck.customDefaults?.set(newValue, forKey: Self.distinctDaysUsedKey)
}
}
}

private var currentSessionStartedAt: Date = .distantPast
private var currentSessionDuration: TimeInterval = .zero
Expand All @@ -55,14 +99,17 @@ final class SessionManager: @unchecked Sendable {

private init() {
if
let existingSessionData = TelemetryDeck.customDefaults?.data(forKey: Self.sessionsKey),
let existingSessionData = TelemetryDeck.customDefaults?.data(forKey: Self.recentSessionsKey),
let existingSessions = try? Self.decoder.decode([StoredSession].self, from: existingSessionData)
{
// upon app start, clean up any sessions older than 90 days to keep dict small
let cutoffDate = Date().addingTimeInterval(-(90 * 24 * 60 * 60))
self.sessions = existingSessions.filter { $0.startedAt > cutoffDate }
self.recentSessions = existingSessions.filter { $0.startedAt > cutoffDate }

// Update deleted sessions count
self.deletedSessionsCount += existingSessions.count - self.recentSessions.count
} else {
self.sessions = []
self.recentSessions = []
}

self.updateDistinctDaysUsed()
Expand All @@ -73,19 +120,17 @@ final class SessionManager: @unchecked Sendable {
// stop automatic duration counting of previous session
self.stopSessionTimer()

// if the sessions are empty, this must be the first start after installing the app
if self.sessions.isEmpty {
// if the recent sessions are empty, this must be the first start after installing the app
if self.recentSessions.isEmpty {
// this ensures we only use the date, not the time –> e.g. "2025-01-31"
let todayFormatted = ISO8601DateFormatter.string(from: Date(), timeZone: .current, formatOptions: [.withFullDate])

self.firstSessionDate = todayFormatted

TelemetryDeck.internalSignal(
"TelemetryDeck.Acquisition.newInstallDetected",
parameters: ["TelemetryDeck.Acquisition.firstSessionDate": todayFormatted]
)

self.persistenceQueue.async {
TelemetryDeck.customDefaults?.set(todayFormatted, forKey: Self.firstInstallDateKey)
}
}

// start a new session
Expand Down Expand Up @@ -124,17 +169,17 @@ final class SessionManager: @unchecked Sendable {
guard self.currentSessionDuration >= 1.0 else { return }

// Add or update the current session
if let existingSessionIndex = self.sessions.lastIndex(where: { $0.startedAt == self.currentSessionStartedAt }) {
self.sessions[existingSessionIndex].durationInSeconds = Int(self.currentSessionDuration)
if let existingSessionIndex = self.recentSessions.lastIndex(where: { $0.startedAt == self.currentSessionStartedAt }) {
self.recentSessions[existingSessionIndex].durationInSeconds = Int(self.currentSessionDuration)
} else {
let newSession = StoredSession(startedAt: self.currentSessionStartedAt, durationInSeconds: Int(self.currentSessionDuration))
self.sessions.append(newSession)
self.recentSessions.append(newSession)
}

// Save changes to UserDefaults without blocking Main thread
self.persistenceQueue.async {
if let updatedSessionData = try? Self.encoder.encode(self.sessions) {
TelemetryDeck.customDefaults?.set(updatedSessionData, forKey: Self.sessionsKey)
if let updatedSessionData = try? Self.encoder.encode(self.recentSessions) {
TelemetryDeck.customDefaults?.set(updatedSessionData, forKey: Self.recentSessionsKey)
}
}
}
Expand All @@ -160,22 +205,10 @@ final class SessionManager: @unchecked Sendable {
private func updateDistinctDaysUsed() {
let todayFormatted = ISO8601DateFormatter.string(from: Date(), timeZone: .current, formatOptions: [.withFullDate])

var distinctDays: [String] = []
if
let existinDaysData = TelemetryDeck.customDefaults?.data(forKey: Self.distinctDaysUsedKey),
let existingDays = try? JSONDecoder().decode([String].self, from: existinDaysData)
{
distinctDays = existingDays
}

var distinctDays = self.distinctDaysUsed
if distinctDays.last != todayFormatted {
distinctDays.append(todayFormatted)

self.persistenceQueue.async {
if let updatedDistinctDaysData = try? JSONEncoder().encode(distinctDays) {
TelemetryDeck.customDefaults?.set(updatedDistinctDaysData, forKey: Self.distinctDaysUsedKey)
}
}
self.distinctDaysUsed = distinctDays
}
}

Expand Down
49 changes: 49 additions & 0 deletions Sources/TelemetryDeck/Signals/Signal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,16 @@ public struct DefaultSignalPayload: Encodable {
"TelemetryDeck.UserPreference.language": Self.preferredLanguage,
"TelemetryDeck.UserPreference.layoutDirection": Self.layoutDirection,
"TelemetryDeck.UserPreference.region": Self.region,

// Pirate Metrics
"TelemetryDeck.Acquisition.firstSessionDate": SessionManager.shared.firstSessionDate,
"TelemetryDeck.Retention.averageSessionSeconds": "\(SessionManager.shared.averageSessionSeconds)",
"TelemetryDeck.Retention.distinctDaysUsed": "\(SessionManager.shared.distinctDaysUsed.count)",
"TelemetryDeck.Retention.totalSessionsCount": "\(SessionManager.shared.totalSessionsCount)",
]

parameters.merge(self.accessibilityParameters, uniquingKeysWith: { $1 })
parameters.merge(self.calendarParameters, uniquingKeysWith: { $1 })

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

if let previousSessionSeconds = SessionManager.shared.previousSessionSeconds {
parameters["TelemetryDeck.Retention.previousSessionSeconds"] = "\(previousSessionSeconds)"
}

return parameters
}
}

// MARK: - Helpers

extension DefaultSignalPayload {
static var calendarParameters: [String: String] {
let calendar = Calendar(identifier: .gregorian)
let now = Date()

// Get components for all the metrics we need
let components = calendar.dateComponents(
[.day, .weekday, .weekOfYear, .month, .hour, .quarter, .yearForWeekOfYear],
from: now
)

// Calculate day of year
let dayOfYear = calendar.ordinality(of: .day, in: .year, for: now) ?? -1

// Convert Sunday=1..Saturday=7 to Monday=1..Sunday=7
let dayOfWeek = components.weekday.map { $0 == 1 ? 7 : $0 - 1 } ?? -1

// Weekend is now days 6 (Saturday) and 7 (Sunday)
let isWeekend = dayOfWeek >= 6

return [
// Day-based metrics
"TelemetryDeck.Calendar.dayOfMonth": "\(components.day ?? -1)",
"TelemetryDeck.Calendar.dayOfWeek": "\(dayOfWeek)", // 1 = Monday, 7 = Sunday
"TelemetryDeck.Calendar.dayOfYear": "\(dayOfYear)",

// Week-based metrics
"TelemetryDeck.Calendar.weekOfYear": "\(components.weekOfYear ?? -1)",
"TelemetryDeck.Calendar.isWeekend": "\(isWeekend)",

// Month and quarter
"TelemetryDeck.Calendar.monthOfYear": "\(components.month ?? -1)",
"TelemetryDeck.Calendar.quarterOfYear": "\(components.quarter ?? -1)",

// Hours in 1-24 format
"TelemetryDeck.Calendar.hourOfDay": "\((components.hour ?? -1) + 1)"
]
}

@MainActor
static var accessibilityParameters: [String: String] {
var a11yParams: [String: String] = [:]
Expand Down

0 comments on commit 060e013

Please sign in to comment.