Skip to content

Commit 23161eb

Browse files
authored
Merge pull request #1 from amayers/amayers/xcode_15
Update for Xcode 15
2 parents 77dbe84 + 8c5157a commit 23161eb

14 files changed

+344
-352
lines changed

Package.resolved

Lines changed: 0 additions & 16 deletions
This file was deleted.

Package.swift

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
77
name: "AnalyticsKit",
8-
platforms: [.iOS(.v13)],
8+
platforms: [.iOS(.v16)],
99
products: [
1010
// Products define the executables and libraries a package produces, and make them visible to other packages.
1111
.library(
1212
name: "AnalyticsKit",
1313
targets: ["AnalyticsKit"]),
1414
],
15-
dependencies: [
16-
// Dependencies declare other packages that this package depends on.
17-
// .package(url: /* package url */, from: "1.0.0"),
18-
.package(url: "https://github.com/amayers/ThreadKit.git", from: "1.0.2")
19-
],
2015
targets: [
2116
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2217
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2318
.target(
2419
name: "AnalyticsKit",
25-
dependencies: ["ThreadKit"]),
20+
dependencies: []),
2621
.testTarget(
2722
name: "AnalyticsKitTests",
2823
dependencies: ["AnalyticsKit"]),

Sources/AnalyticsKit/Events/AppLaunchEvent.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public struct AppLaunchEvent: AnalyticsEvent {
1313
"device_description": self.device.device.model.rawValue,
1414
"ram_size_mb": self.device.ramSizeMB,
1515
"free_disk_space_mb": self.device.freeDiskSpaceMB,
16-
"language_code": Locale.current.languageCode ?? "unknown",
17-
"region_code": Locale.current.regionCode ?? "unknown",
16+
"language_code": Locale.current.language.languageCode?.identifier ?? "unknown",
17+
"region_code": Locale.current.region?.identifier ?? "unknown",
1818
"locale_identifier": Locale.current.identifier
1919
]
2020
}

Sources/AnalyticsKit/Events/SendingDelayedAnalyticsEvent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Wrapper around an `AnalyticsEvent` that records the time that the event was originally fired & the event.
4-
struct SendingDelayedAnalyticsEvent {
4+
public struct SendingDelayedAnalyticsEvent {
55
let event: AnalyticsEvent
66
/// The original time the event happened.
77
let timeEventOccurred: Date
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
extension Array where Element == SendingDelayedAnalyticsEvent {
4+
func oldestFirst() -> Self {
5+
sorted { lhs, rhs in
6+
lhs.timeEventOccurred < rhs.timeEventOccurred
7+
}
8+
}
9+
func shouldSendQueuedEvents() -> Bool {
10+
return if count >= 10 {
11+
true
12+
} else {
13+
(oldestFirst().first?.timeEventOccurred.timeIntervalSinceNow ?? 0) <= -300
14+
}
15+
}
16+
17+
mutating func popEvents(batchSize: Int) -> ArraySlice<Element> {
18+
let range = 0..<Swift.min(count, batchSize)
19+
guard !range.isEmpty else {
20+
return []
21+
}
22+
let events = self[range]
23+
for index in range.sorted(by: >) {
24+
remove(at: index)
25+
}
26+
return events
27+
}
28+
}
Lines changed: 110 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,128 @@
1-
import Foundation
1+
import UIKit
22

33
public final class AnalyticsManager {
44
private enum Constants {
55
static let hasUserApprovedAnalyticsKey = "AnalyticsManager_hasUserApprovedAnalytics"
66
}
7-
8-
public static var shared: AnalyticsManager = AnalyticsManager()
9-
10-
private var services: [AnalyticsService] = []
11-
private let queue = DispatchQueue(label: "com.AnalyticsManager", qos: .utility, autoreleaseFrequency: .workItem, target: .global(qos: .utility))
12-
13-
/// The global switch that toggles analytics tracking. Defaults to `true`
7+
8+
private let service: Service
9+
private let userDefaults: UserDefaults
10+
11+
private let queue: EventQueue
12+
1413
public var hasUserApprovedAnalytics: Bool {
1514
didSet {
16-
dispatchPrecondition(condition: .onQueue(.main))
17-
UserDefaults.standard.set(hasUserApprovedAnalytics, forKey: Constants.hasUserApprovedAnalyticsKey)
18-
if hasUserApprovedAnalytics {
19-
queue.async {
20-
self.services.forEach({ (service) in
21-
service.set(enabled: self.hasUserApprovedAnalytics)
22-
service.configure(userID: UserIdentifier.identifierForVendor())
23-
})
24-
}
25-
}
15+
userDefaults.set(
16+
hasUserApprovedAnalytics,
17+
forKey: Constants.hasUserApprovedAnalyticsKey
18+
)
2619
}
2720
}
2821

29-
private init() {
22+
public init(
23+
service: Service,
24+
queue: EventQueue = .init(),
25+
userDefaults: UserDefaults = .standard
26+
) {
27+
self.service = service
28+
self.queue = queue
29+
self.userDefaults = userDefaults
3030
// This sets the default of `hasUserApprovedAnalytics` to `true`, but once a user toggles the setting manually, this value is overwritten.
31-
UserDefaults.standard.register(defaults: [Constants.hasUserApprovedAnalyticsKey: true])
32-
self.hasUserApprovedAnalytics = UserDefaults.standard.bool(forKey: Constants.hasUserApprovedAnalyticsKey)
33-
}
34-
35-
/// Adds a new `AnalyticsService` that will be used when logging events. You can add as many services as you like and they will all get used.
36-
public func add(service: AnalyticsService) {
37-
queue.async {
38-
service.set(enabled: self.hasUserApprovedAnalytics)
39-
if self.hasUserApprovedAnalytics {
40-
service.configure(userID: UserIdentifier.identifierForVendor())
41-
}
42-
self.services.append(service)
31+
userDefaults.register(defaults: [Constants.hasUserApprovedAnalyticsKey: true])
32+
hasUserApprovedAnalytics = userDefaults.bool(forKey: Constants.hasUserApprovedAnalyticsKey)
33+
34+
Task { @MainActor in
35+
// The following app events should all trigger sending any queued events, since after these events our app might stop running,
36+
// and we'd loose the events.
37+
NotificationCenter.default.addObserver(
38+
self,
39+
selector: #selector(forceSendingAllEvents),
40+
name: UIApplication.didReceiveMemoryWarningNotification,
41+
object: nil
42+
)
43+
NotificationCenter.default.addObserver(
44+
self,
45+
selector: #selector(forceSendingAllEvents),
46+
name: UIApplication.didEnterBackgroundNotification,
47+
object: nil
48+
)
49+
NotificationCenter.default.addObserver(
50+
self,
51+
selector: #selector(forceSendingAllEvents),
52+
name: UIApplication.willResignActiveNotification,
53+
object: nil
54+
)
55+
NotificationCenter.default.addObserver(
56+
self,
57+
selector: #selector(forceSendingAllEvents),
58+
name: UIApplication.willTerminateNotification,
59+
object: nil
60+
)
4361
}
4462
}
45-
46-
/// Logs an event on all enabled services
63+
64+
/// Logs an event on all enabled services. Does not wait for the event to be sent.
4765
public func logCustomEvent(_ event: AnalyticsEvent) {
48-
guard hasUserApprovedAnalytics else { return }
49-
queue.async {
50-
self.services.forEach { (service) in
51-
service.logCustomEvent(event)
66+
Task {
67+
await logCustomEvent(event)
68+
}
69+
}
70+
71+
/// Logs an event on all enabled services. Waits for the event to be sent (if the event queue is ready to be sent).
72+
public func logCustomEvent(_ event: AnalyticsEvent) async {
73+
await queue.add(
74+
event: SendingDelayedAnalyticsEvent(
75+
event: event,
76+
timeEventOccurred: Date()
77+
)
78+
)
79+
await sendQueuedEventsWhenNecessary()
80+
}
81+
82+
// MARK: - Private Methods
83+
84+
@objc nonisolated private func forceSendingAllEvents() {
85+
Task {
86+
while await !queue.events.isEmpty {
87+
await sendBatchOfEvents()
5288
}
53-
event.wasSent()
5489
}
5590
}
91+
92+
private func sendQueuedEventsWhenNecessary() async{
93+
while await queue.events.shouldSendQueuedEvents() {
94+
await sendBatchOfEvents()
95+
}
96+
}
97+
98+
private func sendBatchOfEvents() async {
99+
let events = await queue.pop(batchSize: service.batchSize)
100+
do {
101+
try await service.send(events: events, for: UserIdentifier.identifierForVendor())
102+
events.forEach { $0.event.wasSent() }
103+
} catch {
104+
print("Error sending analytics batch: \(error)")
105+
await queue.reenqueue(events: events)
106+
}
107+
}
108+
}
109+
110+
// MARK: - EventQueue
111+
112+
public actor EventQueue {
113+
private(set) var events: [SendingDelayedAnalyticsEvent] = []
114+
115+
public init() { }
116+
117+
func add(event: SendingDelayedAnalyticsEvent) {
118+
events.append(event)
119+
}
120+
121+
func pop(batchSize: Int) -> ArraySlice<SendingDelayedAnalyticsEvent> {
122+
events.popEvents(batchSize: batchSize)
123+
}
124+
125+
func reenqueue(events: any Collection<SendingDelayedAnalyticsEvent>) {
126+
self.events.append(contentsOf: events)
127+
}
56128
}

Sources/AnalyticsKit/Services/AnalyticsService.swift

Lines changed: 0 additions & 13 deletions
This file was deleted.

Sources/AnalyticsKit/Services/BatchSendingAnalyticsService.swift

Lines changed: 0 additions & 105 deletions
This file was deleted.

0 commit comments

Comments
 (0)