Skip to content

Commit

Permalink
Merge pull request #233 from TelemetryDeck/feature/distinct-days-last…
Browse files Browse the repository at this point in the history
…-month

Add `distinctDaysUsedLastMonth` to better detect user engagement levels
  • Loading branch information
Jeehut authored Feb 17, 2025
2 parents 5f81340 + 731749e commit 9c6644d
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 0 deletions.
24 changes: 24 additions & 0 deletions Sources/TelemetryDeck/Helpers/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ final class SessionManager: @unchecked Sendable {
}
}

var distinctDaysUsedLastMonthCount: Int {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withFullDate]

// Get date 30 days ago
let thirtyDaysAgoDate = Date().addingTimeInterval(-(30 * 24 * 60 * 60))
let thirtyDaysAgoFormatted = dateFormatter.string(from: thirtyDaysAgoDate)

return self.distinctDaysUsed.countISODatesOnOrAfter(cutoffISODate: thirtyDaysAgoFormatted)
}

private var currentSessionStartedAt: Date = .distantPast
private var currentSessionDuration: TimeInterval = .zero

Expand Down Expand Up @@ -263,3 +274,16 @@ final class SessionManager: @unchecked Sendable {
#endif
}
}

extension [String] {
/// Counts ISO-formatted date strings (YYYY-MM-DD) that are on or after the given date.
/// Uses string comparison since ISO dates sort alphabetically like dates chronologically.
///
/// - Parameter cutoffISODate: The ISO date string to compare against
/// - Returns: Count of dates on or after the cutoff
func countISODatesOnOrAfter(cutoffISODate: String) -> Int {
// Simply filter strings that are >= the cutoff date string
// (works because: String compares alphabetically & ISO date format sorts dates alphabetically)
self.filter { $0 >= cutoffISODate }.count
}
}
1 change: 1 addition & 0 deletions Sources/TelemetryDeck/Signals/Signal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public struct DefaultSignalPayload: Encodable {
"TelemetryDeck.Acquisition.firstSessionDate": SessionManager.shared.firstSessionDate,
"TelemetryDeck.Retention.averageSessionSeconds": "\(SessionManager.shared.averageSessionSeconds)",
"TelemetryDeck.Retention.distinctDaysUsed": "\(SessionManager.shared.distinctDaysUsed.count)",
"TelemetryDeck.Retention.distinctDaysUsedLastMonth": "\(SessionManager.shared.distinctDaysUsedLastMonthCount)",
"TelemetryDeck.Retention.totalSessionsCount": "\(SessionManager.shared.totalSessionsCount)",
],
uniquingKeysWith: { $1 }
Expand Down
86 changes: 86 additions & 0 deletions Tests/TelemetryDeckTests/ArrayExtensionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@testable import TelemetryDeck
import Testing

enum ArrayExtensionTests {
enum CountISODatesOnOrAfter {
@Test
static func typicalCase() {
let dates = ["2025-01-01", "2025-01-15", "2025-02-01", "2025-03-01"]

#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2025-01-15") == 3)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2025-02-01") == 2)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2025-03-02") == 0)
}

@Test
static func edgeCases() {
// Empty array
let emptyDates: [String] = []
#expect(emptyDates.countISODatesOnOrAfter(cutoffISODate: "2025-01-01") == 0)

// Single date, various cutoffs
let singleDate = ["2025-01-15"]
#expect(singleDate.countISODatesOnOrAfter(cutoffISODate: "2025-01-14") == 1)
#expect(singleDate.countISODatesOnOrAfter(cutoffISODate: "2025-01-15") == 1)
#expect(singleDate.countISODatesOnOrAfter(cutoffISODate: "2025-01-16") == 0)
}

@Test
static func duplicateDates() {
let datesWithDuplicates = [
"2025-01-01",
"2025-01-01", // Duplicate
"2025-02-01",
"2025-02-01", // Duplicate
"2025-03-01"
]

#expect(datesWithDuplicates.countISODatesOnOrAfter(cutoffISODate: "2025-01-01") == 5)
#expect(datesWithDuplicates.countISODatesOnOrAfter(cutoffISODate: "2025-02-01") == 3)
#expect(datesWithDuplicates.countISODatesOnOrAfter(cutoffISODate: "2025-03-01") == 1)
}

@Test
static func complexDateRanges() {
let dates = [
"2020-12-31", // End of 2020
"2021-01-01", // Start of 2021
"2021-12-31", // End of 2021
"2022-01-01", // Start of 2022
"2022-09-30", // End of September
"2022-10-01", // Start of October
"2023-01-09", // Single digit day
"2023-01-10", // Double digit day
"2023-09-09", // Both single digit
"2023-09-10", // Mixed digits
"2023-10-09", // Mixed digits different order
"2023-10-10", // Both double digits
"2024-02-28", // End of February
"2024-02-29", // Leap year day
"2024-03-01", // Start of March
"2025-01-01" // Far future
]

// Test year boundaries
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2020-12-31") == 16)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2021-01-01") == 15)

// Test month transitions
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2022-09-30") == 12)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2022-10-01") == 11)

// Test single/double digit transitions
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2023-01-09") == 10)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2023-01-10") == 9)

// Test leap year period
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2024-02-28") == 4)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2024-02-29") == 3)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2024-03-01") == 2)

// Test future date
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2025-01-01") == 1)
#expect(dates.countISODatesOnOrAfter(cutoffISODate: "2025-01-02") == 0)
}
}
}

0 comments on commit 9c6644d

Please sign in to comment.