Skip to content

Commit

Permalink
Process tracked events on a serial background queue (close #822)
Browse files Browse the repository at this point in the history
* Add synchronous threads

* Use serial queue

* Make the queue an instance variable

* Add dispatchqueue wraooer

* Use MockDispatchQueueWrapper in tests

* Use queue for tracker setters

* Respond to review comments

* Update Examples repo
  • Loading branch information
mscwilson authored and matus-tomlein committed Oct 10, 2023
1 parent 48a0d06 commit 82afd0f
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 42 deletions.
67 changes: 34 additions & 33 deletions Sources/Core/Tracker/Tracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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? {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Core/Tracker/TrackerEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions Sources/Core/Utils/DispatchQueueWrapper.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
19 changes: 19 additions & 0 deletions Sources/Core/Utils/DispatchQueueWrapperProtocol.swift
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions Tests/Configurations/TestTrackerConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions Tests/Configurations/TestTrackerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
15 changes: 8 additions & 7 deletions Tests/TestSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down
32 changes: 32 additions & 0 deletions Tests/Utils/MockDispatchQueueWrapper.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 82afd0f

Please sign in to comment.