|
1 | | -import Foundation |
| 1 | +import UIKit |
2 | 2 |
|
3 | 3 | public final class AnalyticsManager { |
4 | 4 | private enum Constants { |
5 | 5 | static let hasUserApprovedAnalyticsKey = "AnalyticsManager_hasUserApprovedAnalytics" |
6 | 6 | } |
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 | + |
14 | 13 | public var hasUserApprovedAnalytics: Bool { |
15 | 14 | 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 | + ) |
26 | 19 | } |
27 | 20 | } |
28 | 21 |
|
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 |
30 | 30 | // 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 | + ) |
43 | 61 | } |
44 | 62 | } |
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. |
47 | 65 | 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() |
52 | 88 | } |
53 | | - event.wasSent() |
54 | 89 | } |
55 | 90 | } |
| 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 | + } |
56 | 128 | } |
0 commit comments