diff --git a/Sources/Core/Tracker/Tracker.swift b/Sources/Core/Tracker/Tracker.swift index ee9dffa75..0e52fb775 100644 --- a/Sources/Core/Tracker/Tracker.swift +++ b/Sources/Core/Tracker/Tracker.swift @@ -40,9 +40,8 @@ func uncaughtExceptionHandler(_ exception: NSException) { class Tracker: NSObject { private var platformContextSchema: String = "" private var dataCollection = true - private var builderFinished = false - + private let serialQueue: DispatchQueueWrapperProtocol /// The object used for sessionization, i.e. it characterizes user activity. private(set) var session: Session? @@ -175,14 +174,14 @@ class Tracker: NSObject { return _deepLinkContext } set(deepLinkContext) { - objc_sync_enter(self) - _deepLinkContext = deepLinkContext - if deepLinkContext { - addOrReplace(stateMachine: DeepLinkStateMachine()) - } else { - _ = stateManager.removeStateMachine(DeepLinkStateMachine.identifier) + serialQueue.sync { + self._deepLinkContext = deepLinkContext + if deepLinkContext { + self.addOrReplace(stateMachine: DeepLinkStateMachine()) + } else { + _ = self.stateManager.removeStateMachine(DeepLinkStateMachine.identifier) + } } - objc_sync_exit(self) } } @@ -192,14 +191,14 @@ class Tracker: NSObject { return _screenContext } set(screenContext) { - objc_sync_enter(self) - _screenContext = screenContext - if screenContext { - addOrReplace(stateMachine: ScreenStateMachine()) - } else { - _ = stateManager.removeStateMachine(ScreenStateMachine.identifier) + serialQueue.sync { + self._screenContext = screenContext + if screenContext { + self.addOrReplace(stateMachine: ScreenStateMachine()) + } else { + _ = self.stateManager.removeStateMachine(ScreenStateMachine.identifier) + } } - objc_sync_exit(self) } } @@ -241,14 +240,14 @@ class Tracker: NSObject { return _lifecycleEvents } set(lifecycleEvents) { - objc_sync_enter(self) - _lifecycleEvents = lifecycleEvents - if lifecycleEvents { - addOrReplace(stateMachine: LifecycleStateMachine()) - } else { - _ = stateManager.removeStateMachine(LifecycleStateMachine.identifier) + serialQueue.sync { + self._lifecycleEvents = lifecycleEvents + if lifecycleEvents { + self.addOrReplace(stateMachine: LifecycleStateMachine()) + } else { + _ = self.stateManager.removeStateMachine(LifecycleStateMachine.identifier) + } } - objc_sync_exit(self) } } @@ -288,10 +287,12 @@ class Tracker: NSObject { init(trackerNamespace: String, appId: String?, emitter: Emitter, + dispatchQueue: DispatchQueueWrapperProtocol = DispatchQueueWrapper(label: "snowplow.tracker"), builder: ((Tracker) -> (Void))) { self._emitter = emitter self._appId = appId ?? "" self._trackerNamespace = trackerNamespace + self.serialQueue = dispatchQueue super.init() builder(self) @@ -443,26 +444,26 @@ class Tracker: NSObject { if !dataCollection { return nil } - event.beginProcessing(withTracker: self) - let eventId = processEvent(event) - event.endProcessing(withTracker: self) + let eventId = UUID() + serialQueue.async { + event.beginProcessing(withTracker: self) + self.processEvent(event, eventId) + event.endProcessing(withTracker: self) + } return eventId } // MARK: - Event Decoration - func processEvent(_ event: Event) -> UUID? { - objc_sync_enter(self) + func processEvent(_ event: Event, _ eventId: UUID) { let stateSnapshot = stateManager.trackerState(forProcessedEvent: event) - objc_sync_exit(self) - let trackerEvent = TrackerEvent(event: event, state: stateSnapshot) + let trackerEvent = TrackerEvent(event: event, eventId: eventId, state: stateSnapshot) if let payload = self.payload(with: trackerEvent) { emitter.addPayload(toBuffer: payload) stateManager.afterTrack(event: trackerEvent) - return trackerEvent.eventId + } else { + logDebug(message: "Event not tracked due to filtering") } - logDebug(message: "Event not tracked due to filtering") - return nil } func payload(with event: TrackerEvent) -> Payload? { diff --git a/Sources/Core/Tracker/TrackerEvent.swift b/Sources/Core/Tracker/TrackerEvent.swift index eee18f3f4..dccb4cbcb 100644 --- a/Sources/Core/Tracker/TrackerEvent.swift +++ b/Sources/Core/Tracker/TrackerEvent.swift @@ -39,8 +39,8 @@ class TrackerEvent : InspectableEvent, StateMachineEvent { private(set) var isService: Bool - init(event: Event, state: TrackerStateSnapshot? = nil) { - eventId = UUID() + init(event: Event, eventId: UUID = UUID(), state: TrackerStateSnapshot? = nil) { + self.eventId = eventId timestamp = Int64(Date().timeIntervalSince1970 * 1000) trueTimestamp = event.trueTimestamp entities = event.entities diff --git a/Sources/Core/Utils/DispatchQueueWrapper.swift b/Sources/Core/Utils/DispatchQueueWrapper.swift new file mode 100644 index 000000000..dff08b648 --- /dev/null +++ b/Sources/Core/Utils/DispatchQueueWrapper.swift @@ -0,0 +1,30 @@ +// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. + +import Foundation + +class DispatchQueueWrapper: DispatchQueueWrapperProtocol { + private let queue: DispatchQueue + + init(label: String) { + queue = DispatchQueue(label: label) + } + + func sync(_ callback: @escaping () -> Void) { + queue.sync(execute: callback) + } + + func async(_ callback: @escaping () -> Void) { + queue.async(execute: callback) + } +} diff --git a/Sources/Core/Utils/DispatchQueueWrapperProtocol.swift b/Sources/Core/Utils/DispatchQueueWrapperProtocol.swift new file mode 100644 index 000000000..db52ffcd0 --- /dev/null +++ b/Sources/Core/Utils/DispatchQueueWrapperProtocol.swift @@ -0,0 +1,19 @@ +// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. + +import Foundation + +protocol DispatchQueueWrapperProtocol: AnyObject { + func sync(_ callback: @escaping () -> Void) + func async(_ callback: @escaping () -> Void) +} diff --git a/Tests/Configurations/TestTrackerConfiguration.swift b/Tests/Configurations/TestTrackerConfiguration.swift index f43661be9..110fe6cd3 100644 --- a/Tests/Configurations/TestTrackerConfiguration.swift +++ b/Tests/Configurations/TestTrackerConfiguration.swift @@ -186,8 +186,10 @@ class TestTrackerConfiguration: XCTestCase { let tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig, sessionConfig]) _ = tracker?.track(Timing(category: "cat", variable: "var", timing: 123)) + Thread.sleep(forTimeInterval: 0.1) tracker?.session?.startNewSession() _ = tracker?.track(Timing(category: "cat", variable: "var", timing: 123)) + Thread.sleep(forTimeInterval: 0.1) wait(for: [expectation], timeout: 10) } diff --git a/Tests/Configurations/TestTrackerController.swift b/Tests/Configurations/TestTrackerController.swift index 185789481..201df62ff 100644 --- a/Tests/Configurations/TestTrackerController.swift +++ b/Tests/Configurations/TestTrackerController.swift @@ -50,16 +50,19 @@ class TestTrackerController: XCTestCase { tracker?.emitter?.pause() _ = tracker?.track(Structured(category: "c", action: "a")) + Thread.sleep(forTimeInterval: 0.1) let sessionIdBefore = tracker?.session?.sessionId tracker?.userAnonymisation = true _ = tracker?.track(Structured(category: "c", action: "a")) + Thread.sleep(forTimeInterval: 0.1) let sessionIdAnonymous = tracker?.session?.sessionId XCTAssertFalse((sessionIdBefore == sessionIdAnonymous)) tracker?.userAnonymisation = false _ = tracker?.track(Structured(category: "c", action: "a")) + Thread.sleep(forTimeInterval: 0.1) let sessionIdNotAnonymous = tracker?.session?.sessionId XCTAssertFalse((sessionIdAnonymous == sessionIdNotAnonymous)) diff --git a/Tests/TestSession.swift b/Tests/TestSession.swift index 7a89cfcd7..0226350ef 100644 --- a/Tests/TestSession.swift +++ b/Tests/TestSession.swift @@ -128,7 +128,7 @@ class TestSession: XCTestCase { cleanFile(withNamespace: "t1") let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker = Tracker(trackerNamespace: "t1", appId: nil, emitter: emitter) { tracker in + let tracker = Tracker(trackerNamespace: "t1", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in tracker.installEvent = false tracker.lifecycleEvents = true tracker.sessionContext = true @@ -269,7 +269,7 @@ class TestSession: XCTestCase { cleanFile(withNamespace: "tracker") let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter) { tracker in + let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in tracker.lifecycleEvents = true tracker.sessionContext = true tracker.foregroundTimeout = 100 @@ -300,7 +300,7 @@ class TestSession: XCTestCase { cleanFile(withNamespace: "tracker") let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter) { tracker in + let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in tracker.lifecycleEvents = true tracker.sessionContext = true tracker.foregroundTimeout = 100 @@ -345,12 +345,13 @@ class TestSession: XCTestCase { cleanFile(withNamespace: "tracker2") let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker1 = Tracker(trackerNamespace: "tracker1", appId: nil, emitter: emitter) { tracker in + let queue2 = MockDispatchQueueWrapper(label: "test2") + let tracker1 = Tracker(trackerNamespace: "tracker1", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test1")) { tracker in tracker.sessionContext = true tracker.foregroundTimeout = 10 tracker.backgroundTimeout = 10 } - let tracker2 = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter) { tracker in + let tracker2 = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter, dispatchQueue: queue2) { tracker in tracker.sessionContext = true tracker.foregroundTimeout = 10 tracker.backgroundTimeout = 10 @@ -378,7 +379,7 @@ class TestSession: XCTestCase { XCTAssertEqual(1, tracker2.session!.state!.sessionIndex - initialValue2) // timed out //Recreate tracker2 - let tracker2b = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter) { tracker in + let tracker2b = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter, dispatchQueue: queue2) { tracker in tracker.sessionContext = true tracker.foregroundTimeout = 5 tracker.backgroundTimeout = 5 @@ -398,7 +399,7 @@ class TestSession: XCTestCase { storeAsV3_0(withNamespace: "tracker", eventId: "eventId", sessionId: "sessionId", sessionIndex: 123, userId: "userId") let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter) { tracker in + let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in tracker.sessionContext = true } let event = Structured(category: "c", action: "a") diff --git a/Tests/Utils/MockDispatchQueueWrapper.swift b/Tests/Utils/MockDispatchQueueWrapper.swift new file mode 100644 index 000000000..b4d39eee0 --- /dev/null +++ b/Tests/Utils/MockDispatchQueueWrapper.swift @@ -0,0 +1,32 @@ +// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. + +import Foundation +@testable import SnowplowTracker + +class MockDispatchQueueWrapper: DispatchQueueWrapperProtocol { + private let queue: DispatchQueue + + init(label: String) { + queue = DispatchQueue(label: label) + } + + func sync(_ callback: @escaping () -> Void) { + queue.sync(execute: callback) + } + + func async(_ callback: @escaping () -> Void) { + // execute synchronously! + queue.sync(execute: callback) + } +}