From 9d9d9db30aee463426a7ec4460a550135359cc13 Mon Sep 17 00:00:00 2001 From: Cornelius Horstmann Date: Sun, 1 Nov 2020 12:20:10 +0100 Subject: [PATCH 1/2] Changed the Dispatcher to use a swift Result --- MatomoTracker/Dispatcher.swift | 6 +++++- MatomoTracker/MatomoTracker.swift | 21 +++++++++++---------- MatomoTracker/URLSessionDispatcher.swift | 12 ++++++------ MatomoTrackerTests/DispatcherStub.swift | 12 +++--------- MatomoTrackerTests/TrackerSpec.swift | 10 +++++----- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/MatomoTracker/Dispatcher.swift b/MatomoTracker/Dispatcher.swift index 9414ca96..2be1e13f 100644 --- a/MatomoTracker/Dispatcher.swift +++ b/MatomoTracker/Dispatcher.swift @@ -6,5 +6,9 @@ public protocol Dispatcher { var userAgent: String? { get } - func send(events: [Event], success: @escaping ()->(), failure: @escaping (_ error: Error)->()) + func send(events: [Event], completion: @escaping (Result) -> Void) +} + +extension Dispatcher { + //func send(events: [Event], success: @escaping ()->(), failure: @escaping (_ error: Error)->()) } diff --git a/MatomoTracker/MatomoTracker.swift b/MatomoTracker/MatomoTracker.swift index b60c4297..f5fce343 100644 --- a/MatomoTracker/MatomoTracker.swift +++ b/MatomoTracker/MatomoTracker.swift @@ -176,22 +176,23 @@ final public class MatomoTracker: NSObject { self.logger.info("Finished dispatching events") return } - self.dispatcher.send(events: events, success: { [weak self] in + self.dispatcher.send(events: events) { [weak self] result in guard let self = self else { return } - DispatchQueue.main.async { - self.queue.remove(events: events, completion: { + switch result { + case .success:DispatchQueue.main.async { + self.queue.remove(events: events) { self.logger.info("Dispatched batch of \(events.count) events.") DispatchQueue.main.async { self.dispatchBatch() } - }) + } + } + case .failure(let error): + self.isDispatching = false + self.startDispatchTimer() + self.logger.warning("Failed dispatching events with error \(error)") } - }, failure: { [weak self] error in - guard let self = self else { return } - self.isDispatching = false - self.startDispatchTimer() - self.logger.warning("Failed dispatching events with error \(error)") - }) + } } } diff --git a/MatomoTracker/URLSessionDispatcher.swift b/MatomoTracker/URLSessionDispatcher.swift index c7e546df..f896f669 100644 --- a/MatomoTracker/URLSessionDispatcher.swift +++ b/MatomoTracker/URLSessionDispatcher.swift @@ -22,16 +22,16 @@ public final class URLSessionDispatcher: Dispatcher { self.userAgent = userAgent ?? UserAgent(application: Application.makeCurrentApplication(), device: Device.makeCurrentDevice()).stringValue } - public func send(events: [Event], success: @escaping ()->(), failure: @escaping (_ error: Error)->()) { + public func send(events: [Event], completion: @escaping (Result) -> Void) { let jsonBody: Data do { jsonBody = try serializer.jsonData(for: events) } catch { - failure(error) + completion(.failure(error)) return } let request = buildRequest(baseURL: baseURL, method: "POST", contentType: "application/json; charset=utf-8", body: jsonBody) - send(request: request, success: success, failure: failure) + send(request: request, completion: completion) } private func buildRequest(baseURL: URL, method: String, contentType: String? = nil, body: Data? = nil) -> URLRequest { @@ -43,14 +43,14 @@ public final class URLSessionDispatcher: Dispatcher { return request } - private func send(request: URLRequest, success: @escaping ()->(), failure: @escaping (_ error: Error)->()) { + private func send(request: URLRequest, completion: @escaping (Result) -> Void) { let task = session.dataTask(with: request) { data, response, error in // should we check the response? // let dataString = String(data: data!, encoding: String.Encoding.utf8) if let error = error { - failure(error) + completion(.failure(error)) } else { - success() + completion(.success(())) } } task.resume() diff --git a/MatomoTrackerTests/DispatcherStub.swift b/MatomoTrackerTests/DispatcherStub.swift index 029ec361..466a8d21 100644 --- a/MatomoTrackerTests/DispatcherStub.swift +++ b/MatomoTrackerTests/DispatcherStub.swift @@ -5,22 +5,16 @@ final class DispatcherStub: Dispatcher { public var baseURL: URL = URL(string: "http://matomo.org/spec_url")! struct Callback { - typealias SendEvents = (_ events: [Event], _ success: () -> (), _ failure: (Error) -> ()) -> () + typealias SendEvents = (_ events: [Event], _ completion: @escaping (Result) -> Void) -> () } var sendEvents: Callback.SendEvents? = nil let userAgent: String? = "DispatcherStub" - func send(event: Event, success: @escaping () -> (), failure: @escaping (Error) -> ()) { + func send(events: [Event], completion: @escaping (Result) -> Void) { DispatchQueue.global(qos: .background).async { - self.send(events: [event], success: success, failure: failure) - } - } - - func send(events: [Event], success: @escaping () -> (), failure: @escaping (Error) -> ()) { - DispatchQueue.global(qos: .background).async { - self.sendEvents?(events, success, failure) + self.sendEvents?(events, completion) } } } diff --git a/MatomoTrackerTests/TrackerSpec.swift b/MatomoTrackerTests/TrackerSpec.swift index 36f770ff..d666580e 100644 --- a/MatomoTrackerTests/TrackerSpec.swift +++ b/MatomoTrackerTests/TrackerSpec.swift @@ -48,7 +48,7 @@ class TrackerSpec: QuickSpec { } it("should give dequeued events to the dispatcher") { var eventsDispatched: [Event] = [] - let trackerFixture = TrackerFixture.nonEmptyQueueWithSendEventsCallback() { events, _,_ in + let trackerFixture = TrackerFixture.nonEmptyQueueWithSendEventsCallback() { events, _ in eventsDispatched = events } trackerFixture.tracker.dispatch() @@ -80,9 +80,9 @@ class TrackerSpec: QuickSpec { } it("should start a new DispatchTimer if dispatching failed") { var numberOfDispatches = 0 - let trackerFixture = TrackerFixture.withSendEventsCallback() { events, success, failure in + let trackerFixture = TrackerFixture.withSendEventsCallback() { events, completion in numberOfDispatches += 1 - failure(NSError(domain: "spec", code: 0)) + completion(.failure(NSError(domain: "spec", code: 0))) } trackerFixture.tracker.queue(event: EventFixture.event()) trackerFixture.tracker.dispatchInterval = 0.5 @@ -90,9 +90,9 @@ class TrackerSpec: QuickSpec { } it("should start a new DispatchTimer if dispatching succeeded") { var numberOfDispatches = 0 - let trackerFixture = TrackerFixture.withSendEventsCallback() { events, success, failure in + let trackerFixture = TrackerFixture.withSendEventsCallback() { events, completion in numberOfDispatches += 1 - success() + completion(.success(())) } trackerFixture.tracker.queue(event: EventFixture.event()) let autoTracker = AutoTracker(tracker: trackerFixture.tracker, trackingInterval: 0.01) From f8727d3222450455472575decdc095eb839f076c Mon Sep 17 00:00:00 2001 From: Cornelius Horstmann Date: Sun, 1 Nov 2020 13:36:00 +0100 Subject: [PATCH 2/2] Implemented BackgroundDispatcher which is wrapping event sending into a background task --- Example/ios/iOS Example/AppDelegate.swift | 2 +- .../MatomoTracker+SharedInstance.swift | 4 +- MatomoTracker.xcodeproj/project.pbxproj | 28 ++++++++-- MatomoTracker/BackgroundDispatcher.swift | 52 +++++++++++++++++++ MatomoTracker/Dispatcher.swift | 13 +++-- MatomoTracker/MatomoTracker.swift | 2 +- MatomoTracker/URLSessionDispatcher.swift | 2 +- 7 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 MatomoTracker/BackgroundDispatcher.swift diff --git a/Example/ios/iOS Example/AppDelegate.swift b/Example/ios/iOS Example/AppDelegate.swift index 77904479..ef8508ee 100644 --- a/Example/ios/iOS Example/AppDelegate.swift +++ b/Example/ios/iOS Example/AppDelegate.swift @@ -13,12 +13,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + MatomoTracker.shared.dispatch() } func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - MatomoTracker.shared.dispatch() } func applicationWillEnterForeground(_ application: UIApplication) { diff --git a/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift b/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift index 699e008b..e7445e4a 100644 --- a/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift +++ b/Example/ios/iOS Example/MatomoTracker+SharedInstance.swift @@ -1,11 +1,13 @@ import Foundation import MatomoTracker +import UIKit extension MatomoTracker { static let shared: MatomoTracker = { let queue = UserDefaultsQueue(UserDefaults.standard, autoSave: true) let dispatcher = URLSessionDispatcher(baseURL: URL(string: "https://demo2.matomo.org/piwik.php")!) - let matomoTracker = MatomoTracker(siteId: "23", queue: queue, dispatcher: dispatcher) + let automaticBackgroundDispatcher = BackgroundDispatcher(underlyingDispatcher: dispatcher, application: UIApplication.shared) + let matomoTracker = MatomoTracker(siteId: "23", queue: queue, dispatcher: automaticBackgroundDispatcher) matomoTracker.logger = DefaultLogger(minLevel: .verbose) matomoTracker.migrateFromFourPointFourSharedInstance() return matomoTracker diff --git a/MatomoTracker.xcodeproj/project.pbxproj b/MatomoTracker.xcodeproj/project.pbxproj index 3ecb9467..51a58aa0 100644 --- a/MatomoTracker.xcodeproj/project.pbxproj +++ b/MatomoTracker.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 1F80856F1E6B4B9800A61AAF /* Locale+HttpAcceptLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F80856E1E6B4B9800A61AAF /* Locale+HttpAcceptLanguage.swift */; }; 1F963074201B37A3007B2AE7 /* PiwikUserDefaultsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F963073201B37A3007B2AE7 /* PiwikUserDefaultsSpec.swift */; }; 1F963075201B37DC007B2AE7 /* MemoryQueueFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1949F71E17B2C800458199 /* MemoryQueueFixtures.swift */; }; + 1FA77F1B254EDA9A0046EA7C /* BackgroundDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA77F1A254EDA9A0046EA7C /* BackgroundDispatcher.swift */; }; 1FC2B429201F8C010061F5AD /* CustomVariable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC2B428201F8C010061F5AD /* CustomVariable.swift */; }; 1FCA6D451DBE0B2F0033F01C /* MatomoTracker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FCA6D3B1DBE0B2F0033F01C /* MatomoTracker.framework */; }; 1FCA6D4C1DBE0B2F0033F01C /* MatomoTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCA6D3E1DBE0B2F0033F01C /* MatomoTracker.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -77,6 +78,7 @@ 1F7C667E1F8C096F0066CC64 /* MainThread.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainThread.swift; sourceTree = ""; }; 1F80856E1E6B4B9800A61AAF /* Locale+HttpAcceptLanguage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Locale+HttpAcceptLanguage.swift"; sourceTree = ""; }; 1F963073201B37A3007B2AE7 /* PiwikUserDefaultsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiwikUserDefaultsSpec.swift; sourceTree = ""; }; + 1FA77F1A254EDA9A0046EA7C /* BackgroundDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundDispatcher.swift; sourceTree = ""; }; 1FC2B428201F8C010061F5AD /* CustomVariable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomVariable.swift; sourceTree = ""; }; 1FCA6D3B1DBE0B2F0033F01C /* MatomoTracker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MatomoTracker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1FCA6D3E1DBE0B2F0033F01C /* MatomoTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MatomoTracker.h; sourceTree = ""; }; @@ -158,6 +160,25 @@ name = Extensions; sourceTree = ""; }; + 1FA77F13254ED2C70046EA7C /* Dispatcher */ = { + isa = PBXGroup; + children = ( + 1F092C191E26B44500394B30 /* Dispatcher.swift */, + 1F6F0CE01E61E4F3008170FC /* URLSessionDispatcher.swift */, + 1FA77F1A254EDA9A0046EA7C /* BackgroundDispatcher.swift */, + ); + name = Dispatcher; + sourceTree = ""; + }; + 1FA77F14254ED2D80046EA7C /* Queue */ = { + isa = PBXGroup; + children = ( + 1F1949F11E17A91100458199 /* MemoryQueue.swift */, + 1F3CA58B1E09A30600121FDC /* Queue.swift */, + ); + name = Queue; + sourceTree = ""; + }; 1FCA6D311DBE0B2F0033F01C = { isa = PBXGroup; children = ( @@ -184,14 +205,12 @@ 1FCA6D3D1DBE0B2F0033F01C /* MatomoTracker */ = { isa = PBXGroup; children = ( + 1FA77F14254ED2D80046EA7C /* Queue */, + 1FA77F13254ED2C70046EA7C /* Dispatcher */, 1F80856D1E6B4B8000A61AAF /* Extensions */, 1F0A15CC1E6335CA00FEAE72 /* Event */, - 1F092C191E26B44500394B30 /* Dispatcher.swift */, - 1F1949F11E17A91100458199 /* MemoryQueue.swift */, 1F092C131E224C3E00394B30 /* MatomoUserDefaults.swift */, - 1F3CA58B1E09A30600121FDC /* Queue.swift */, 1F6F0CD61E61E35A008170FC /* MatomoTracker.swift */, - 1F6F0CE01E61E4F3008170FC /* URLSessionDispatcher.swift */, 1F38EBF71EE568D10021FBF8 /* Logger.swift */, 1FDC917D1F1A65150046F506 /* Application.swift */, 1FDC917E1F1A65150046F506 /* Device.swift */, @@ -413,6 +432,7 @@ 5ACB1FAD21426FFB007C766B /* OrderItem.swift in Sources */, 1F7C667F1F8C096F0066CC64 /* MainThread.swift in Sources */, 1F80856F1E6B4B9800A61AAF /* Locale+HttpAcceptLanguage.swift in Sources */, + 1FA77F1B254EDA9A0046EA7C /* BackgroundDispatcher.swift in Sources */, 1F0A15CE1E6335D800FEAE72 /* Visitor.swift in Sources */, 1FDC917F1F1A65150046F506 /* Application.swift in Sources */, 1F6F0CE11E61E4F3008170FC /* URLSessionDispatcher.swift in Sources */, diff --git a/MatomoTracker/BackgroundDispatcher.swift b/MatomoTracker/BackgroundDispatcher.swift new file mode 100644 index 00000000..9cb81fc1 --- /dev/null +++ b/MatomoTracker/BackgroundDispatcher.swift @@ -0,0 +1,52 @@ +// +// BackgroundDispatcher.swift +// MatomoTracker +// +// Created by Cornelius Horstmann on 01.11.20. +// Copyright © 2020 Matomo. All rights reserved. +// + +#if os(iOS) || os(tvOS) +import Foundation +import UIKit + +public class BackgroundDispatcher: Dispatcher { + + private let underlyingDispatcher: Dispatcher + private weak var application: UIApplication? + + public init(underlyingDispatcher: Dispatcher, application: UIApplication) { + self.underlyingDispatcher = underlyingDispatcher + self.application = application + } + + public var baseURL: URL { + get { + underlyingDispatcher.baseURL + } + } + + public func send(events: [Event], completion: @escaping (Result) -> Void) { + performBackgroundTask(withName: "Matomo") { [weak self] backgroundCompletion in + self?.underlyingDispatcher.send(events: events) { + backgroundCompletion() + completion($0) + } + } + } + + private func performBackgroundTask(withName name: String, closure: @escaping (_ completion: @escaping () -> Void) -> Void) { + guard let application = application else { + return closure() {} + } + let identifier = application.beginBackgroundTask(withName: name) { + // Todo: Better logging + print("expired") + } + closure { [weak application] in + application?.endBackgroundTask(identifier) + } + } +} + +#endif diff --git a/MatomoTracker/Dispatcher.swift b/MatomoTracker/Dispatcher.swift index 2be1e13f..1ab94090 100644 --- a/MatomoTracker/Dispatcher.swift +++ b/MatomoTracker/Dispatcher.swift @@ -4,11 +4,18 @@ public protocol Dispatcher { var baseURL: URL { get } - var userAgent: String? { get } - func send(events: [Event], completion: @escaping (Result) -> Void) } extension Dispatcher { - //func send(events: [Event], success: @escaping ()->(), failure: @escaping (_ error: Error)->()) + + @available(*, deprecated, message: "send(events:, completio:) instead") + func send(events: [Event], success: @escaping ()->(), failure: @escaping (_ error: Error)->()) { + send(events: events) { result in + switch result { + case .success(_): success() + case .failure(let error): failure(error) + } + } + } } diff --git a/MatomoTracker/MatomoTracker.swift b/MatomoTracker/MatomoTracker.swift index f5fce343..97745f6c 100644 --- a/MatomoTracker/MatomoTracker.swift +++ b/MatomoTracker/MatomoTracker.swift @@ -143,7 +143,7 @@ final public class MatomoTracker: NSObject { private(set) var isDispatching = false - /// Manually start the dispatching process. You might want to call this method in AppDelegates `applicationDidEnterBackground` to transmit all data + /// Manually start the dispatching process. You might want to call this method in AppDelegates `applicationWillResignActive` to transmit all data /// whenever the user leaves the application. @objc public func dispatch() { guard !isDispatching else { diff --git a/MatomoTracker/URLSessionDispatcher.swift b/MatomoTracker/URLSessionDispatcher.swift index f896f669..5fdc5fd7 100644 --- a/MatomoTracker/URLSessionDispatcher.swift +++ b/MatomoTracker/URLSessionDispatcher.swift @@ -7,7 +7,7 @@ public final class URLSessionDispatcher: Dispatcher { private let session: URLSession public let baseURL: URL - public private(set) var userAgent: String? + private var userAgent: String? /// Generate a URLSessionDispatcher instance ///