From 98fb04cca6636ed630f49633ae4bb06f5d5efd62 Mon Sep 17 00:00:00 2001 From: Cornelius Horstmann Date: Fri, 6 Sep 2024 11:33:24 +0200 Subject: [PATCH] Implementation of a persistent event queue (#408) * Implemented the UserDefaultsQueue * Updated Nimble # Conflicts: # Podfile * Added UserDefaultsQueueSpec * Added documentation and usage of the UserDefaultsQueue to the example app # Conflicts: # Example/ios/ios.xcodeproj/project.pbxproj * Added a changelog entry # Conflicts: # CHANGELOG.md # Conflicts: # CHANGELOG.md * Updated specs to latest Quick and Nimble versions --- CHANGELOG.md | 1 + .../MatomoTracker+SharedInstance.swift | 2 +- .../ios/iOS Example/UserDefaultsQueue.swift | 63 ------- Example/ios/ios.xcodeproj/project.pbxproj | 4 - MatomoTracker.xcodeproj/project.pbxproj | 8 + MatomoTracker/UserDefaultsQueue.swift | 70 ++++++++ .../UserDefaultsQueueSpec.swift | 167 ++++++++++++++++++ Podfile | 2 +- README.md | 8 +- 9 files changed, 255 insertions(+), 70 deletions(-) delete mode 100644 Example/ios/iOS Example/UserDefaultsQueue.swift create mode 100644 MatomoTracker/UserDefaultsQueue.swift create mode 100644 MatomoTrackerTests/UserDefaultsQueueSpec.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 052bf37f..8785ef12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased * **feature** Added functionality to reset a Matomo instance. [#434](https://github.com/matomo-org/matomo-sdk-ios/pull/434) +* **feature** Added a persistent event queue storing events in the UserDefaults. [#137](https://github.com/matomo-org/matomo-sdk-ios/issues/137) (by @bobunmeng and @brototyp) ## 7.6.0 * **feature** Added support for watchOS. [#352](https://github.com/matomo-org/matomo-sdk-ios/issues/352) diff --git a/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift b/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift index 699e008b..4248e258 100644 --- a/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift +++ b/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift @@ -3,7 +3,7 @@ import MatomoTracker extension MatomoTracker { static let shared: MatomoTracker = { - let queue = UserDefaultsQueue(UserDefaults.standard, autoSave: true) + let queue = UserDefaultsQueue(userDefaults: UserDefaults.standard) let dispatcher = URLSessionDispatcher(baseURL: URL(string: "https://demo2.matomo.org/piwik.php")!) let matomoTracker = MatomoTracker(siteId: "23", queue: queue, dispatcher: dispatcher) matomoTracker.logger = DefaultLogger(minLevel: .verbose) diff --git a/Example/ios/iOS Example/UserDefaultsQueue.swift b/Example/ios/iOS Example/UserDefaultsQueue.swift deleted file mode 100644 index c8c15f95..00000000 --- a/Example/ios/iOS Example/UserDefaultsQueue.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import MatomoTracker - -public final class UserDefaultsQueue: NSObject, Queue { - private var items: [Event] { - didSet { - if autoSave { - try? UserDefaultsQueue.write(items, to: userDefaults) - } - } - } - private let userDefaults: UserDefaults - private let autoSave: Bool - - init(_ userDefaults: UserDefaults, autoSave: Bool = false) { - self.userDefaults = userDefaults - self.autoSave = autoSave - self.items = (try? UserDefaultsQueue.readEvents(from: userDefaults)) ?? [] - super.init() - } - - public var eventCount: Int { - return items.count - } - - public func enqueue(events: [Event], completion: (()->())?) { - items.append(contentsOf: events) - completion?() - } - - public func first(limit: Int, completion: (_ items: [Event])->()) { - let amount = [limit,eventCount].min()! - let dequeuedItems = Array(items[0..()) { - items = items.filter({ event in !events.contains(where: { eventToRemove in eventToRemove.uuid == event.uuid })}) - completion() - } - - public func save() throws { - try UserDefaultsQueue.write(items, to: userDefaults) - } -} - -extension UserDefaultsQueue { - - private static let userDefaultsKey = "UserDefaultsQueue.items" - - private static func readEvents(from userDefaults: UserDefaults) throws -> [Event] { - guard let data = userDefaults.data(forKey: userDefaultsKey) else { return [] } - let decoder = JSONDecoder() - return try decoder.decode([Event].self, from: data) - } - - private static func write(_ events: [Event], to userDefaults: UserDefaults) throws { - let encoder = JSONEncoder() - let data = try encoder.encode(events) - userDefaults.set(data, forKey: userDefaultsKey) - } - -} diff --git a/Example/ios/ios.xcodeproj/project.pbxproj b/Example/ios/ios.xcodeproj/project.pbxproj index fde67e3f..dc1c457b 100644 --- a/Example/ios/ios.xcodeproj/project.pbxproj +++ b/Example/ios/ios.xcodeproj/project.pbxproj @@ -43,7 +43,6 @@ 1F72DA731E62E78E00EFF764 /* ConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F72DA721E62E78E00EFF764 /* ConfigurationViewController.swift */; }; 1F9C69F31F89254200728626 /* CustomTrackingParametersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F9C69F21F89254200728626 /* CustomTrackingParametersViewController.swift */; }; 1FA444982047A16800F7E37E /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA444972047A16800F7E37E /* SearchViewController.swift */; }; - 1FC2A0E52195A10A0039EE0C /* UserDefaultsQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC2A0E42195A10A0039EE0C /* UserDefaultsQueue.swift */; }; 1FC89ADD1FE2A2A5002EEB27 /* MatomoTracker+SharedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC89ADC1FE2A2A5002EEB27 /* MatomoTracker+SharedInstance.swift */; }; 1FDC91771F1A648C0046F506 /* CustomDimensionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FDC91711F1A648C0046F506 /* CustomDimensionsViewController.swift */; }; 1FDC91791F1A648C0046F506 /* ObjectiveCCompatibilityChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FDC91741F1A648C0046F506 /* ObjectiveCCompatibilityChecker.m */; }; @@ -86,7 +85,6 @@ 1F72DA721E62E78E00EFF764 /* ConfigurationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationViewController.swift; sourceTree = ""; }; 1F9C69F21F89254200728626 /* CustomTrackingParametersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTrackingParametersViewController.swift; sourceTree = ""; }; 1FA444972047A16800F7E37E /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; - 1FC2A0E42195A10A0039EE0C /* UserDefaultsQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsQueue.swift; sourceTree = ""; }; 1FC89ADC1FE2A2A5002EEB27 /* MatomoTracker+SharedInstance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MatomoTracker+SharedInstance.swift"; sourceTree = ""; }; 1FDC91711F1A648C0046F506 /* CustomDimensionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomDimensionsViewController.swift; sourceTree = ""; }; 1FDC91731F1A648C0046F506 /* ObjectiveCCompatibilityChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjectiveCCompatibilityChecker.h; sourceTree = ""; }; @@ -229,7 +227,6 @@ 1FDC91741F1A648C0046F506 /* ObjectiveCCompatibilityChecker.m */, 1F72DA6D1E61ECAF00EFF764 /* ios-Bridging-Header.h */, 1FC89ADC1FE2A2A5002EEB27 /* MatomoTracker+SharedInstance.swift */, - 1FC2A0E42195A10A0039EE0C /* UserDefaultsQueue.swift */, ); path = "iOS Example"; sourceTree = ""; @@ -406,7 +403,6 @@ 1F7001BF216B6F47007A9355 /* GoalsViewController.swift in Sources */, 24F6AE8F1F61FDE200C6C22C /* UserIDViewController.swift in Sources */, EB82ABD820DA125100083494 /* ContentViewController.swift in Sources */, - 1FC2A0E52195A10A0039EE0C /* UserDefaultsQueue.swift in Sources */, 1FFE0C272095C0E500DE23B1 /* CampaignViewController.swift in Sources */, 1F03BC9B1E86CF770002F0AD /* ScreenViewController.swift in Sources */, 1F01F8FE2C89A8E400327D62 /* EcommerceViewController.swift in Sources */, diff --git a/MatomoTracker.xcodeproj/project.pbxproj b/MatomoTracker.xcodeproj/project.pbxproj index f87990b0..329efb83 100644 --- a/MatomoTracker.xcodeproj/project.pbxproj +++ b/MatomoTracker.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 1F1949F41E17B06600458199 /* MemoryQueueSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1949F31E17B06600458199 /* MemoryQueueSpec.swift */; }; 1F38EBF81EE568D10021FBF8 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F38EBF71EE568D10021FBF8 /* Logger.swift */; }; 1F3CA58C1E09A30600121FDC /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F3CA58B1E09A30600121FDC /* Queue.swift */; }; + 1F500A602725A4F7004B128B /* UserDefaultsQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F500A5F2725A4F7004B128B /* UserDefaultsQueue.swift */; }; + 1F500A6227272301004B128B /* UserDefaultsQueueSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F500A6127272301004B128B /* UserDefaultsQueueSpec.swift */; }; 1F5927552178716E001478DC /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = 1F5927542178716E001478DC /* CHANGELOG.md */; }; 1F6B2F032529CE8A00D2A591 /* UserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6B2F012529CC3400D2A591 /* UserAgent.swift */; }; 1F6F0CD71E61E35A008170FC /* MatomoTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6F0CD61E61E35A008170FC /* MatomoTracker.swift */; }; @@ -63,6 +65,8 @@ 1F1949F31E17B06600458199 /* MemoryQueueSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryQueueSpec.swift; sourceTree = ""; }; 1F38EBF71EE568D10021FBF8 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 1F3CA58B1E09A30600121FDC /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = ""; }; + 1F500A5F2725A4F7004B128B /* UserDefaultsQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsQueue.swift; sourceTree = ""; }; + 1F500A6127272301004B128B /* UserDefaultsQueueSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsQueueSpec.swift; sourceTree = ""; }; 1F5927542178716E001478DC /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 1F6B2F012529CC3400D2A591 /* UserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgent.swift; sourceTree = ""; }; 1F6F0CD61E61E35A008170FC /* MatomoTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MatomoTracker.swift; sourceTree = ""; }; @@ -188,6 +192,7 @@ 1F0A15CC1E6335CA00FEAE72 /* Event */, 1F092C191E26B44500394B30 /* Dispatcher.swift */, 1F1949F11E17A91100458199 /* MemoryQueue.swift */, + 1F500A5F2725A4F7004B128B /* UserDefaultsQueue.swift */, 1F092C131E224C3E00394B30 /* MatomoUserDefaults.swift */, 1F3CA58B1E09A30600121FDC /* Queue.swift */, 1F6F0CD61E61E35A008170FC /* MatomoTracker.swift */, @@ -210,6 +215,7 @@ 1F63E1D5216B503D000C4C00 /* Stubs */, 1F1949F51E17B2A400458199 /* Fixtures */, 1F1949F31E17B06600458199 /* MemoryQueueSpec.swift */, + 1F500A6127272301004B128B /* UserDefaultsQueueSpec.swift */, 1F6F0CDB1E61E377008170FC /* TrackerSpec.swift */, 1F963073201B37A3007B2AE7 /* PiwikUserDefaultsSpec.swift */, 1F8DCCDB25A1C7C700C98793 /* EventAPISerializerSpec.swift */, @@ -398,6 +404,7 @@ files = ( 1F092C161E225E0200394B30 /* Event.swift in Sources */, 1F092C141E224C3E00394B30 /* MatomoUserDefaults.swift in Sources */, + 1F500A602725A4F7004B128B /* UserDefaultsQueue.swift in Sources */, 1F6F0CD71E61E35A008170FC /* MatomoTracker.swift in Sources */, 1F6B2F032529CE8A00D2A591 /* UserAgent.swift in Sources */, 1F1949F21E17A91100458199 /* MemoryQueue.swift in Sources */, @@ -430,6 +437,7 @@ 1F8DCCD825A1BA4900C98793 /* Session+Fixture.swift in Sources */, 1F8DCCD625A1BA2C00C98793 /* Visitor+Fixture.swift in Sources */, 1F8DCCD425A1B9E100C98793 /* Event+Fixture.swift in Sources */, + 1F500A6227272301004B128B /* UserDefaultsQueueSpec.swift in Sources */, 1F1949F41E17B06600458199 /* MemoryQueueSpec.swift in Sources */, 1F6F0CDF1E61E377008170FC /* TrackerSpec.swift in Sources */, ); diff --git a/MatomoTracker/UserDefaultsQueue.swift b/MatomoTracker/UserDefaultsQueue.swift new file mode 100644 index 00000000..be2b522a --- /dev/null +++ b/MatomoTracker/UserDefaultsQueue.swift @@ -0,0 +1,70 @@ +import Foundation + +/// The UserDefaultsQueue is a **not thread safe** queue that persists all events in the `UserDefaults`. +/// Per default only the last 100 Events will be queued. If there are more Events than that, the oldest ones will be dropped. +public final class UserDefaultsQueue: NSObject, Queue { + + private let userDefaults: UserDefaults + private let maximumQueueSize: Int? + + private let encoder: PropertyListEncoder = PropertyListEncoder() + private let decoder: PropertyListDecoder = PropertyListDecoder() + + private var enqueuedEvents: [Event] { + get { + guard let data = userDefaults.object(forKey: UserDefaultsQueue.Key.queuedEvents) as? [Data] + else { return [] } + return data.map { try? decoder.decode(Event.self, from: $0) }.compactMap { $0 } + } + set { + let eventsToStore: [Event] + if let maximumQueueSize = maximumQueueSize { + eventsToStore = newValue.suffix(max(0,maximumQueueSize)) + } else { + eventsToStore = newValue + } + let data = eventsToStore.map { try? encoder.encode($0) }.compactMap { $0 } + userDefaults.set(data, forKey: UserDefaultsQueue.Key.queuedEvents) + } + } + + public var eventCount: Int { + assertMainThread() + return enqueuedEvents.count + } + + /// Initializes a new UserDefaultsQueue + /// - Parameters: + /// - suiteName: The UserDefaults in which the events should be persisted. + /// - maximumQueueSize: The maximum number of events to be queued, 100 per default. If nil, the number of events is not limited. + public init(userDefaults: UserDefaults, maximumQueueSize: Int? = 100) { + self.userDefaults = userDefaults + self.maximumQueueSize = maximumQueueSize + } + + public func enqueue(events: [Event], completion: (() -> Void)?) { + assertMainThread() + enqueuedEvents.append(contentsOf: events) + completion?() + } + + public func first(limit: Int, completion: @escaping ([Event]) -> Void) { + assertMainThread() + + let amount = [limit,eventCount].min()! + let dequeuedItems = Array(enqueuedEvents[0.. Void) { + assertMainThread() + enqueuedEvents = enqueuedEvents.filter({ event in !events.contains(where: { eventToRemove in eventToRemove.uuid == event.uuid })}) + completion() + } +} + +extension UserDefaultsQueue { + internal struct Key { + static let queuedEvents = "MatomoQueuedEvents" + } +} diff --git a/MatomoTrackerTests/UserDefaultsQueueSpec.swift b/MatomoTrackerTests/UserDefaultsQueueSpec.swift new file mode 100644 index 00000000..acc142c4 --- /dev/null +++ b/MatomoTrackerTests/UserDefaultsQueueSpec.swift @@ -0,0 +1,167 @@ +@testable import MatomoTracker +import Quick +import Nimble + +class UserDefaultsQueueSpec: QuickSpec { + override class func setUp() { + Nimble.PollingDefaults.timeout = .seconds(10) + Nimble.PollingDefaults.pollInterval = .milliseconds(100) + } + + override class func spec() { + let userDefaults = UserDefaults(suiteName: "UserDefaultsQueueSpec")! + afterEach { + self.removeAllInSuite(suite: "UserDefaultsQueueSpec") + } + describe("init") { + it("should return not null") { + let queue = UserDefaultsQueue(userDefaults: userDefaults) + expect(queue).toNot(beNil()) + } + it("should limit to 100 events per default") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + for _ in 0..<111 { + queue.enqueue(event: .fixture()) + } + expect(queue.eventCount) == 100 + } + it("should limit to the given maximumQueueSize") { + var queue = UserDefaultsQueue(userDefaults: userDefaults, maximumQueueSize: 10) + for _ in 0..<13 { + queue.enqueue(event: .fixture()) + } + expect(queue.eventCount) == 10 + } + it("should not limit if maximumQueueSize is set to nil") { + var queue = UserDefaultsQueue(userDefaults: userDefaults, maximumQueueSize: nil) + for _ in 0..<111 { + queue.enqueue(event: .fixture()) + } + expect(queue.eventCount) == 111 + } + } + describe("eventCount") { + it("should return 0 with an empty queue") { + let queue = UserDefaultsQueue(userDefaults: userDefaults) + expect(queue.eventCount) == 0 + } + it("should return 2 with a queue with two items") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + queue.enqueue(event: .fixture()) + expect(queue.eventCount) == 2 + } + } + describe("enqueue") { + it("should increase item count") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + expect(queue.eventCount).toEventually(equal(1)) + } + it("should call the completion closure") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + var calledClosure = false + queue.enqueue(event: .fixture()) { + calledClosure = true + } + expect(calledClosure).toEventually(beTrue()) + } + it("should enqueue the object") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + let event = Event.fixture() + var dequeued: Event? = nil + queue.enqueue(event: event) + queue.first(limit: 1) { event in + dequeued = event.first + } + expect(dequeued).toEventuallyNot(beNil()) + expect(dequeued?.uuid).toEventually(equal(event.uuid)) + } + } + describe("first") { + context("with an empty queue") { + let queue = UserDefaultsQueue(userDefaults: userDefaults) + it("should call the completionblock without items") { + var dequeuedItems: [Any]? = nil + queue.first(limit: 10) { events in + dequeuedItems = events + } + expect(dequeuedItems).toEventuallyNot(beNil()) + expect(dequeuedItems?.count).toEventually(equal(0)) + } + } + context("with a queue with two items") { + it("should return one item if just asked for one") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + queue.enqueue(event: .fixture()) + var dequeuedItems: [Any]? = nil + queue.first(limit: 1) { events in + dequeuedItems = events + } + expect(dequeuedItems?.count).toEventually(equal(1)) + } + it("should return two items if asked for two or more") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + queue.enqueue(event: .fixture()) + var dequeuedItems: [Any]? = nil + queue.first(limit: 10) { events in + dequeuedItems = events + } + expect(dequeuedItems?.count).toEventually(equal(2)) + } + it("should not remove objects from the queue") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + queue.enqueue(event: .fixture()) + queue.first(limit: 1, completion: { items in }) + expect(queue.eventCount) == 2 + } + } + } + describe("remove") { + it("should remove events that are enqueued") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + queue.enqueue(event: .fixture()) + var dequeuedItem: Event? = nil + queue.first(limit: 1) { events in + dequeuedItem = events.first + } + queue.remove(events: [dequeuedItem!], completion: {}) + expect(queue.eventCount) == 1 + } + it("should ignore if one event is tried to remove twice") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + queue.enqueue(event: .fixture()) + var dequeuedItem: Event? = nil + queue.first(limit: 1) { events in + dequeuedItem = events.first + } + queue.remove(events: [dequeuedItem!], completion: {}) + queue.remove(events: [dequeuedItem!], completion: {}) + expect(queue.eventCount) == 1 + } + it("should skip not queued events") { + var queue = UserDefaultsQueue(userDefaults: userDefaults) + queue.enqueue(event: .fixture()) + queue.enqueue(event: .fixture()) + var dequeuedItem: Event? = nil + queue.first(limit: 1) { events in + dequeuedItem = events.first + } + queue.remove(events: [dequeuedItem!, .fixture()], completion: {}) + expect(queue.eventCount) == 1 + } + } + } + private class func removeAllInSuite(suite: String) { + guard let newuserDefaults = UserDefaults(suiteName: suite) else { return } + let allKeys: [String]? = Array(newuserDefaults.dictionaryRepresentation().keys) + for key in allKeys ?? [] { + newuserDefaults.removeObject(forKey: key) + } + } +} diff --git a/Podfile b/Podfile index e85095f0..566a9e81 100644 --- a/Podfile +++ b/Podfile @@ -49,4 +49,4 @@ post_install do |installer| config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" end end -end \ No newline at end of file +end diff --git a/README.md b/README.md index b6f84cea..4a3f7c0d 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,13 @@ You can define the url property on every `Event`. If none is defined, the SDK wi ### Event dispatching -Whenever you track an event or a page view it is stored in memory first. In every dispatch run a batch of those events are sent to the server. If the device is offline or the server doesn't respond these events will be kept and resent at a later time. Events currently aren't stored on disk and will be lost if the application is terminated. [#137](https://github.com/matomo-org/matomo-sdk-ios/issues/137) +Whenever you track an event or a page view it is stored in memory first. In every dispatch run a batch of those events are sent to the server. If the device is offline or the server doesn't respond these events will be kept and resent at a later time. Events currently aren't stored on disk and will be lost if the application is terminated. [#137](https://github.com/matomo-org/matomo-sdk-ios/issues/137). You can use an experimental implementation of a Queue that stores all Events in the UserDefaults. + +```swift +let queue = UserDefaultsQueue(userDefaults: UserDefaults.standard) +let dispatcher = URLSessionDispatcher(baseURL: URL(string: "https://example.com/matomo.php")!) +let matomoTracker = MatomoTracker(siteId: "1", queue: queue, dispatcher: dispatcher) +``` ## Contributing Please read [CONTRIBUTING.md](https://github.com/matomo-org/matomo-sdk-ios/blob/develop/CONTRIBUTING.md) for details.