Skip to content

Commit

Permalink
Implementation of a persistent event queue (#408)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
brototyp authored Sep 6, 2024
1 parent 7b372a1 commit 98fb04c
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Example/ios/iOS Example/MatomoTracker+SharedInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 0 additions & 63 deletions Example/ios/iOS Example/UserDefaultsQueue.swift

This file was deleted.

4 changes: 0 additions & 4 deletions Example/ios/ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -86,7 +85,6 @@
1F72DA721E62E78E00EFF764 /* ConfigurationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationViewController.swift; sourceTree = "<group>"; };
1F9C69F21F89254200728626 /* CustomTrackingParametersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTrackingParametersViewController.swift; sourceTree = "<group>"; };
1FA444972047A16800F7E37E /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
1FC2A0E42195A10A0039EE0C /* UserDefaultsQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsQueue.swift; sourceTree = "<group>"; };
1FC89ADC1FE2A2A5002EEB27 /* MatomoTracker+SharedInstance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MatomoTracker+SharedInstance.swift"; sourceTree = "<group>"; };
1FDC91711F1A648C0046F506 /* CustomDimensionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomDimensionsViewController.swift; sourceTree = "<group>"; };
1FDC91731F1A648C0046F506 /* ObjectiveCCompatibilityChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjectiveCCompatibilityChecker.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -229,7 +227,6 @@
1FDC91741F1A648C0046F506 /* ObjectiveCCompatibilityChecker.m */,
1F72DA6D1E61ECAF00EFF764 /* ios-Bridging-Header.h */,
1FC89ADC1FE2A2A5002EEB27 /* MatomoTracker+SharedInstance.swift */,
1FC2A0E42195A10A0039EE0C /* UserDefaultsQueue.swift */,
);
path = "iOS Example";
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
8 changes: 8 additions & 0 deletions MatomoTracker.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -63,6 +65,8 @@
1F1949F31E17B06600458199 /* MemoryQueueSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryQueueSpec.swift; sourceTree = "<group>"; };
1F38EBF71EE568D10021FBF8 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
1F3CA58B1E09A30600121FDC /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = "<group>"; };
1F500A5F2725A4F7004B128B /* UserDefaultsQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsQueue.swift; sourceTree = "<group>"; };
1F500A6127272301004B128B /* UserDefaultsQueueSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsQueueSpec.swift; sourceTree = "<group>"; };
1F5927542178716E001478DC /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
1F6B2F012529CC3400D2A591 /* UserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgent.swift; sourceTree = "<group>"; };
1F6F0CD61E61E35A008170FC /* MatomoTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MatomoTracker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -188,6 +192,7 @@
1F0A15CC1E6335CA00FEAE72 /* Event */,
1F092C191E26B44500394B30 /* Dispatcher.swift */,
1F1949F11E17A91100458199 /* MemoryQueue.swift */,
1F500A5F2725A4F7004B128B /* UserDefaultsQueue.swift */,
1F092C131E224C3E00394B30 /* MatomoUserDefaults.swift */,
1F3CA58B1E09A30600121FDC /* Queue.swift */,
1F6F0CD61E61E35A008170FC /* MatomoTracker.swift */,
Expand All @@ -210,6 +215,7 @@
1F63E1D5216B503D000C4C00 /* Stubs */,
1F1949F51E17B2A400458199 /* Fixtures */,
1F1949F31E17B06600458199 /* MemoryQueueSpec.swift */,
1F500A6127272301004B128B /* UserDefaultsQueueSpec.swift */,
1F6F0CDB1E61E377008170FC /* TrackerSpec.swift */,
1F963073201B37A3007B2AE7 /* PiwikUserDefaultsSpec.swift */,
1F8DCCDB25A1C7C700C98793 /* EventAPISerializerSpec.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
);
Expand Down
70 changes: 70 additions & 0 deletions MatomoTracker/UserDefaultsQueue.swift
Original file line number Diff line number Diff line change
@@ -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..<amount])
completion(dequeuedItems)
}

public func remove(events: [Event], completion: @escaping () -> 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"
}
}
Loading

0 comments on commit 98fb04c

Please sign in to comment.