diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38d8a174a..d94a98bc0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,8 +87,16 @@ jobs: -sdk "${{ matrix.sdk }}" \ -destination "${{ matrix.destination }}" \ -only-testing ${{ matrix.target }} \ + -resultBundlePath TestResults \ clean test | xcpretty + - name: Create test results + uses: kishikawakatsumi/xcresulttool@v1 + with: + path: TestResults.xcresult + title: "Test results: ${{ matrix.name }}" + if: success() || failure() + build_objc_demo_app: name: "ObjC demo (iOS ${{ matrix.version.ios }})" needs: test_framework diff --git a/Sources/Core/Emitter/Emitter.swift b/Sources/Core/Emitter/Emitter.swift index 70e6d0add..1c51f973b 100644 --- a/Sources/Core/Emitter/Emitter.swift +++ b/Sources/Core/Emitter/Emitter.swift @@ -16,107 +16,79 @@ import Foundation /// This class sends events to the collector. let POST_WRAPPER_BYTES = 88 -class Emitter: NSObject, EmitterEventProcessing { +class Emitter: EmitterEventProcessing { - private var timer: Timer? - private var dataOperationQueue: OperationQueue = OperationQueue() - private var builderFinished = false + private var timer: InternalQueueTimer? + + private var pausedEmit = false + + /// Custom NetworkConnection instance to handle connection outside the emitter. + private let networkConnection: NetworkConnection + + /// Tracker namespace – required by SQLiteEventStore to name the database + let namespace: String + + let eventStore: EventStore - private var sendingCheck = SendingCheck() /// Whether the emitter is currently sending. - var isSending: Bool { return sendingCheck.sending } + var isSending: Bool = false - private var _urlEndpoint: String? /// Collector endpoint. var urlEndpoint: String? { get { - if builderFinished { - return networkConnection?.urlEndpoint?.absoluteString + if let networkConnection = networkConnection as? DefaultNetworkConnection { + return networkConnection.urlEndpoint?.absoluteString } - return _urlEndpoint + return nil } set { - _urlEndpoint = newValue - if builderFinished { - setupNetworkConnection() + if let urlString = newValue, + let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.urlString = urlString } } } - - private var _namespace: String? - var namespace: String? { + + /// Security of requests - ProtocolHttp or ProtocolHttps. + var `protocol`: ProtocolOptions { get { - return _namespace + if let networkConnection = networkConnection as? DefaultNetworkConnection { + return networkConnection.protocol + } + return EmitterDefaults.httpProtocol } - set(namespace) { - _namespace = namespace - if builderFinished && eventStore == nil { - #if os(tvOS) || os(watchOS) - eventStore = MemoryEventStore() - #else - eventStore = SQLiteEventStore(namespace: _namespace) - #endif + set { + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.protocol = newValue } } } - - private var _method: HttpMethodOptions = EmitterDefaults.httpMethod + /// Chosen HTTP method - .get or .post. var method: HttpMethodOptions { get { - return _method - } - set(method) { - _method = method - if builderFinished && networkConnection != nil { - setupNetworkConnection() + if let networkConnection = networkConnection as? DefaultNetworkConnection { + return networkConnection.httpMethod } + return EmitterDefaults.httpMethod } - } - - private var _protocol: ProtocolOptions = EmitterDefaults.httpProtocol - /// Security of requests - ProtocolHttp or ProtocolHttps. - var `protocol`: ProtocolOptions { - get { - return _protocol - } - set(`protocol`) { - _protocol = `protocol` - if builderFinished && networkConnection != nil { - setupNetworkConnection() + set(method) { + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.httpMethod = method } } } - private var _bufferOption: BufferOption = EmitterDefaults.bufferOption + /// Buffer option - var bufferOption: BufferOption { - get { - return _bufferOption - } - set(bufferOption) { - if !isSending { - _bufferOption = bufferOption - } - } - } + var bufferOption: BufferOption = EmitterDefaults.bufferOption - private weak var _callback: RequestCallback? /// Callbacks supplied with number of failures and successes of sent events. - var callback: RequestCallback? { - get { - return _callback - } - set(callback) { - _callback = callback - } - } + weak var callback: RequestCallback? private var _emitRange = EmitterDefaults.emitRange /// Number of events retrieved from the database when needed. var emitRange: Int { - get { - return _emitRange - } + get { return _emitRange } set(emitRange) { if emitRange > 0 { _emitRange = emitRange @@ -124,35 +96,31 @@ class Emitter: NSObject, EmitterEventProcessing { } } - private var _emitThreadPoolSize = EmitterDefaults.emitThreadPoolSize /// Number of threads used for emitting events. var emitThreadPoolSize: Int { get { - return _emitThreadPoolSize + if let networkConnection = networkConnection as? DefaultNetworkConnection { + return networkConnection.emitThreadPoolSize + } + return EmitterDefaults.emitThreadPoolSize } set(emitThreadPoolSize) { if emitThreadPoolSize > 0 { - _emitThreadPoolSize = emitThreadPoolSize - if dataOperationQueue.maxConcurrentOperationCount != emitThreadPoolSize { - dataOperationQueue.maxConcurrentOperationCount = _emitThreadPoolSize - } - if builderFinished && networkConnection != nil { - setupNetworkConnection() + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.emitThreadPoolSize = emitThreadPoolSize } } } } - private var _byteLimitGet = EmitterDefaults.byteLimitGet /// Byte limit for GET requests. + private var _byteLimitGet = EmitterDefaults.byteLimitGet var byteLimitGet: Int { - get { - return _byteLimitGet - } + get { return _byteLimitGet } set(byteLimitGet) { _byteLimitGet = byteLimitGet - if builderFinished && networkConnection != nil { - setupNetworkConnection() + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.byteLimitGet = byteLimitGet } } } @@ -160,81 +128,56 @@ class Emitter: NSObject, EmitterEventProcessing { private var _byteLimitPost = EmitterDefaults.byteLimitPost /// Byte limit for POST requests. var byteLimitPost: Int { - get { - return _byteLimitPost - } + get { return _byteLimitPost } set(byteLimitPost) { _byteLimitPost = byteLimitPost - if builderFinished && networkConnection != nil { - setupNetworkConnection() + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.byteLimitPost = byteLimitPost } } } - private var _serverAnonymisation = EmitterDefaults.serverAnonymisation /// Whether to anonymise server-side user identifiers including the `network_userid` and `user_ipaddress`. var serverAnonymisation: Bool { get { - return _serverAnonymisation + if let networkConnection = networkConnection as? DefaultNetworkConnection { + return networkConnection.serverAnonymisation + } + return EmitterDefaults.serverAnonymisation } set(serverAnonymisation) { - _serverAnonymisation = serverAnonymisation - if builderFinished && networkConnection != nil { - setupNetworkConnection() + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.serverAnonymisation = serverAnonymisation } } } - private var _customPostPath: String? /// Custom endpoint path for POST requests. var customPostPath: String? { get { - return _customPostPath + if let networkConnection = networkConnection as? DefaultNetworkConnection { + return networkConnection.customPostPath + } + return nil } set(customPath) { - _customPostPath = customPath - if builderFinished && networkConnection != nil { - setupNetworkConnection() + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.customPostPath = customPath } } } /// Custom header requests. - private var _requestHeaders: [String : String]? var requestHeaders: [String : String]? { get { - return _requestHeaders - } - set(requestHeaders) { - _requestHeaders = requestHeaders - if builderFinished && networkConnection != nil { - setupNetworkConnection() + if let networkConnection = networkConnection as? DefaultNetworkConnection { + return networkConnection.requestHeaders } + return nil } - } - - private var _networkConnection: NetworkConnection? - /// Custom NetworkConnection istance to handle connection outside the emitter. - var networkConnection: NetworkConnection? { - get { - return _networkConnection - } - set(networkConnection) { - _networkConnection = networkConnection - if builderFinished && _networkConnection != nil { - setupNetworkConnection() - } - } - } - - private var _eventStore: EventStore? - var eventStore: EventStore? { - get { - return _eventStore - } - set(eventStore) { - if !builderFinished || self.eventStore == nil || self.eventStore?.count() == 0 { - _eventStore = eventStore + set(requestHeaders) { + if let networkConnection = networkConnection as? DefaultNetworkConnection { + networkConnection.requestHeaders = requestHeaders } } } @@ -242,12 +185,8 @@ class Emitter: NSObject, EmitterEventProcessing { /// Custom retry rules for HTTP status codes. private var _customRetryForStatusCodes: [Int : Bool] = [:] var customRetryForStatusCodes: [Int : Bool]? { - get { - return _customRetryForStatusCodes - } - set(customRetryForStatusCodes) { - _customRetryForStatusCodes = customRetryForStatusCodes ?? [:] - } + get { return _customRetryForStatusCodes } + set { _customRetryForStatusCodes = newValue ?? [:] } } /// Whether retrying failed requests is allowed @@ -255,73 +194,67 @@ class Emitter: NSObject, EmitterEventProcessing { /// Returns the number of events in the DB. var dbCount: Int { - return Int(eventStore?.count() ?? 0) + return Int(eventStore.count()) } // MARK: - Initialization - init(urlEndpoint: String, - builder: ((Emitter) -> (Void))) { - super.init() - self._urlEndpoint = urlEndpoint + init(namespace: String, + urlEndpoint: String, + method: HttpMethodOptions? = nil, + protocol: ProtocolOptions? = nil, + customPostPath: String? = nil, + requestHeaders: [String: String]? = nil, + serverAnonymisation: Bool? = nil, + eventStore: EventStore? = nil, + builder: ((Emitter) -> (Void))? = nil) { + self.namespace = namespace + self.eventStore = eventStore ?? Emitter.defaultEventStore(namespace: namespace) + + let defaultNetworkConnection = DefaultNetworkConnection( + urlString: urlEndpoint, + httpMethod: method ?? EmitterDefaults.httpMethod, + customPostPath: customPostPath + ) + defaultNetworkConnection.requestHeaders = requestHeaders + defaultNetworkConnection.serverAnonymisation = serverAnonymisation ?? EmitterDefaults.serverAnonymisation + networkConnection = defaultNetworkConnection - builder(self) - setup() - } + builder?(self) + resumeTimer() + } init(networkConnection: NetworkConnection, - builder: ((Emitter) -> (Void))) { - super.init() - self._networkConnection = networkConnection + namespace: String, + eventStore: EventStore? = nil, + builder: ((Emitter) -> (Void))? = nil) { + self.networkConnection = networkConnection + self.namespace = namespace + self.eventStore = eventStore ?? Emitter.defaultEventStore(namespace: namespace) - builder(self) - setup() - } - - private func setup() { - dataOperationQueue.maxConcurrentOperationCount = emitThreadPoolSize - setupNetworkConnection() + builder?(self) resumeTimer() - builderFinished = true } - - private func setupNetworkConnection() { - if !builderFinished && networkConnection != nil { - return - } - if let url = _urlEndpoint { - var endpoint = "\(url)" - if !endpoint.hasPrefix("http") { - let `protocol` = self.protocol == .https ? "https://" : "http://" - endpoint = `protocol` + endpoint - } - let defaultNetworkConnection = DefaultNetworkConnection( - urlString: endpoint, - httpMethod: method, - customPostPath: customPostPath - ) - defaultNetworkConnection.requestHeaders = requestHeaders - defaultNetworkConnection.emitThreadPoolSize = emitThreadPoolSize - defaultNetworkConnection.byteLimitGet = byteLimitGet - defaultNetworkConnection.byteLimitPost = byteLimitPost - defaultNetworkConnection.serverAnonymisation = serverAnonymisation - _networkConnection = defaultNetworkConnection - } + + deinit { + pauseTimer() + } + + private static func defaultEventStore(namespace: String) -> EventStore { +#if os(tvOS) || os(watchOS) + return MemoryEventStore() +#else + return SQLiteEventStore(namespace: namespace) +#endif } // MARK: - Pause/Resume methods func resumeTimer() { - weak var weakSelf = self - - if timer != nil { - pauseTimer() - } + pauseTimer() - DispatchQueue.main.async { - weakSelf?.timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(kSPDefaultBufferTimeout), repeats: true) { [weak self] timer in - self?.flush() - } + self.timer = InternalQueue.startTimer(TimeInterval(kSPDefaultBufferTimeout)) { [weak self] in + self?.flush() } } @@ -333,118 +266,113 @@ class Emitter: NSObject, EmitterEventProcessing { /// Allows sending events to collector. func resumeEmit() { - sendingCheck.pausedEmit = false + pausedEmit = false flush() } /// Suspends sending events to collector. func pauseEmit() { - sendingCheck.pausedEmit = true + pausedEmit = true } /// Insert a Payload object into the buffer to be sent to collector. /// This method will add the payload to the database and flush (send all events). /// - Parameter eventPayload: A Payload containing a completed event to be added into the buffer. func addPayload(toBuffer eventPayload: Payload) { - DispatchQueue.global(qos: .default).async { [weak self] in - self?.eventStore?.addEvent(eventPayload) - self?.flush() - } + self.eventStore.addEvent(eventPayload) + self.flush() } /// Empties the buffer of events using the respective HTTP request method. func flush() { - if Thread.isMainThread { - DispatchQueue.global(qos: .default).async { [self] in - sendGuard() - } - } else { - sendGuard() + if requestToStartSending() { + self.attemptEmit() } } // MARK: - Control methods - private func sendGuard() { - if sendingCheck.requestToStartSending() { - objc_sync_enter(self) - attemptEmit() - objc_sync_exit(self) - sendingCheck.sending = false - } - } - private func attemptEmit() { - guard let eventStore = eventStore else { return } - if eventStore.count() == 0 { + InternalQueue.onQueuePrecondition() + + let events = eventStore.emittableEvents(withQueryLimit: UInt(emitRange)) + if events.isEmpty { logDebug(message: "Database empty. Returning.") + stopSending() return } - - let events = eventStore.emittableEvents(withQueryLimit: UInt(emitRange)) + let requests = buildRequests(fromEvents: events) - let sendResults = networkConnection?.sendRequests(requests) - - logVerbose(message: "Processing emitter results.") - - var successCount = 0 - var failedWillRetryCount = 0 - var failedWontRetryCount = 0 - var removableEvents: [Int64] = [] - - for result in sendResults ?? [] { - let resultIndexArray = result.storeIds - if result.isSuccessful { - successCount += resultIndexArray?.count ?? 0 - if let array = resultIndexArray { - removableEvents.append(contentsOf: array) + + let processResults: ([RequestResult]) -> Void = { sendResults in + logVerbose(message: "Processing emitter results.") + + var successCount = 0 + var failedWillRetryCount = 0 + var failedWontRetryCount = 0 + var removableEvents: [Int64] = [] + + for result in sendResults { + let resultIndexArray = result.storeIds + if result.isSuccessful { + successCount += resultIndexArray?.count ?? 0 + if let array = resultIndexArray { + removableEvents.append(contentsOf: array) + } + } else if result.shouldRetry(self.customRetryForStatusCodes, retryAllowed: self.retryFailedRequests) { + failedWillRetryCount += resultIndexArray?.count ?? 0 + } else { + failedWontRetryCount += resultIndexArray?.count ?? 0 + if let array = resultIndexArray { + removableEvents.append(contentsOf: array) + } + logError(message: String(format: "Sending events to Collector failed with status %ld. Events will be dropped.", result.statusCode ?? -1)) } - } else if result.shouldRetry(customRetryForStatusCodes, retryAllowed: retryFailedRequests) { - failedWillRetryCount += resultIndexArray?.count ?? 0 - } else { - failedWontRetryCount += resultIndexArray?.count ?? 0 - if let array = resultIndexArray { - removableEvents.append(contentsOf: array) + } + let allFailureCount = failedWillRetryCount + failedWontRetryCount + + _ = self.eventStore.removeEvents(withIds: removableEvents) + + logDebug(message: String(format: "Success Count: %d", successCount)) + logDebug(message: String(format: "Failure Count: %d", allFailureCount)) + + if let callback = self.callback { + if allFailureCount == 0 { + callback.onSuccess(withCount: successCount) + } else { + callback.onFailure(withCount: allFailureCount, successCount: successCount) } - logError(message: String(format: "Sending events to Collector failed with status %ld. Events will be dropped.", result.statusCode ?? -1)) } - } - let allFailureCount = failedWillRetryCount + failedWontRetryCount - - let _ = eventStore.removeEvents(withIds: removableEvents) - - logDebug(message: String(format: "Success Count: %d", successCount)) - logDebug(message: String(format: "Failure Count: %d", allFailureCount)) - - if callback != nil { - if allFailureCount == 0 { - callback?.onSuccess(withCount: successCount) + + if failedWillRetryCount > 0 && successCount == 0 { + logDebug(message: "Ending emitter run as all requests failed.") + + self.scheduleStopSending() } else { - callback?.onFailure(withCount: allFailureCount, successCount: successCount) + self.attemptEmit() } } - - if failedWillRetryCount > 0 && successCount == 0 { - logDebug(message: "Ending emitter run as all requests failed.") - Thread.sleep(forTimeInterval: 5) - return - } else { - self.attemptEmit() + + emitAsync { + let sendResults = self.networkConnection.sendRequests(requests) + + InternalQueue.async { + processResults(sendResults) + } } } private func buildRequests(fromEvents events: [EmitterEvent]) -> [Request] { var requests: [Request] = [] - guard let networkConnection = networkConnection else { return requests } let sendingTime = Utilities.getTimestamp() - let httpMethod = networkConnection.httpMethod + let byteLimit = method == .get ? byteLimitGet : byteLimitPost - if httpMethod == .get { + if method == .get { for event in events { let payload = event.payload addSendingTime(to: payload, timestamp: sendingTime) - let oversize = isOversize(payload) + let oversize = isOversize(payload, byteLimit: byteLimit) let request = Request(payload: payload, emitterEventId: event.storeId, oversize: oversize) requests.append(request) } @@ -462,10 +390,10 @@ class Emitter: NSObject, EmitterEventProcessing { let emitterEventId = event.storeId addSendingTime(to: payload, timestamp: sendingTime) - if isOversize(payload) { + if isOversize(payload, byteLimit: byteLimit) { let request = Request(payload: payload, emitterEventId: emitterEventId, oversize: true) requests.append(request) - } else if isOversize(payload, previousPayloads: eventArray) { + } else if isOversize(payload, byteLimit: byteLimit, previousPayloads: eventArray) { let request = Request(payloads: eventArray, emitterEventIds: indexArray) requests.append(request) @@ -494,16 +422,7 @@ class Emitter: NSObject, EmitterEventProcessing { return requests } - private func isOversize(_ payload: Payload) -> Bool { - return isOversize(payload, previousPayloads: []) - } - - private func isOversize(_ payload: Payload, previousPayloads: [Payload]) -> Bool { - let byteLimit = networkConnection?.httpMethod == .get ? byteLimitGet : byteLimitPost - return isOversize(payload, byteLimit: byteLimit, previousPayloads: previousPayloads) - } - - private func isOversize(_ payload: Payload, byteLimit: Int, previousPayloads: [Payload]) -> Bool { + private func isOversize(_ payload: Payload, byteLimit: Int, previousPayloads: [Payload] = []) -> Bool { var totalByteSize = payload.byteSize for previousPayload in previousPayloads { totalByteSize += previousPayload.byteSize @@ -512,50 +431,34 @@ class Emitter: NSObject, EmitterEventProcessing { return totalByteSize + wrapperBytes > byteLimit } - func addSendingTime(to payload: Payload, timestamp: NSNumber) { + private func addSendingTime(to payload: Payload, timestamp: NSNumber) { payload.addValueToPayload(String(format: "%lld", timestamp.int64Value), forKey: kSPSentTimestamp) } - - deinit { - pauseTimer() - } -} - -fileprivate class SendingCheck { - private var _sending = false - var sending: Bool { - get { - return lock { return _sending } - } - set { - lock { _sending = newValue } + + private func requestToStartSending() -> Bool { + if !isSending && !pausedEmit { + isSending = true + return true + } else { + return false } } - private var _pausedEmit = false - var pausedEmit: Bool { - get { - return lock { return _pausedEmit } - } - set { - lock { _pausedEmit = newValue } + private func scheduleStopSending() { + InternalQueue.asyncAfter(TimeInterval(5)) { [weak self] in + self?.stopSending() } } - func requestToStartSending() -> Bool { - return lock { - if !_sending && !_pausedEmit { - _sending = true - return true - } else { - return false - } - } + private func stopSending() { + isSending = false } - private func lock(closure: () -> T) -> T { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - return closure() + // MARK: - dispatch queues + + private let emitQueue = DispatchQueue(label: "snowplow.emitter") + + private func emitAsync(_ callback: @escaping () -> Void) { + emitQueue.async(execute: callback) } } diff --git a/Sources/Core/InternalQueue/EcommerceControllerIQWrapper.swift b/Sources/Core/InternalQueue/EcommerceControllerIQWrapper.swift new file mode 100644 index 000000000..da2477cba --- /dev/null +++ b/Sources/Core/InternalQueue/EcommerceControllerIQWrapper.swift @@ -0,0 +1,39 @@ +// 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 EcommerceControllerIQWrapper: EcommerceController { + + private let controller: EcommerceController + + init(controller: EcommerceController) { + self.controller = controller + } + + func setEcommerceScreen(_ screen: EcommerceScreenEntity) { + InternalQueue.sync { controller.setEcommerceScreen(screen) } + } + + func setEcommerceUser(_ user: EcommerceUserEntity) { + InternalQueue.sync { controller.setEcommerceUser(user) } + } + + func removeEcommerceScreen() { + InternalQueue.sync { controller.removeEcommerceScreen() } + } + + func removeEcommerceUser() { + InternalQueue.sync { controller.removeEcommerceUser() } + } +} diff --git a/Sources/Core/InternalQueue/EmitterControllerIQWrapper.swift b/Sources/Core/InternalQueue/EmitterControllerIQWrapper.swift new file mode 100644 index 000000000..ce60d20af --- /dev/null +++ b/Sources/Core/InternalQueue/EmitterControllerIQWrapper.swift @@ -0,0 +1,93 @@ +// 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 EmitterControllerIQWrapper: EmitterController { + + private let controller: EmitterController + + init(controller: EmitterController) { + self.controller = controller + } + + // MARK: - Properties + + var bufferOption: BufferOption { + get { return InternalQueue.sync { controller.bufferOption } } + set { InternalQueue.sync { controller.bufferOption = newValue } } + } + + var byteLimitGet: Int { + get { return InternalQueue.sync { controller.byteLimitGet } } + set { InternalQueue.sync { controller.byteLimitGet = newValue } } + } + + var byteLimitPost: Int { + get { return InternalQueue.sync { controller.byteLimitPost } } + set { InternalQueue.sync { controller.byteLimitPost = newValue } } + } + + var serverAnonymisation: Bool { + get { return InternalQueue.sync { controller.serverAnonymisation } } + set { InternalQueue.sync { controller.serverAnonymisation = newValue } } + } + + var emitRange: Int { + get { return InternalQueue.sync { controller.emitRange } } + set { InternalQueue.sync { controller.emitRange = newValue } } + } + + var threadPoolSize: Int { + get { return InternalQueue.sync { controller.threadPoolSize } } + set { InternalQueue.sync { controller.threadPoolSize = newValue } } + } + + var requestCallback: RequestCallback? { + get { return InternalQueue.sync { controller.requestCallback } } + set { InternalQueue.sync { controller.requestCallback = newValue } } + } + + var dbCount: Int { + return InternalQueue.sync { controller.dbCount } + } + + var isSending: Bool { + return InternalQueue.sync { controller.isSending } + } + + var customRetryForStatusCodes: [Int : Bool]? { + get { return InternalQueue.sync { controller.customRetryForStatusCodes } } + set { InternalQueue.sync { controller.customRetryForStatusCodes = newValue } } + } + + var retryFailedRequests: Bool { + get { return InternalQueue.sync { controller.retryFailedRequests } } + set { InternalQueue.sync { controller.retryFailedRequests = newValue } } + } + + // MARK: - Methods + + func flush() { + InternalQueue.sync { controller.flush() } + } + + func pause() { + InternalQueue.sync { controller.pause() } + } + + func resume() { + InternalQueue.sync { controller.resume() } + } + +} diff --git a/Sources/Core/InternalQueue/GDPRControllerIQWrapper.swift b/Sources/Core/InternalQueue/GDPRControllerIQWrapper.swift new file mode 100644 index 000000000..a03cecc8b --- /dev/null +++ b/Sources/Core/InternalQueue/GDPRControllerIQWrapper.swift @@ -0,0 +1,60 @@ +// 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 GDPRControllerIQWrapper: GDPRController { + + private let controller: GDPRController + + init(controller: GDPRController) { + self.controller = controller + } + + // MARK: - Methods + + func reset(basis: GDPRProcessingBasis, documentId: String?, documentVersion: String?, documentDescription: String?) { + InternalQueue.sync { + controller.reset(basis: basis, documentId: documentId, documentVersion: documentVersion, documentDescription: documentDescription) + } + } + + func disable() { + InternalQueue.sync { controller.disable() } + } + + var isEnabled: Bool { + return InternalQueue.sync { controller.isEnabled } + } + + func enable() -> Bool { + InternalQueue.sync { controller.enable() } + } + + var basisForProcessing: GDPRProcessingBasis { + InternalQueue.sync { controller.basisForProcessing } + } + + var documentId: String? { + InternalQueue.sync { controller.documentId } + } + + var documentVersion: String? { + InternalQueue.sync { controller.documentVersion } + } + + var documentDescription: String? { + InternalQueue.sync { controller.documentDescription } + } + +} diff --git a/Sources/Core/InternalQueue/GlobalContextsControllerIQWrapper.swift b/Sources/Core/InternalQueue/GlobalContextsControllerIQWrapper.swift new file mode 100644 index 000000000..86c47375d --- /dev/null +++ b/Sources/Core/InternalQueue/GlobalContextsControllerIQWrapper.swift @@ -0,0 +1,41 @@ +// 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 GlobalContextsControllerIQWrapper: GlobalContextsController { + + private let controller: GlobalContextsController + + init(controller: GlobalContextsController) { + self.controller = controller + } + + var contextGenerators: [String : GlobalContext] { + get { InternalQueue.sync { controller.contextGenerators } } + set { InternalQueue.sync { controller.contextGenerators = newValue } } + } + + func add(tag: String, contextGenerator generator: GlobalContext) -> Bool { + return InternalQueue.sync { controller.add(tag: tag, contextGenerator: generator) } + } + + func remove(tag: String) -> GlobalContext? { + return InternalQueue.sync { controller.remove(tag: tag) } + } + + var tags: [String] { + return InternalQueue.sync { controller.tags } + } + +} diff --git a/Sources/Core/InternalQueue/InternalQueue.swift b/Sources/Core/InternalQueue/InternalQueue.swift new file mode 100644 index 000000000..93f739597 --- /dev/null +++ b/Sources/Core/InternalQueue/InternalQueue.swift @@ -0,0 +1,56 @@ +// 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 InternalQueue { + static func sync(_ callback: () -> T) -> T { + dispatchPrecondition(condition: .notOnQueue(serialQueue)) + + return serialQueue.sync(execute: callback) + } + + static func async(_ callback: @escaping () -> Void) { + serialQueue.async(execute: callback) + } + + static func asyncAfter(_ interval: TimeInterval, _ callback: @escaping () -> Void) { + serialQueue.asyncAfter(deadline: .now() + interval, execute: callback) + } + + static func startTimer(_ interval: TimeInterval, _ callback: @escaping () -> Void) -> InternalQueueTimer { + let timer = InternalQueueTimer() + + asyncAfter(interval) { + timerFired(timer: timer, interval: interval, callback: callback) + } + + return timer + } + + static private func timerFired(timer: InternalQueueTimer, interval: TimeInterval, callback: @escaping () -> Void) { + if timer.active { + asyncAfter(interval) { + timerFired(timer: timer, interval: interval, callback: callback) + } + + callback() + } + } + + static func onQueuePrecondition() { + dispatchPrecondition(condition: .onQueue(serialQueue)) + } + + private static let serialQueue = DispatchQueue(label: "snowplow") +} diff --git a/Sources/Core/Utils/DispatchQueueWrapperProtocol.swift b/Sources/Core/InternalQueue/InternalQueueTimer.swift similarity index 83% rename from Sources/Core/Utils/DispatchQueueWrapperProtocol.swift rename to Sources/Core/InternalQueue/InternalQueueTimer.swift index db52ffcd0..f11103018 100644 --- a/Sources/Core/Utils/DispatchQueueWrapperProtocol.swift +++ b/Sources/Core/InternalQueue/InternalQueueTimer.swift @@ -13,7 +13,10 @@ import Foundation -protocol DispatchQueueWrapperProtocol: AnyObject { - func sync(_ callback: @escaping () -> Void) - func async(_ callback: @escaping () -> Void) +class InternalQueueTimer { + var active = true + + func invalidate() { + active = false + } } diff --git a/Sources/Core/InternalQueue/MediaControllerIQWrapper.swift b/Sources/Core/InternalQueue/MediaControllerIQWrapper.swift new file mode 100644 index 000000000..7c766721a --- /dev/null +++ b/Sources/Core/InternalQueue/MediaControllerIQWrapper.swift @@ -0,0 +1,59 @@ +// 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 +#if !os(watchOS) +import AVKit +#endif + +class MediaControllerIQWrapper: MediaController { + + private let controller: MediaController + + init(controller: MediaController) { + self.controller = controller + } + + func startMediaTracking(id: String) -> MediaTracking { + return InternalQueue.sync { + MediaTrackingIQWrapper(tracking: controller.startMediaTracking(id: id)) + } + } + + func startMediaTracking(id: String, player: MediaPlayerEntity? = nil) -> MediaTracking { + return InternalQueue.sync { + MediaTrackingIQWrapper(tracking: controller.startMediaTracking(id: id, player: player)) + } + } + + func startMediaTracking(configuration: MediaTrackingConfiguration) -> MediaTracking { + return InternalQueue.sync { + MediaTrackingIQWrapper(tracking: controller.startMediaTracking(configuration: configuration)) + } + } + +#if !os(watchOS) + func startMediaTracking(player: AVPlayer, + configuration: MediaTrackingConfiguration) -> MediaTracking { + return InternalQueue.sync { controller.startMediaTracking(player: player, configuration: configuration) } + } +#endif + + func mediaTracking(id: String) -> MediaTracking? { + return InternalQueue.sync { controller.mediaTracking(id: id) } + } + + func endMediaTracking(id: String) { + InternalQueue.sync { controller.endMediaTracking(id: id) } + } +} diff --git a/Sources/Core/InternalQueue/MediaTrackingIQWrapper.swift b/Sources/Core/InternalQueue/MediaTrackingIQWrapper.swift new file mode 100644 index 000000000..7d27f86ce --- /dev/null +++ b/Sources/Core/InternalQueue/MediaTrackingIQWrapper.swift @@ -0,0 +1,68 @@ +// 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 MediaTrackingIQWrapper: MediaTracking { + + private let tracking: MediaTracking + + init(tracking: MediaTracking) { + self.tracking = tracking + } + + var id: String { + return InternalQueue.sync { tracking.id } + } + + // MARK: Update methods overloads + + func update(player: MediaPlayerEntity?) { + return InternalQueue.sync { tracking.update(player: player) } + } + + func update(player: MediaPlayerEntity?, ad: MediaAdEntity?, adBreak: MediaAdBreakEntity?) { + return InternalQueue.sync { tracking.update(player: player, ad: ad, adBreak: adBreak) } + } + + // MARK: Track methods overloads + + func track(_ event: Event) { + InternalQueue.sync { tracking.track(event) } + } + + func track(_ event: Event, player: MediaPlayerEntity?) { + InternalQueue.sync { tracking.track(event, player: player) } + } + + func track(_ event: Event, ad: MediaAdEntity?) { + InternalQueue.sync { tracking.track(event, ad: ad) } + } + + func track(_ event: Event, player: MediaPlayerEntity?, ad: MediaAdEntity?) { + InternalQueue.sync { tracking.track(event, player: player, ad: ad) } + } + + func track(_ event: Event, adBreak: MediaAdBreakEntity?) { + InternalQueue.sync { tracking.track(event, adBreak: adBreak) } + } + + func track(_ event: Event, player: MediaPlayerEntity?, adBreak: MediaAdBreakEntity?) { + InternalQueue.sync { tracking.track(event, player: player, adBreak: adBreak) } + } + + func track(_ event: Event, player: MediaPlayerEntity?, ad: MediaAdEntity?, adBreak: MediaAdBreakEntity?) { + InternalQueue.sync { tracking.track(event, player: player, ad: ad, adBreak: adBreak) } + } + +} diff --git a/Sources/Core/InternalQueue/NetworkControllerIQWrapper.swift b/Sources/Core/InternalQueue/NetworkControllerIQWrapper.swift new file mode 100644 index 000000000..634f3b2f6 --- /dev/null +++ b/Sources/Core/InternalQueue/NetworkControllerIQWrapper.swift @@ -0,0 +1,46 @@ +// 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 NetworkControllerIQWrapper: NetworkController { + + private let controller: NetworkController + + init(controller: NetworkController) { + self.controller = controller + } + + // MARK: - Properties + + var endpoint: String? { + get { return InternalQueue.sync { controller.endpoint } } + set { InternalQueue.sync { controller.endpoint = newValue } } + } + + var method: HttpMethodOptions { + get { return InternalQueue.sync { controller.method } } + set { InternalQueue.sync { controller.method = newValue } } + } + + var customPostPath: String? { + get { return InternalQueue.sync { controller.customPostPath } } + set { InternalQueue.sync { controller.customPostPath = newValue } } + } + + var requestHeaders: [String : String]? { + get { return InternalQueue.sync { controller.requestHeaders } } + set { InternalQueue.sync { controller.requestHeaders = newValue } } + } + +} diff --git a/Sources/Core/Utils/DispatchQueueWrapper.swift b/Sources/Core/InternalQueue/PluginsControllerIQWrapper.swift similarity index 60% rename from Sources/Core/Utils/DispatchQueueWrapper.swift rename to Sources/Core/InternalQueue/PluginsControllerIQWrapper.swift index dff08b648..6611af1c9 100644 --- a/Sources/Core/Utils/DispatchQueueWrapper.swift +++ b/Sources/Core/InternalQueue/PluginsControllerIQWrapper.swift @@ -13,18 +13,23 @@ import Foundation -class DispatchQueueWrapper: DispatchQueueWrapperProtocol { - private let queue: DispatchQueue +class PluginsControllerIQWrapper: PluginsController { - init(label: String) { - queue = DispatchQueue(label: label) + private let controller: PluginsController + + init(controller: PluginsController) { + self.controller = controller } - func sync(_ callback: @escaping () -> Void) { - queue.sync(execute: callback) + var identifiers: [String] { + return InternalQueue.sync { controller.identifiers } } - func async(_ callback: @escaping () -> Void) { - queue.async(execute: callback) + func add(plugin: PluginIdentifiable) { + InternalQueue.sync { controller.add(plugin: plugin) } + } + + func remove(identifier: String) { + InternalQueue.sync { controller.remove(identifier: identifier) } } } diff --git a/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift b/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift new file mode 100644 index 000000000..8529c9322 --- /dev/null +++ b/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift @@ -0,0 +1,87 @@ +// 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 SessionControllerIQWrapper: SessionController { + + private let controller: SessionController + + init(controller: SessionController) { + self.controller = controller + } + + func pause() { + InternalQueue.sync { controller.pause() } + } + + func resume() { + InternalQueue.sync { controller.resume() } + } + + func startNewSession() { + InternalQueue.sync { controller.startNewSession() } + } + + // MARK: - Properties + + var foregroundTimeout: Measurement { + get { InternalQueue.sync { controller.foregroundTimeout } } + set { InternalQueue.sync { controller.foregroundTimeout = newValue } } + } + + var foregroundTimeoutInSeconds: Int { + get { InternalQueue.sync { controller.foregroundTimeoutInSeconds } } + set { InternalQueue.sync { controller.foregroundTimeoutInSeconds = newValue } } + } + + var backgroundTimeout: Measurement { + get { InternalQueue.sync { controller.backgroundTimeout } } + set { InternalQueue.sync { controller.backgroundTimeout = newValue } } + } + + var backgroundTimeoutInSeconds: Int { + get { InternalQueue.sync { controller.backgroundTimeoutInSeconds } } + set { InternalQueue.sync { controller.backgroundTimeoutInSeconds = newValue } } + } + + var onSessionStateUpdate: ((_ sessionState: SessionState) -> Void)? { + get { InternalQueue.sync { controller.onSessionStateUpdate } } + set { InternalQueue.sync { controller.onSessionStateUpdate = newValue } } + } + + var sessionIndex: Int { + InternalQueue.sync { controller.sessionIndex } + } + + var sessionId: String? { + InternalQueue.sync { controller.sessionId } + } + + var userId: String? { + InternalQueue.sync { controller.userId } + } + + var isInBackground: Bool { + InternalQueue.sync { controller.isInBackground } + } + + var backgroundIndex: Int { + InternalQueue.sync { controller.backgroundIndex } + } + + var foregroundIndex: Int { + InternalQueue.sync { controller.foregroundIndex } + } + +} diff --git a/Sources/Core/InternalQueue/SubjectControllerIQWrapper.swift b/Sources/Core/InternalQueue/SubjectControllerIQWrapper.swift new file mode 100644 index 000000000..b2c2a6a1d --- /dev/null +++ b/Sources/Core/InternalQueue/SubjectControllerIQWrapper.swift @@ -0,0 +1,118 @@ +// 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 SubjectControllerIQWrapper: SubjectController { + + private let controller: SubjectController + + init(controller: SubjectController) { + self.controller = controller + } + + // MARK: - Properties + + var userId: String? { + get { return InternalQueue.sync { controller.userId } } + set { InternalQueue.sync { controller.userId = newValue } } + } + + var networkUserId: String? { + get { return InternalQueue.sync { controller.networkUserId } } + set { InternalQueue.sync { controller.networkUserId = newValue } } + } + + var domainUserId: String? { + get { return InternalQueue.sync { controller.domainUserId } } + set { InternalQueue.sync { controller.domainUserId = newValue } } + } + + var useragent: String? { + get { return InternalQueue.sync { controller.useragent } } + set { InternalQueue.sync { controller.useragent = newValue } } + } + + var ipAddress: String? { + get { return InternalQueue.sync { controller.ipAddress } } + set { InternalQueue.sync { controller.ipAddress = newValue } } + } + + var timezone: String? { + get { return InternalQueue.sync { controller.timezone } } + set { InternalQueue.sync { controller.timezone = newValue } } + } + + var language: String? { + get { return InternalQueue.sync { controller.language } } + set { InternalQueue.sync { controller.language = newValue } } + } + + var screenResolution: SPSize? { + get { return InternalQueue.sync { controller.screenResolution } } + set { InternalQueue.sync { controller.screenResolution = newValue } } + } + + var screenViewPort: SPSize? { + get { return InternalQueue.sync { controller.screenViewPort } } + set { InternalQueue.sync { controller.screenViewPort = newValue } } + } + + var colorDepth: NSNumber? { + get { return InternalQueue.sync { controller.colorDepth } } + set { InternalQueue.sync { controller.colorDepth = newValue } } + } + + // MARK: - GeoLocalization + + var geoLatitude: NSNumber? { + get { return InternalQueue.sync { controller.geoLatitude } } + set { InternalQueue.sync { controller.geoLatitude = newValue } } + } + + var geoLongitude: NSNumber? { + get { return InternalQueue.sync { controller.geoLongitude } } + set { InternalQueue.sync { controller.geoLongitude = newValue } } + } + + var geoLatitudeLongitudeAccuracy: NSNumber? { + get { return InternalQueue.sync { controller.geoLatitudeLongitudeAccuracy } } + set { InternalQueue.sync { controller.geoLatitudeLongitudeAccuracy = newValue } } + } + + var geoAltitude: NSNumber? { + get { return InternalQueue.sync { controller.geoAltitude } } + set { InternalQueue.sync { controller.geoAltitude = newValue } } + } + + var geoAltitudeAccuracy: NSNumber? { + get { return InternalQueue.sync { controller.geoAltitudeAccuracy } } + set { InternalQueue.sync { controller.geoAltitudeAccuracy = newValue } } + } + + var geoSpeed: NSNumber? { + get { return InternalQueue.sync { controller.geoSpeed } } + set { InternalQueue.sync { controller.geoSpeed = newValue } } + } + + var geoBearing: NSNumber? { + get { return InternalQueue.sync { controller.geoBearing } } + set { InternalQueue.sync { controller.geoBearing = newValue } } + } + + var geoTimestamp: NSNumber? { + get { return InternalQueue.sync { controller.geoTimestamp } } + set { InternalQueue.sync { controller.geoTimestamp = newValue } } + } + +} diff --git a/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift b/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift new file mode 100644 index 000000000..7e3def3bd --- /dev/null +++ b/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift @@ -0,0 +1,228 @@ +// 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 TrackerControllerIQWrapper: TrackerController { + + private let controller: TrackerControllerImpl + + init(controller: TrackerControllerImpl) { + self.controller = controller + } + + // MARK: - Controllers + + var network: NetworkController? { + return InternalQueue.sync { + if let network = controller.network { + return NetworkControllerIQWrapper(controller: network) + } else { + return nil + } + } + } + + var emitter: EmitterController? { + return InternalQueue.sync { + if let emitter = controller.emitter { + return EmitterControllerIQWrapper(controller: emitter) + } else { + return nil + } + } + } + + var gdpr: GDPRController? { + return InternalQueue.sync { + if let gdpr = controller.gdpr { + return GDPRControllerIQWrapper(controller: gdpr) + } else { + return nil + } + } + } + + var globalContexts: GlobalContextsController? { + return InternalQueue.sync { + if let globalContexts = controller.globalContexts { + return GlobalContextsControllerIQWrapper(controller: globalContexts) + } else { + return nil + } + } + } + + var subject: SubjectController? { + return InternalQueue.sync { + if let subject = controller.subject { + return SubjectControllerIQWrapper(controller: subject) + } else { + return nil + } + } + } + + var session: SessionController? { + return InternalQueue.sync { + if let session = controller.session { + return SessionControllerIQWrapper(controller: session) + } else { + return nil + } + } + } + + var plugins: PluginsController { + return InternalQueue.sync { PluginsControllerIQWrapper(controller: controller.plugins) } + } + + var media: MediaController { + return InternalQueue.sync { MediaControllerIQWrapper(controller: controller.media) } + } + + var ecommerce: EcommerceController { + return InternalQueue.sync { EcommerceControllerIQWrapper(controller: controller.ecommerce) } + } + + // MARK: - Control methods + + func pause() { + InternalQueue.sync { controller.pause() } + } + + func resume() { + InternalQueue.sync { controller.resume() } + } + + func track(_ event: Event) -> UUID { + let eventId = UUID() + InternalQueue.async { self.controller.track(event, eventId: eventId) } + return eventId + } + + // MARK: - Properties' setters and getters + + var appId: String { + get { return InternalQueue.sync { controller.appId } } + set { InternalQueue.sync { controller.appId = newValue } } + } + + var namespace: String { + return InternalQueue.sync { controller.namespace } + } + + var devicePlatform: DevicePlatform { + get { return InternalQueue.sync { controller.devicePlatform } } + set { InternalQueue.sync { controller.devicePlatform = newValue } } + } + + var base64Encoding: Bool { + get { return InternalQueue.sync { controller.base64Encoding } } + set { InternalQueue.sync { controller.base64Encoding = newValue } } + } + + var logLevel: LogLevel { + get { return InternalQueue.sync { controller.logLevel } } + set { InternalQueue.sync { controller.logLevel = newValue } } + } + + var loggerDelegate: LoggerDelegate? { + get { return InternalQueue.sync { controller.loggerDelegate } } + set { InternalQueue.sync { controller.loggerDelegate = newValue } } + } + + var applicationContext: Bool { + get { return InternalQueue.sync { controller.applicationContext } } + set { InternalQueue.sync { controller.applicationContext = newValue } } + } + + var platformContext: Bool { + get { return InternalQueue.sync { controller.platformContext } } + set { InternalQueue.sync { controller.platformContext = newValue } } + } + + var platformContextProperties: [PlatformContextProperty]? { + get { return InternalQueue.sync { controller.platformContextProperties } } + set { InternalQueue.sync { controller.platformContextProperties = newValue } } + } + + var geoLocationContext: Bool { + get { return InternalQueue.sync { controller.geoLocationContext } } + set { InternalQueue.sync { controller.geoLocationContext = newValue } } + } + + var diagnosticAutotracking: Bool { + get { return InternalQueue.sync { controller.diagnosticAutotracking } } + set { InternalQueue.sync { controller.diagnosticAutotracking = newValue } } + } + + var exceptionAutotracking: Bool { + get { return InternalQueue.sync { controller.exceptionAutotracking } } + set { InternalQueue.sync { controller.exceptionAutotracking = newValue } } + } + + var installAutotracking: Bool { + get { return InternalQueue.sync { controller.installAutotracking } } + set { InternalQueue.sync { controller.installAutotracking = newValue } } + } + + var lifecycleAutotracking: Bool { + get { return InternalQueue.sync { controller.lifecycleAutotracking } } + set { InternalQueue.sync { controller.lifecycleAutotracking = newValue } } + } + + var deepLinkContext: Bool { + get { return InternalQueue.sync { controller.deepLinkContext } } + set { InternalQueue.sync { controller.deepLinkContext = newValue } } + } + + var screenContext: Bool { + get { return InternalQueue.sync { controller.screenContext } } + set { InternalQueue.sync { controller.screenContext = newValue } } + } + + var screenViewAutotracking: Bool { + get { return InternalQueue.sync { controller.screenViewAutotracking } } + set { InternalQueue.sync { controller.screenViewAutotracking = newValue } } + } + + var trackerVersionSuffix: String? { + get { return InternalQueue.sync { controller.trackerVersionSuffix } } + set { InternalQueue.sync { controller.trackerVersionSuffix = newValue } } + } + + var sessionContext: Bool { + get { return InternalQueue.sync { controller.sessionContext } } + set { InternalQueue.sync { controller.sessionContext = newValue } } + } + + var userAnonymisation: Bool { + get { return InternalQueue.sync { controller.userAnonymisation } } + set { InternalQueue.sync { controller.userAnonymisation = newValue } } + } + + var advertisingIdentifierRetriever: (() -> UUID?)? { + get { return InternalQueue.sync { controller.advertisingIdentifierRetriever } } + set { InternalQueue.sync { controller.advertisingIdentifierRetriever = newValue } } + } + + var isTracking: Bool { + return InternalQueue.sync { controller.isTracking } + } + + var version: String { + return InternalQueue.sync { controller.version } + } + +} diff --git a/Sources/Core/Media/Controllers/AVPlayerSubscription.swift b/Sources/Core/Media/Controllers/AVPlayerSubscription.swift index a5c319570..0591ab111 100644 --- a/Sources/Core/Media/Controllers/AVPlayerSubscription.swift +++ b/Sources/Core/Media/Controllers/AVPlayerSubscription.swift @@ -47,21 +47,23 @@ class AVPlayerSubscription { // add a playback rate observer to find out when the user plays or pauses the videos rateObserver = player.observe(\.rate, options: [.old, .new]) { [weak self] player, change in - guard let oldRate = change.oldValue else { return } - guard let newRate = change.newValue else { return } - - if oldRate != 0 && newRate == 0 { // paused - self?.lastPauseTime = player.currentTime() - self?.track(MediaPauseEvent()) - } else if oldRate == 0 && newRate != 0 { // started playing - // when the current time diverges significantly, i.e. more than 1 second, from what it was when last paused, track a seek event - if let lastPauseTime = self?.lastPauseTime { - if abs(player.currentTime().seconds - lastPauseTime.seconds) > 1 { - self?.track(MediaSeekEndEvent()) + InternalQueue.async { + guard let oldRate = change.oldValue else { return } + guard let newRate = change.newValue else { return } + + if oldRate != 0 && newRate == 0 { // paused + self?.lastPauseTime = player.currentTime() + self?.track(MediaPauseEvent()) + } else if oldRate == 0 && newRate != 0 { // started playing + // when the current time diverges significantly, i.e. more than 1 second, from what it was when last paused, track a seek event + if let lastPauseTime = self?.lastPauseTime { + if abs(player.currentTime().seconds - lastPauseTime.seconds) > 1 { + self?.track(MediaSeekEndEvent()) + } } + self?.lastPauseTime = nil + self?.track(MediaPlayEvent()) } - self?.lastPauseTime = nil - self?.track(MediaPlayEvent()) } } @@ -92,15 +94,17 @@ class AVPlayerSubscription { /// Handles notifications from the notification center subscriptions @objc private func handleNotification(_ notification: Notification) { - switch notification.name { - case .AVPlayerItemPlaybackStalled: - track(MediaBufferStartEvent()) - case .AVPlayerItemDidPlayToEndTime: - track(MediaEndEvent()) - case .AVPlayerItemFailedToPlayToEndTime: - track(MediaErrorEvent(errorDescription: player.error?.localizedDescription)) - default: - return + InternalQueue.async { + switch notification.name { + case .AVPlayerItemPlaybackStalled: + self.track(MediaBufferStartEvent()) + case .AVPlayerItemDidPlayToEndTime: + self.track(MediaEndEvent()) + case .AVPlayerItemFailedToPlayToEndTime: + self.track(MediaErrorEvent(errorDescription: self.player.error?.localizedDescription)) + default: + return + } } } @@ -135,7 +139,9 @@ class AVPlayerSubscription { positionObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in - self?.update() + InternalQueue.async { + self?.update() + } } } diff --git a/Sources/Core/Media/Controllers/MediaPingInterval.swift b/Sources/Core/Media/Controllers/MediaPingInterval.swift index 03e8369b7..b57a7b7c0 100644 --- a/Sources/Core/Media/Controllers/MediaPingInterval.swift +++ b/Sources/Core/Media/Controllers/MediaPingInterval.swift @@ -16,8 +16,8 @@ import Foundation class MediaPingInterval { var pingInterval: Int - private var timer: Timer? - private var timerProvider: Timer.Type + private var timer: InternalQueueTimer? + private var startTimer: (TimeInterval, @escaping () -> Void) -> InternalQueueTimer private var paused: Bool? private var numPausedPings: Int = 0 private var maxPausedPings: Int = 1 @@ -25,12 +25,12 @@ class MediaPingInterval { init(pingInterval: Int? = nil, maxPausedPings: Int? = nil, - timerProvider: Timer.Type = Timer.self) { + startTimer: @escaping (TimeInterval, @escaping () -> Void) -> InternalQueueTimer = InternalQueue.startTimer) { if let maxPausedPings = maxPausedPings { self.maxPausedPings = maxPausedPings } self.pingInterval = pingInterval ?? 30 - self.timerProvider = timerProvider + self.startTimer = startTimer } func update(player: MediaPlayerEntity) { @@ -41,8 +41,8 @@ class MediaPingInterval { func subscribe(callback: @escaping () -> ()) { end() - timer = timerProvider.scheduledTimer(withTimeInterval: TimeInterval(pingInterval), - repeats: true) { _ in + timer = startTimer(TimeInterval(pingInterval)) { [weak self] in + guard let self = self else { return } if !self.isPaused || self.numPausedPings < self.maxPausedPings { if self.isPaused { self.numPausedPings += 1 diff --git a/Sources/Core/Media/Controllers/MediaTrackingImpl.swift b/Sources/Core/Media/Controllers/MediaTrackingImpl.swift index c71f06429..ef7f0b956 100644 --- a/Sources/Core/Media/Controllers/MediaTrackingImpl.swift +++ b/Sources/Core/Media/Controllers/MediaTrackingImpl.swift @@ -115,8 +115,8 @@ class MediaTrackingImpl: MediaTracking { player: MediaPlayerEntity? = nil, ad: MediaAdEntity? = nil, adBreak: MediaAdBreakEntity? = nil) { - objc_sync_enter(self) - + InternalQueue.onQueuePrecondition() + // update state if let player = player { self.player.update(from: player) @@ -143,8 +143,6 @@ class MediaTrackingImpl: MediaTracking { if let event = event { adTracking.updateForNextEvent(event: event) } - - objc_sync_exit(self) } private func addEntitiesAndTrack(event: Event) { diff --git a/Sources/Core/NetworkConnection/NetworkControllerImpl.swift b/Sources/Core/NetworkConnection/NetworkControllerImpl.swift index 9d026540a..c5f591d90 100644 --- a/Sources/Core/NetworkConnection/NetworkControllerImpl.swift +++ b/Sources/Core/NetworkConnection/NetworkControllerImpl.swift @@ -16,10 +16,6 @@ import Foundation class NetworkControllerImpl: Controller, NetworkController { private var requestCallback: RequestCallback? - var isCustomNetworkConnection: Bool { - return emitter.networkConnection != nil && !(emitter.networkConnection is DefaultNetworkConnection) - } - // MARK: - Properties var endpoint: String? { diff --git a/Sources/Core/Session/Session.swift b/Sources/Core/Session/Session.swift index 85a51863c..1f97d07b7 100644 --- a/Sources/Core/Session/Session.swift +++ b/Sources/Core/Session/Session.swift @@ -17,41 +17,42 @@ import UIKit #endif class Session { - /// Whether the application is in the background or foreground - private(set) var inBackground = false + + // MARK: - Private properties + + private var dataPersistence: DataPersistence? + /// The event index + private var eventIndex = 0 + private var isNewSession = true + private var isSessionCheckerEnabled = false + private var lastSessionCheck: NSNumber = Utilities.getTimestamp() + /// Returns the current session state + private var state: SessionState? + /// The current tracker associated with the session + private weak var tracker: Tracker? + + // MARK: - Properties + /// The session's userId - private(set) var userId: String + let userId: String + /// Whether the application is in the background or foreground + var inBackground: Bool = false /// The foreground index count - private(set) var foregroundIndex = 0 + var foregroundIndex = 0 /// The background index count - private(set) var backgroundIndex = 0 - /// The event index - private(set) var eventIndex = 0 - /// The current tracker associated with the session - private(set) weak var tracker: Tracker? - /// Returns the current session state - private(set) var state: SessionState? + var backgroundIndex = 0 /// Callback to be called when the session is updated - public var onSessionStateUpdate: ((_ sessionState: SessionState) -> Void)? - + var onSessionStateUpdate: ((_ sessionState: SessionState) -> Void)? /// The currently set Foreground Timeout in milliseconds - public var foregroundTimeout = TrackerDefaults.foregroundTimeout + var foregroundTimeout = TrackerDefaults.foregroundTimeout /// The currently set Background Timeout in milliseconds - public var backgroundTimeout = TrackerDefaults.backgroundTimeout - - private var isNewSession = true - private var isSessionCheckerEnabled = false - private var lastSessionCheck: NSNumber = Utilities.getTimestamp() - private var dataPersistence: DataPersistence? - - /// Initializes a newly allocated SnowplowSession - /// - Parameters: - /// - foregroundTimeout: the session timeout while it is in the foreground - /// - backgroundTimeout: the session timeout while it is in the background - /// - Returns: a SnowplowSession - convenience init(foregroundTimeout: Int, andBackgroundTimeout backgroundTimeout: Int) { - self.init(foregroundTimeout: foregroundTimeout, andBackgroundTimeout: backgroundTimeout, andTracker: nil) - } + var backgroundTimeout = TrackerDefaults.backgroundTimeout + var sessionIndex: Int? { return state?.sessionIndex } + var sessionId: String? { return state?.sessionId } + var previousSessionId: String? { return state?.previousSessionId } + var firstEventId: String? { return state?.firstEventId } + + // MARK: - Constructor and destructor /// Initializes a newly allocated SnowplowSession /// - Parameters: @@ -59,12 +60,12 @@ class Session { /// - backgroundTimeout: the session timeout while it is in the background /// - tracker: reference to the associated tracker of the session /// - Returns: a SnowplowSession - init(foregroundTimeout: Int, andBackgroundTimeout backgroundTimeout: Int, andTracker tracker: Tracker?) { + init(foregroundTimeout: Int, backgroundTimeout: Int, trackerNamespace: String? = nil, tracker: Tracker? = nil) { self.foregroundTimeout = foregroundTimeout * 1000 self.backgroundTimeout = backgroundTimeout * 1000 self.tracker = tracker - if let namespace = tracker?.trackerNamespace { + if let namespace = trackerNamespace { dataPersistence = DataPersistence.getFor(namespace: namespace) } let storedSessionDict = dataPersistence?.session @@ -96,6 +97,12 @@ class Session { #endif } + deinit { +#if os(iOS) || os(tvOS) + NotificationCenter.default.removeObserver(self) +#endif + } + // MARK: - Public /// Starts the recurring timer check for sessions @@ -122,7 +129,7 @@ class Session { /// - Returns: a SnowplowPayload containing the session dictionary func getDictWithEventId(_ eventId: String?, eventTimestamp: Int64, userAnonymisation: Bool) -> [String : Any]? { var context: [String : Any]? = nil - objc_sync_enter(self) + if isSessionCheckerEnabled { if shouldUpdate() { update(eventId: eventId, eventTimestamp: eventTimestamp) @@ -134,12 +141,11 @@ class Session { } lastSessionCheck = Utilities.getTimestamp() } - + eventIndex += 1 - + context = state?.sessionContext context?[kSPSessionEventIndex] = NSNumber(value: eventIndex) - objc_sync_exit(self) if userAnonymisation { // mask the user identifier @@ -207,40 +213,38 @@ class Session { dataPersistence?.session = sessionToPersist eventIndex = 0 } + + // MARK: - background and foreground notifications @objc func updateInBackground() { - if !inBackground && tracker?.lifecycleEvents ?? false { - backgroundIndex += 1 - sendBackgroundEvent() - inBackground = true + InternalQueue.async { + if self.tracker?.lifecycleEvents ?? false { + guard let backgroundIndex = self.incrementBackgroundIndexIfNotInBackground() else { return } + _ = self.tracker?.track(Background(index: backgroundIndex)) + self.inBackground = true + } } } @objc func updateInForeground() { - if inBackground && tracker?.lifecycleEvents ?? false { - foregroundIndex += 1 - sendForegroundEvent() - inBackground = false - } - } - - func sendBackgroundEvent() { - if let tracker = tracker { - let backgroundEvent = Background(index: backgroundIndex) - let _ = tracker.track(backgroundEvent) + InternalQueue.async { + if self.tracker?.lifecycleEvents ?? false { + guard let foregroundIndex = self.incrementForegroundIndexIfInBackground() else { return } + _ = self.tracker?.track(Foreground(index: foregroundIndex)) + self.inBackground = false + } } } - - func sendForegroundEvent() { - if let tracker = tracker { - let foregroundEvent = Foreground(index: foregroundIndex) - let _ = tracker.track(foregroundEvent) - } + + private func incrementBackgroundIndexIfNotInBackground() -> Int? { + if self.inBackground { return nil } + self.backgroundIndex += 1 + return self.backgroundIndex } - - deinit { - #if os(iOS) - NotificationCenter.default.removeObserver(self) - #endif + + private func incrementForegroundIndexIfInBackground() -> Int? { + if !self.inBackground { return nil } + self.foregroundIndex += 1 + return self.foregroundIndex } } diff --git a/Sources/Core/Session/SessionControllerImpl.swift b/Sources/Core/Session/SessionControllerImpl.swift index 0a8eade38..1cd116ab0 100644 --- a/Sources/Core/Session/SessionControllerImpl.swift +++ b/Sources/Core/Session/SessionControllerImpl.swift @@ -107,7 +107,7 @@ class SessionControllerImpl: Controller, SessionController { logDiagnostic(message: "Attempt to access SessionController fields when disabled") return -1 } - return session?.state?.sessionIndex ?? -1 + return session?.sessionIndex ?? -1 } var sessionId: String? { @@ -115,7 +115,7 @@ class SessionControllerImpl: Controller, SessionController { logDiagnostic(message: "Attempt to access SessionController fields when disabled") return nil } - return session?.state?.sessionId + return session?.sessionId } var userId: String? { diff --git a/Sources/Core/StateMachine/StateFuture.swift b/Sources/Core/StateMachine/StateFuture.swift index c6d9f73ff..81a109b20 100644 --- a/Sources/Core/StateMachine/StateFuture.swift +++ b/Sources/Core/StateMachine/StateFuture.swift @@ -31,8 +31,6 @@ class StateFuture { } func computeState() -> State? { - objc_sync_enter(self) - defer { objc_sync_exit(self) } if computedState == nil { if let stateMachine = stateMachine, let event = event { computedState = stateMachine.transition(from: event, state: previousState?.computeState()) diff --git a/Sources/Core/StateMachine/StateManager.swift b/Sources/Core/StateMachine/StateManager.swift index 4e5e910b0..b4b911d5d 100644 --- a/Sources/Core/StateMachine/StateManager.swift +++ b/Sources/Core/StateMachine/StateManager.swift @@ -23,9 +23,6 @@ class StateManager { private var trackerState = TrackerState() func addOrReplaceStateMachine(_ stateMachine: StateMachineProtocol) { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - if let previousStateMachine = identifierToStateMachine[stateMachine.identifier] { if type(of: stateMachine) == type(of: previousStateMachine) { return @@ -56,9 +53,6 @@ class StateManager { } func removeStateMachine(_ stateMachineIdentifier: String) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - guard let stateMachine = identifierToStateMachine[stateMachineIdentifier] else { return false } @@ -88,9 +82,6 @@ class StateManager { } func trackerState(forProcessedEvent event: Event) -> TrackerStateSnapshot? { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - if let sdEvent = event as? SelfDescribingAbstract { var stateMachines = Array(eventSchemaToStateMachine[sdEvent.schema] ?? []) stateMachines.append(contentsOf: eventSchemaToStateMachine["*"] ?? []) @@ -121,9 +112,6 @@ class StateManager { } func filter(event: InspectableEvent & StateMachineEvent) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - guard let schema = event.schema ?? event.eventName else { return true } var stateMachines = eventSchemaToFilter[schema] ?? [] stateMachines.append(contentsOf: eventSchemaToFilter["*"] ?? []) @@ -138,9 +126,6 @@ class StateManager { } func entities(forProcessedEvent event: InspectableEvent & StateMachineEvent) -> [SelfDescribingJson] { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - guard let schema = event.schema ?? event.eventName else { return [] } var result: [SelfDescribingJson] = [] var stateMachines = eventSchemaToEntitiesGenerator[schema] ?? [] @@ -156,9 +141,6 @@ class StateManager { } func addPayloadValues(to event: InspectableEvent & StateMachineEvent) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - guard let schema = event.schema else { return true } var failures = 0 var stateMachines = eventSchemaToPayloadUpdater[schema] ?? [] @@ -177,10 +159,8 @@ class StateManager { func afterTrack(event: InspectableEvent & StateMachineEvent) { guard let schema = event.schema ?? event.eventName else { return } - objc_sync_enter(self) var stateMachines = eventSchemaToAfterTrackCallback[schema] ?? [] stateMachines.append(contentsOf: eventSchemaToAfterTrackCallback["*"] ?? []) - objc_sync_exit(self) if !stateMachines.isEmpty { DispatchQueue.global(qos: .default).async { diff --git a/Sources/Core/StateMachine/TrackerState.swift b/Sources/Core/StateMachine/TrackerState.swift index 3157260d8..611e04d4b 100644 --- a/Sources/Core/StateMachine/TrackerState.swift +++ b/Sources/Core/StateMachine/TrackerState.swift @@ -19,29 +19,20 @@ class TrackerState: TrackerStateSnapshot { /// Set a future computable state with a specific state identifier func setStateFuture(_ state: StateFuture, identifier stateIdentifier: String) { - objc_sync_enter(self) trackerState[stateIdentifier] = state - objc_sync_exit(self) } /// Get a future computable state associated with a state identifier func stateFuture(withIdentifier stateIdentifier: String) -> StateFuture? { - objc_sync_enter(self) - defer { objc_sync_exit(self) } return trackerState[stateIdentifier] } func remove(withIdentifier stateIdentifer: String) { - objc_sync_enter(self) trackerState.removeValue(forKey: stateIdentifer) - objc_sync_exit(self) } /// Get an immutable copy of the whole tracker state func snapshot() -> TrackerStateSnapshot? { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - let newTrackerState = TrackerState() newTrackerState.trackerState = trackerState return newTrackerState diff --git a/Sources/Core/Storage/MemoryEventStore.swift b/Sources/Core/Storage/MemoryEventStore.swift index 1e0f569b7..62f5b13f0 100644 --- a/Sources/Core/Storage/MemoryEventStore.swift +++ b/Sources/Core/Storage/MemoryEventStore.swift @@ -19,7 +19,6 @@ class MemoryEventStore: NSObject, EventStore { var index: Int64 var orderedSet: NSMutableOrderedSet - convenience override init() { self.init(limit: 250) } @@ -33,22 +32,22 @@ class MemoryEventStore: NSObject, EventStore { // Interface methods func addEvent(_ payload: Payload) { - objc_sync_enter(self) + InternalQueue.onQueuePrecondition() + let item = EmitterEvent(payload: payload, storeId: index) orderedSet.add(item) - objc_sync_exit(self) index += 1 } func count() -> UInt { - objc_sync_enter(self) - defer { objc_sync_exit(self) } + InternalQueue.onQueuePrecondition() + return UInt(orderedSet.count) } func emittableEvents(withQueryLimit queryLimit: UInt) -> [EmitterEvent] { - objc_sync_enter(self) - defer { objc_sync_exit(self) } + InternalQueue.onQueuePrecondition() + let setCount = (orderedSet).count if setCount <= 0 { return [] @@ -71,19 +70,21 @@ class MemoryEventStore: NSObject, EventStore { } func removeAllEvents() -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } + InternalQueue.onQueuePrecondition() + orderedSet.removeAllObjects() return true } func removeEvent(withId storeId: Int64) -> Bool { + InternalQueue.onQueuePrecondition() + return removeEvents(withIds: [storeId]) } func removeEvents(withIds storeIds: [Int64]) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } + InternalQueue.onQueuePrecondition() + var itemsToRemove: [EmitterEvent] = [] for item in orderedSet { guard let item = item as? EmitterEvent else { diff --git a/Sources/Core/Storage/SQLiteEventStore.swift b/Sources/Core/Storage/SQLiteEventStore.swift index e36c1bda2..53afe9753 100644 --- a/Sources/Core/Storage/SQLiteEventStore.swift +++ b/Sources/Core/Storage/SQLiteEventStore.swift @@ -37,57 +37,47 @@ class SQLiteEventStore: NSObject, EventStore { // MARK: SPEventStore implementation methods func addEvent(_ payload: Payload) { - sync { - self.database.insertRow(payload.dictionary) - } + InternalQueue.onQueuePrecondition() + + self.database.insertRow(payload.dictionary) } func removeEvent(withId storeId: Int64) -> Bool { - sync { - return database.deleteRows(ids: [storeId]) - } + InternalQueue.onQueuePrecondition() + + return database.deleteRows(ids: [storeId]) } func removeEvents(withIds storeIds: [Int64]) -> Bool { - sync { - return database.deleteRows(ids: storeIds) - } + InternalQueue.onQueuePrecondition() + + return database.deleteRows(ids: storeIds) } func removeAllEvents() -> Bool { - sync { - return database.deleteRows() - } + InternalQueue.onQueuePrecondition() + + return database.deleteRows() } func count() -> UInt { - sync { - if let count = database.countRows() { - return UInt(count) - } - return 0 + InternalQueue.onQueuePrecondition() + + if let count = database.countRows() { + return UInt(count) } + return 0 } func emittableEvents(withQueryLimit queryLimit: UInt) -> [EmitterEvent] { - sync { - let limit = min(Int(queryLimit), sendLimit) - let rows = database.readRows(numRows: limit) - return rows.map { row in - let payload = Payload(dictionary: row.data) - return EmitterEvent(payload: payload, storeId: row.id) - } - } - } - - // MARK: - Dispatch queue - - private let dispatchQueue = DispatchQueue(label: "snowplow.event_store") - - private func sync(_ callback: () -> T) -> T { - dispatchPrecondition(condition: .notOnQueue(dispatchQueue)) + InternalQueue.onQueuePrecondition() - return dispatchQueue.sync(execute: callback) + let limit = min(Int(queryLimit), sendLimit) + let rows = database.readRows(numRows: limit) + return rows.map { row in + let payload = Payload(dictionary: row.data) + return EmitterEvent(payload: payload, storeId: row.id) + } } } diff --git a/Sources/Core/Subject/PlatformContext.swift b/Sources/Core/Subject/PlatformContext.swift index db59ecd39..2b8b0bbee 100644 --- a/Sources/Core/Subject/PlatformContext.swift +++ b/Sources/Core/Subject/PlatformContext.swift @@ -54,7 +54,6 @@ class PlatformContext { /// - Parameter userAnonymisation: Whether to anonymise user identifiers (IDFA values) func fetchPlatformDict(userAnonymisation: Bool, advertisingIdentifierRetriever: (() -> UUID?)?) -> Payload { #if os(iOS) - objc_sync_enter(self) let now = Date().timeIntervalSince1970 if now - lastUpdatedEphemeralMobileDict >= mobileDictUpdateFrequency { setEphemeralMobileDict() @@ -62,7 +61,6 @@ class PlatformContext { if now - lastUpdatedEphemeralNetworkDict >= networkDictUpdateFrequency { setEphemeralNetworkDict() } - objc_sync_exit(self) #endif if userAnonymisation { // mask user identifiers diff --git a/Sources/Core/Subject/Subject.swift b/Sources/Core/Subject/Subject.swift index 48dd09735..610e4ee10 100644 --- a/Sources/Core/Subject/Subject.swift +++ b/Sources/Core/Subject/Subject.swift @@ -21,14 +21,12 @@ class Subject : NSObject { private var geoDict: [String : NSObject] = [:] var platformContext = false + var platformContextProperties: [PlatformContextProperty]? { - get { - platformContextManager.platformContextProperties - } - set { - platformContextManager.platformContextProperties = newValue - } + get { return platformContextManager.platformContextProperties } + set { platformContextManager.platformContextProperties = newValue } } + var geoLocationContext = false // MARK: - Standard Dictionary @@ -38,9 +36,7 @@ class Subject : NSObject { private var _userId: String? /// The user's ID. var userId: String? { - get { - _userId - } + get { return _userId } set(uid) { _userId = uid standardDict[kSPUid] = uid @@ -49,9 +45,7 @@ class Subject : NSObject { private var _networkUserId: String? var networkUserId: String? { - get { - _networkUserId - } + get { return _networkUserId } set(nuid) { _networkUserId = nuid standardDict[kSPNetworkUid] = nuid @@ -61,9 +55,7 @@ class Subject : NSObject { private var _domainUserId: String? /// The domain UID. var domainUserId: String? { - get { - _domainUserId - } + get { return _domainUserId } set(duid) { _domainUserId = duid standardDict[kSPDomainUid] = duid @@ -73,9 +65,7 @@ class Subject : NSObject { private var _useragent: String? /// The user agent (also known as browser string). var useragent: String? { - get { - _useragent - } + get { return _useragent } set(useragent) { _useragent = useragent standardDict[kSPUseragent] = useragent @@ -85,9 +75,7 @@ class Subject : NSObject { private var _ipAddress: String? /// The user's IP address. var ipAddress: String? { - get { - _ipAddress - } + get { return _ipAddress } set(ip) { _ipAddress = ip standardDict[kSPIpAddress] = ip @@ -97,9 +85,7 @@ class Subject : NSObject { private var _timezone: String? /// The user's timezone. var timezone: String? { - get { - _timezone - } + get { return _timezone } set(timezone) { _timezone = timezone standardDict[kSPTimezone] = timezone @@ -109,9 +95,7 @@ class Subject : NSObject { private var _language: String? /// The user's language. var language: String? { - get { - _language - } + get { return _language } set(lang) { _language = lang standardDict[kSPLanguage] = lang @@ -121,9 +105,7 @@ class Subject : NSObject { private var _colorDepth: NSNumber? /// The user's color depth. var colorDepth: NSNumber? { - get { - _colorDepth - } + get { return _colorDepth } set(depth) { _colorDepth = depth let res = "\(depth?.stringValue ?? "")" @@ -133,9 +115,7 @@ class Subject : NSObject { var _screenResolution: SPSize? var screenResolution: SPSize? { - get { - _screenResolution - } + get { return _screenResolution } set { _screenResolution = newValue if let size = newValue { @@ -149,9 +129,7 @@ class Subject : NSObject { var _screenViewPort: SPSize? var screenViewPort: SPSize? { - get { - _screenViewPort - } + get { return _screenViewPort } set { _screenViewPort = newValue if let size = newValue { @@ -160,7 +138,6 @@ class Subject : NSObject { } else { standardDict.removeValue(forKey: kSPViewPort) } - } } @@ -170,81 +147,49 @@ class Subject : NSObject { /// Latitude value for the geolocation context. var geoLatitude: NSNumber? { - get { - return geoDict[kSPGeoLatitude] as? NSNumber - } - set(latitude) { - geoDict[kSPGeoLatitude] = latitude - } + get { return geoDict[kSPGeoLatitude] as? NSNumber } + set(latitude) { geoDict[kSPGeoLatitude] = latitude } } /// Longitude value for the geo context. var geoLongitude: NSNumber? { - get { - return geoDict[kSPGeoLongitude] as? NSNumber - } - set(longitude) { - geoDict[kSPGeoLongitude] = longitude - } + get { return geoDict[kSPGeoLongitude] as? NSNumber } + set(longitude) { geoDict[kSPGeoLongitude] = longitude } } /// LatitudeLongitudeAccuracy value for the geolocation context. var geoLatitudeLongitudeAccuracy: NSNumber? { - get { - return geoDict[kSPGeoLatLongAccuracy] as? NSNumber - } - set(latitudeLongitudeAccuracy) { - geoDict[kSPGeoLatLongAccuracy] = latitudeLongitudeAccuracy - } + get { return geoDict[kSPGeoLatLongAccuracy] as? NSNumber } + set { geoDict[kSPGeoLatLongAccuracy] = newValue } } /// Altitude value for the geolocation context. var geoAltitude: NSNumber? { - get { - return geoDict[kSPGeoAltitude] as? NSNumber - } - set(altitude) { - geoDict[kSPGeoAltitude] = altitude - } + get { return geoDict[kSPGeoAltitude] as? NSNumber } + set(altitude) { geoDict[kSPGeoAltitude] = altitude } } /// AltitudeAccuracy value for the geolocation context. var geoAltitudeAccuracy: NSNumber? { - get { - return geoDict[kSPGeoAltitudeAccuracy] as? NSNumber - } - set(altitudeAccuracy) { - geoDict[kSPGeoAltitudeAccuracy] = altitudeAccuracy - } + get { return geoDict[kSPGeoAltitudeAccuracy] as? NSNumber } + set(altitudeAccuracy) { geoDict[kSPGeoAltitudeAccuracy] = altitudeAccuracy } } var geoBearing: NSNumber? { - get { - return geoDict[kSPGeoBearing] as? NSNumber - } - set(bearing) { - geoDict[kSPGeoBearing] = bearing - } + get { return geoDict[kSPGeoBearing] as? NSNumber } + set(bearing) { geoDict[kSPGeoBearing] = bearing } } /// Speed value for the geolocation context. var geoSpeed: NSNumber? { - get { - return geoDict[kSPGeoSpeed] as? NSNumber - } - set(speed) { - geoDict[kSPGeoSpeed] = speed - } + get { return geoDict[kSPGeoSpeed] as? NSNumber } + set(speed) { geoDict[kSPGeoSpeed] = speed } } /// Timestamp value for the geolocation context. var geoTimestamp: NSNumber? { - get { - return geoDict[kSPGeoTimestamp] as? NSNumber - } - set(timestamp) { - geoDict[kSPGeoTimestamp] = timestamp - } + get { return geoDict[kSPGeoTimestamp] as? NSNumber } + set(timestamp) { geoDict[kSPGeoTimestamp] = timestamp } } init(platformContext: Bool = false, @@ -253,9 +198,9 @@ class Subject : NSObject { subjectConfiguration config: SubjectConfiguration? = nil) { self.platformContextManager = PlatformContext(platformContextProperties: platformContextProperties) super.init() - self.platformContextProperties = platformContextProperties + platformContextManager.platformContextProperties = platformContextProperties self.platformContext = platformContext - geoLocationContext = geoContext + self.geoLocationContext = geoContext screenResolution = Utilities.resolution screenViewPort = Utilities.viewPort @@ -294,14 +239,14 @@ class Subject : NSObject { func standardDict(userAnonymisation: Bool) -> [String : String] { if userAnonymisation { - var copy = standardDict + var copy = self.standardDict copy.removeValue(forKey: kSPUid) copy.removeValue(forKey: kSPDomainUid) copy.removeValue(forKey: kSPNetworkUid) copy.removeValue(forKey: kSPIpAddress) return copy } - return standardDict + return self.standardDict } /// Gets all platform dictionary pairs to decorate event with. Returns nil if not enabled. @@ -331,4 +276,5 @@ class Subject : NSObject { return nil } } + } diff --git a/Sources/Core/Tracker/ServiceProvider.swift b/Sources/Core/Tracker/ServiceProvider.swift index 4fd828af1..c25c88710 100644 --- a/Sources/Core/Tracker/ServiceProvider.swift +++ b/Sources/Core/Tracker/ServiceProvider.swift @@ -14,7 +14,7 @@ import Foundation class ServiceProvider: NSObject, ServiceProviderProtocol { - private(set) var namespace: String + let namespace: String var isTrackerInitialized: Bool { return _tracker != nil } @@ -233,17 +233,11 @@ class ServiceProvider: NSObject, ServiceProviderProtocol { func makeEmitter() -> Emitter { let builder = { (emitter: Emitter) in - emitter.method = self.networkConfiguration.method - emitter.protocol = self.networkConfiguration.protocol - emitter.customPostPath = self.networkConfiguration.customPostPath - emitter.requestHeaders = self.networkConfiguration.requestHeaders emitter.emitThreadPoolSize = self.emitterConfiguration.threadPoolSize emitter.byteLimitGet = self.emitterConfiguration.byteLimitGet emitter.byteLimitPost = self.emitterConfiguration.byteLimitPost - emitter.serverAnonymisation = self.emitterConfiguration.serverAnonymisation emitter.emitRange = self.emitterConfiguration.emitRange emitter.bufferOption = self.emitterConfiguration.bufferOption - emitter.eventStore = self.emitterConfiguration.eventStore emitter.callback = self.emitterConfiguration.requestCallback emitter.customRetryForStatusCodes = self.emitterConfiguration.customRetryForStatusCodes emitter.retryFailedRequests = self.emitterConfiguration.retryFailedRequests @@ -251,9 +245,24 @@ class ServiceProvider: NSObject, ServiceProviderProtocol { let emitter: Emitter if let networkConnection = networkConfiguration.networkConnection { - emitter = Emitter(networkConnection: networkConnection, builder: builder) + emitter = Emitter( + networkConnection: networkConnection, + namespace: self.namespace, + eventStore: self.emitterConfiguration.eventStore, + builder: builder + ) } else { - emitter = Emitter(urlEndpoint: networkConfiguration.endpoint ?? "", builder: builder) + emitter = Emitter( + namespace: self.namespace, + urlEndpoint: networkConfiguration.endpoint ?? "", + method: self.networkConfiguration.method, + protocol: self.networkConfiguration.protocol, + customPostPath: self.networkConfiguration.customPostPath, + requestHeaders: self.networkConfiguration.requestHeaders, + serverAnonymisation: self.emitterConfiguration.serverAnonymisation, + eventStore: self.emitterConfiguration.eventStore, + builder: builder + ) } if emitterConfiguration.isPaused { diff --git a/Sources/Core/Tracker/Tracker.swift b/Sources/Core/Tracker/Tracker.swift index 0e52fb775..d3268468a 100644 --- a/Sources/Core/Tracker/Tracker.swift +++ b/Sources/Core/Tracker/Tracker.swift @@ -41,7 +41,6 @@ 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? @@ -50,8 +49,7 @@ class Tracker: NSObject { /// Current screen view state. private(set) var currentScreenState: ScreenState? - private var trackerData: [String : String]? = nil - func setTrackerData() { + private func trackerPayloadData() -> [String : String] { var trackerVersion = kSPVersion if trackerVersionSuffix.count != 0 { var allowedCharSet = CharacterSet.alphanumerics @@ -61,25 +59,17 @@ class Tracker: NSObject { trackerVersion = "\(trackerVersion) \(suffix)" } } - trackerData = [ - kSPTrackerVersion : trackerVersion, - kSPNamespace : trackerNamespace, - kSPAppId : appId + return [ + kSPTrackerVersion: trackerVersion, + kSPNamespace: trackerNamespace, + kSPAppId: appId ] } // MARK: - Setter - private var _emitter: Emitter /// The emitter used to send events. - var emitter: Emitter { - get { - return _emitter - } - set(emitter) { - _emitter = emitter - } - } + let emitter: Emitter /// The subject used to represent the current user and persist user information. var subject: Subject? @@ -88,46 +78,13 @@ class Tracker: NSObject { var base64Encoded = TrackerDefaults.base64Encoded /// A unique identifier for an application. - private var _appId: String - var appId: String { - get { - return _appId - } - set(appId) { - _appId = appId - if builderFinished && trackerData != nil { - setTrackerData() - } - } - } + var appId: String - private(set) var _trackerNamespace: String /// The identifier for the current tracker. - var trackerNamespace: String { - get { - return _trackerNamespace - } - set(trackerNamespace) { - _trackerNamespace = trackerNamespace - if builderFinished && trackerData != nil { - setTrackerData() - } - } - } + let trackerNamespace: String /// Version suffix for tracker wrappers. - private var _trackerVersionSuffix: String = TrackerDefaults.trackerVersionSuffix - var trackerVersionSuffix: String { - get { - return _trackerVersionSuffix - } - set(trackerVersionSuffix) { - _trackerVersionSuffix = trackerVersionSuffix - if builderFinished && trackerData != nil { - setTrackerData() - } - } - } + var trackerVersionSuffix: String = TrackerDefaults.trackerVersionSuffix var devicePlatform: DevicePlatform = TrackerDefaults.devicePlatform @@ -162,8 +119,9 @@ class Tracker: NSObject { } else if builderFinished && session == nil && sessionContext { session = Session( foregroundTimeout: foregroundTimeout, - andBackgroundTimeout: backgroundTimeout, - andTracker: self) + backgroundTimeout: backgroundTimeout, + trackerNamespace: trackerNamespace, + tracker: self) } } } @@ -174,13 +132,11 @@ class Tracker: NSObject { return _deepLinkContext } set(deepLinkContext) { - serialQueue.sync { - self._deepLinkContext = deepLinkContext - if deepLinkContext { - self.addOrReplace(stateMachine: DeepLinkStateMachine()) - } else { - _ = self.stateManager.removeStateMachine(DeepLinkStateMachine.identifier) - } + self._deepLinkContext = deepLinkContext + if deepLinkContext { + self.addOrReplace(stateMachine: DeepLinkStateMachine()) + } else { + _ = self.stateManager.removeStateMachine(DeepLinkStateMachine.identifier) } } } @@ -191,13 +147,11 @@ class Tracker: NSObject { return _screenContext } set(screenContext) { - serialQueue.sync { - self._screenContext = screenContext - if screenContext { - self.addOrReplace(stateMachine: ScreenStateMachine()) - } else { - _ = self.stateManager.removeStateMachine(ScreenStateMachine.identifier) - } + self._screenContext = screenContext + if screenContext { + self.addOrReplace(stateMachine: ScreenStateMachine()) + } else { + _ = self.stateManager.removeStateMachine(ScreenStateMachine.identifier) } } } @@ -240,13 +194,11 @@ class Tracker: NSObject { return _lifecycleEvents } set(lifecycleEvents) { - serialQueue.sync { - self._lifecycleEvents = lifecycleEvents - if lifecycleEvents { - self.addOrReplace(stateMachine: LifecycleStateMachine()) - } else { - _ = self.stateManager.removeStateMachine(LifecycleStateMachine.identifier) - } + self._lifecycleEvents = lifecycleEvents + if lifecycleEvents { + self.addOrReplace(stateMachine: LifecycleStateMachine()) + } else { + _ = self.stateManager.removeStateMachine(LifecycleStateMachine.identifier) } } } @@ -287,12 +239,10 @@ 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 + self.emitter = emitter + self.appId = appId ?? "" + self.trackerNamespace = trackerNamespace super.init() builder(self) @@ -308,14 +258,12 @@ class Tracker: NSObject { } private func setup() { - emitter.namespace = trackerNamespace // Needed to correctly send events to the right EventStore - setTrackerData() - if sessionContext { session = Session( foregroundTimeout: foregroundTimeout, - andBackgroundTimeout: backgroundTimeout, - andTracker: self) + backgroundTimeout: backgroundTimeout, + trackerNamespace: trackerNamespace, + tracker: self) } UIKitScreenViewTracking.setup() @@ -346,7 +294,9 @@ class Tracker: NSObject { private func checkInstall() { if installEvent { - DispatchQueue.global(qos: .default).async { [weak self] in + InternalQueue.async { [weak self] in + guard let self = self else { return } + let installTracker = InstallTracker() let previousTimestamp = installTracker.previousInstallTimestamp installTracker.clearPreviousInstallTimestamp() @@ -357,7 +307,7 @@ class Tracker: NSObject { let installEvent = SelfDescribingJson(schema: kSPApplicationInstallSchema, andDictionary: data) let event = SelfDescribing(eventData: installEvent) event.trueTimestamp = previousTimestamp // it can be nil - let _ = self?.track(event) + let _ = self.track(event) } } } @@ -400,13 +350,15 @@ class Tracker: NSObject { let topViewControllerClassName = notification.userInfo?["topViewControllerClassName"] as? String let viewControllerClassName = notification.userInfo?["viewControllerClassName"] as? String - - if autotrackScreenViews { - let event = ScreenView(name: name, screenId: nil) - event.type = type - event.viewControllerClassName = viewControllerClassName - event.topViewControllerClassName = topViewControllerClassName - let _ = track(event) + + InternalQueue.async { + if self.autotrackScreenViews { + let event = ScreenView(name: name, screenId: nil) + event.type = type + event.viewControllerClassName = viewControllerClassName + event.topViewControllerClassName = topViewControllerClassName + let _ = self.track(event) + } } } @@ -417,9 +369,11 @@ class Tracker: NSObject { let error = userInfo?["error"] as? Error let exception = userInfo?["exception"] as? NSException - if trackerDiagnostic { - let event = TrackerError(source: tag, message: message, error: error, exception: exception) - let _ = track(event) + InternalQueue.async { + if self.trackerDiagnostic { + let event = TrackerError(source: tag, message: message, error: error, exception: exception) + let _ = self.track(event) + } } } @@ -428,10 +382,12 @@ class Tracker: NSObject { guard let message = userInfo?["message"] as? String else { return } let stacktrace = userInfo?["stacktrace"] as? String - if exceptionEvents { - let event = SNOWError(message: message) - event.stackTrace = stacktrace - let _ = track(event) + InternalQueue.async { + if self.exceptionEvents { + let event = SNOWError(message: message) + event.stackTrace = stacktrace + let _ = self.track(event) + } } } @@ -439,13 +395,11 @@ class Tracker: NSObject { /// Tracks an event despite its specific type. /// - Parameter event: The event to track - /// - Returns: The event ID or nil in case tracking is paused - func track(_ event: Event) -> UUID? { - if !dataCollection { - return nil - } - let eventId = UUID() - serialQueue.async { + /// - Returns: The event ID + func track(_ event: Event, eventId: UUID = UUID()) -> UUID { + InternalQueue.onQueuePrecondition() + + if dataCollection { event.beginProcessing(withTracker: self) self.processEvent(event, eventId) event.endProcessing(withTracker: self) @@ -515,9 +469,7 @@ class Tracker: NSObject { payload.addValueToPayload(String(format: "%lld", ttInMilliSeconds), forKey: kSPTrueTimestamp) } // Tracker info (version, namespace, app ID) - if let trackerData = trackerData { - payload.addDictionaryToPayload(trackerData) - } + payload.addDictionaryToPayload(trackerPayloadData()) // Subject if let subjectDict = subject?.standardDict(userAnonymisation: userAnonymisation) { payload.addDictionaryToPayload(subjectDict) diff --git a/Sources/Core/Tracker/TrackerControllerImpl.swift b/Sources/Core/Tracker/TrackerControllerImpl.swift index afc0a5f90..62b56618f 100644 --- a/Sources/Core/Tracker/TrackerControllerImpl.swift +++ b/Sources/Core/Tracker/TrackerControllerImpl.swift @@ -69,8 +69,12 @@ class TrackerControllerImpl: Controller, TrackerController { dirtyConfig.isPaused = false tracker.resumeEventTracking() } + + func track(_ event: Event, eventId: UUID) { + _ = tracker.track(event, eventId: eventId) + } - func track(_ event: Event) -> UUID? { + func track(_ event: Event) -> UUID { return tracker.track(event) } diff --git a/Sources/Core/Utils/DataPersistence.swift b/Sources/Core/Utils/DataPersistence.swift index 9d2d3b887..c59851944 100644 --- a/Sources/Core/Utils/DataPersistence.swift +++ b/Sources/Core/Utils/DataPersistence.swift @@ -24,8 +24,6 @@ var sessionKey = "session" class DataPersistence { var data: [String : [String : Any]] { get { - objc_sync_enter(self) - defer { objc_sync_exit(self) } if !isStoredOnFile { return ((UserDefaults.standard.dictionary(forKey: userDefaultsKey) ?? [:]) as? [String : [String : Any]]) ?? [:] } @@ -51,13 +49,11 @@ class DataPersistence { return result ?? [:] } set(data) { - objc_sync_enter(self) if let fileUrl = fileUrl { let _ = storeDictionary(data, fileURL: fileUrl) } else { UserDefaults.standard.set(data, forKey: userDefaultsKey) } - objc_sync_exit(self) } } @@ -66,11 +62,9 @@ class DataPersistence { return (data)[sessionKey] } set(session) { - objc_sync_enter(self) var data = self.data data[sessionKey] = session self.data = data - objc_sync_exit(self) } } @@ -100,8 +94,6 @@ class DataPersistence { if escapedNamespace.count <= 0 { return nil } - objc_sync_enter(DataPersistence.self) - defer { objc_sync_exit(DataPersistence.self) } if let instances = instances { if let instance = instances[escapedNamespace] { @@ -118,9 +110,7 @@ class DataPersistence { class func remove(withNamespace namespace: String) -> Bool { if let instance = DataPersistence.getFor(namespace: namespace) { - objc_sync_enter(DataPersistence.self) instances?.removeValue(forKey: instance.escapedNamespace) - objc_sync_exit(DataPersistence.self) let _ = instance.removeAll() } return true @@ -139,9 +129,6 @@ class DataPersistence { func removeAll() -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - UserDefaults.standard.removeObject(forKey: userDefaultsKey) if let fileUrl = fileUrl { diff --git a/Sources/Snowplow/Controllers/TrackerController.swift b/Sources/Snowplow/Controllers/TrackerController.swift index 3c5fcdf4c..9be50d4c5 100644 --- a/Sources/Snowplow/Controllers/TrackerController.swift +++ b/Sources/Snowplow/Controllers/TrackerController.swift @@ -62,9 +62,9 @@ public protocol TrackerController: TrackerConfigurationProtocol { /// Track the event. /// The tracker will take care to process and send the event assigning `event_id` and `device_timestamp`. /// - Parameter event: The event to track. - /// - Returns: The event ID or nil in case tracking is paused + /// - Returns: The event ID @objc - func track(_ event: Event) -> UUID? + func track(_ event: Event) -> UUID /// Pause the tracker. /// The tracker will stop any new activity tracking but it will continue to send remaining events /// already tracked but not sent yet. diff --git a/Sources/Snowplow/Network/DefaultNetworkConnection.swift b/Sources/Snowplow/Network/DefaultNetworkConnection.swift index 900604d9d..143339764 100644 --- a/Sources/Snowplow/Network/DefaultNetworkConnection.swift +++ b/Sources/Snowplow/Network/DefaultNetworkConnection.swift @@ -19,52 +19,34 @@ public class DefaultNetworkConnection: NSObject, NetworkConnection { // The protocol for connection to the collector @objc public var `protocol`: ProtocolOptions { - get { - return _protocol - } - set { - _protocol = newValue - if builderFinished { setup() } - } + get { return _protocol } + set { _protocol = newValue; setup() } } private var _urlString: String /// The collector endpoint. @objc public var urlString: String { - get { - return urlEndpoint?.absoluteString ?? _urlString - } - set { - _urlString = newValue - if builderFinished { setup() } - } + get { return urlEndpoint?.absoluteString ?? _urlString } + set { _urlString = newValue; setup() } } - @objc - private(set) public var urlEndpoint: URL? + + private var _urlEndpoint: URL? + public var urlEndpoint: URL? { return _urlEndpoint } private var _httpMethod: HttpMethodOptions = .post /// HTTP method, should be .get or .post. @objc public var httpMethod: HttpMethodOptions { - get { - return _httpMethod - } - set(method) { - _httpMethod = method - if builderFinished && urlEndpoint != nil { - setup() - } - } + get { return _httpMethod } + set(method) { _httpMethod = method; setup() } } private var _emitThreadPoolSize = 15 /// The number of threads used by the emitter. @objc public var emitThreadPoolSize: Int { - get { - return _emitThreadPoolSize - } + get { return _emitThreadPoolSize } set(emitThreadPoolSize) { self._emitThreadPoolSize = emitThreadPoolSize if dataOperationQueue.maxConcurrentOperationCount != emitThreadPoolSize { @@ -72,22 +54,30 @@ public class DefaultNetworkConnection: NSObject, NetworkConnection { } } } + /// Maximum event size for a GET request. + @objc public var byteLimitGet: Int = 40000 + /// Maximum event size for a POST request. @objc public var byteLimitPost = 40000 + + private var _customPostPath: String? /// A custom path that is used on the endpoint to send requests. - @objc - public var customPostPath: String? + @objc public var customPostPath: String? { + get { return _customPostPath } + set { _customPostPath = newValue; setup() } + } + /// Custom headers (key, value) for http requests. @objc public var requestHeaders: [String : String]? + /// Whether to anonymise server-side user identifiers including the `network_userid` and `user_ipaddress` @objc public var serverAnonymisation = false private var dataOperationQueue = OperationQueue() - private var builderFinished = false @objc public init(urlString: String, @@ -96,52 +86,13 @@ public class DefaultNetworkConnection: NSObject, NetworkConnection { customPostPath: String? = nil) { self._urlString = urlString super.init() - self.httpMethod = httpMethod - self.protocol = `protocol` - self.customPostPath = customPostPath + self._httpMethod = httpMethod + self._protocol = `protocol` + self._customPostPath = customPostPath setup() } // MARK: - Implement SPNetworkConnection protocol - - private func setup() { - // Decode url to extract protocol - let url = URL(string: _urlString) - var endpoint = _urlString - if url?.scheme == "https" { - `protocol` = .https - } else if url?.scheme == "http" { - `protocol` = .http - } else { - `protocol` = .https - endpoint = "https://\(_urlString)" - } - - // Configure - let urlPrefix = `protocol` == .http ? "http://" : "https://" - var urlSuffix = _httpMethod == .get ? kSPEndpointGet : kSPEndpointPost - if _httpMethod == .post { - if let customPostPath = customPostPath { urlSuffix = customPostPath } - } - - // Remove trailing slashes from endpoint to avoid double slashes when appending path - endpoint = endpoint.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - - urlEndpoint = URL(string: endpoint)?.appendingPathComponent(urlSuffix) - - // Log - if urlEndpoint?.scheme != nil && urlEndpoint?.host != nil { - logDebug(message: "Emitter URL created successfully '\(urlEndpoint?.absoluteString ?? "-")'") - } else { - logDebug(message: "Invalid emitter URL: '\(urlEndpoint?.absoluteString ?? "-")'") - } - let userDefaults = UserDefaults.standard - userDefaults.set(endpoint, forKey: kSPErrorTrackerUrl) - userDefaults.set(urlSuffix, forKey: kSPErrorTrackerProtocol) - userDefaults.set(urlPrefix, forKey: kSPErrorTrackerMethod) - - builderFinished = true - } @objc public func sendRequests(_ requests: [Request]) -> [RequestResult] { @@ -185,8 +136,45 @@ public class DefaultNetworkConnection: NSObject, NetworkConnection { } // MARK: - Private methods + + private func setup() { + // Decode url to extract protocol + let url = URL(string: _urlString) + var endpoint = _urlString + if url?.scheme == "https" { + _protocol = .https + } else if url?.scheme == "http" { + _protocol = .http + } else { + _protocol = .https + endpoint = "https://\(_urlString)" + } - func buildPost(_ request: Request) -> URLRequest { + // Configure + let urlPrefix = _protocol == .http ? "http://" : "https://" + var urlSuffix = _httpMethod == .get ? kSPEndpointGet : kSPEndpointPost + if _httpMethod == .post { + if let customPostPath = _customPostPath { urlSuffix = customPostPath } + } + + // Remove trailing slashes from endpoint to avoid double slashes when appending path + endpoint = endpoint.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + _urlEndpoint = URL(string: endpoint)?.appendingPathComponent(urlSuffix) + + // Log + if _urlEndpoint?.scheme != nil && _urlEndpoint?.host != nil { + logDebug(message: "Emitter URL created successfully '\(_urlEndpoint?.absoluteString ?? "-")'") + } else { + logDebug(message: "Invalid emitter URL: '\(_urlEndpoint?.absoluteString ?? "-")'") + } + let userDefaults = UserDefaults.standard + userDefaults.set(endpoint, forKey: kSPErrorTrackerUrl) + userDefaults.set(urlSuffix, forKey: kSPErrorTrackerProtocol) + userDefaults.set(urlPrefix, forKey: kSPErrorTrackerMethod) + } + + private func buildPost(_ request: Request) -> URLRequest { var requestData: Data? = nil do { requestData = try JSONSerialization.data(withJSONObject: request.payload?.dictionary ?? [:], options: []) @@ -208,7 +196,7 @@ public class DefaultNetworkConnection: NSObject, NetworkConnection { return urlRequest } - func buildGet(_ request: Request) -> URLRequest { + private func buildGet(_ request: Request) -> URLRequest { let payload = request.payload?.dictionary ?? [:] let url = "\(urlEndpoint!.absoluteString)?\(Utilities.urlEncode(payload))" let anUrl = URL(string: url)! @@ -224,11 +212,12 @@ public class DefaultNetworkConnection: NSObject, NetworkConnection { return urlRequest } - func applyValuesAndHeaderFields(_ requestHeaders: [String : String], to request: inout URLRequest) { + private func applyValuesAndHeaderFields(_ requestHeaders: [String : String], to request: inout URLRequest) { (requestHeaders as NSDictionary).enumerateKeysAndObjects({ key, obj, stop in if let key = key as? String, let obj = obj as? String { request.setValue(obj, forHTTPHeaderField: key) } }) } + } diff --git a/Sources/Snowplow/Payload/Payload.swift b/Sources/Snowplow/Payload/Payload.swift index f6aa86e49..8485f206d 100644 --- a/Sources/Snowplow/Payload/Payload.swift +++ b/Sources/Snowplow/Payload/Payload.swift @@ -22,8 +22,6 @@ public class Payload: NSObject { /// Returns the payload of that particular SPPayload object. /// - Returns: NSDictionary of data in the object. public var dictionary: [String : Any] { - objc_sync_enter(self) - defer { objc_sync_exit(self) } return payload } @@ -31,8 +29,6 @@ public class Payload: NSObject { /// - Returns: A long representing the byte size of the payload. @objc public var byteSize: Int { - objc_sync_enter(self) - defer { objc_sync_exit(self) } if let data = try? JSONSerialization.data(withJSONObject: payload) { return data.count } @@ -66,7 +62,6 @@ public class Payload: NSObject { /// - key: A key of type NSString @objc public func addValueToPayload(_ value: Any?, forKey key: String) { - objc_sync_enter(self) if value == nil { if payload[key] != nil { payload.removeValue(forKey: key) @@ -74,7 +69,6 @@ public class Payload: NSObject { } else { payload[key] = value } - objc_sync_exit(self) } /// Adds a dictionary of attributes to be appended into the SPPayload instance. It does NOT overwrite the existing data in the object. diff --git a/Sources/Snowplow/Snowplow.swift b/Sources/Snowplow/Snowplow.swift index b20b0801f..b123cb3de 100644 --- a/Sources/Snowplow/Snowplow.swift +++ b/Sources/Snowplow/Snowplow.swift @@ -199,13 +199,15 @@ public class Snowplow: NSObject { /// - Returns: The tracker instance created. @objc public class func createTracker(namespace: String, network networkConfiguration: NetworkConfiguration, configurations: [ConfigurationProtocol] = []) -> TrackerController { - if let serviceProvider = serviceProviderInstances[namespace] { - serviceProvider.reset(configurations: configurations + [networkConfiguration]) - return serviceProvider.trackerController - } else { - let serviceProvider = ServiceProvider(namespace: namespace, network: networkConfiguration, configurations: configurations) - let _ = registerInstance(serviceProvider) - return serviceProvider.trackerController + InternalQueue.sync { + if let serviceProvider = serviceProviderInstances[namespace] { + serviceProvider.reset(configurations: configurations + [networkConfiguration]) + return TrackerControllerIQWrapper(controller: serviceProvider.trackerController) + } else { + let serviceProvider = ServiceProvider(namespace: namespace, network: networkConfiguration, configurations: configurations) + let _ = registerInstance(serviceProvider) + return TrackerControllerIQWrapper(controller: serviceProvider.trackerController) + } } } @@ -248,7 +250,12 @@ public class Snowplow: NSObject { /// calling `setTrackerAsDefault(TrackerController)`. @objc public class func defaultTracker() -> TrackerController? { - return defaultServiceProvider?.trackerController + InternalQueue.sync { + if let controller = defaultServiceProvider?.trackerController { + return TrackerControllerIQWrapper(controller: controller) + } + return nil + } } /// Using the namespace identifier is possible to get the trackerController if already instanced. @@ -257,7 +264,12 @@ public class Snowplow: NSObject { /// - Returns: The tracker if it exist with that namespace. @objc public class func tracker(namespace: String) -> TrackerController? { - return serviceProviderInstances[namespace]?.trackerController + InternalQueue.sync { + if let controller = serviceProviderInstances[namespace]?.trackerController { + return TrackerControllerIQWrapper(controller: controller) + } + return nil + } } /// Set the passed tracker as default tracker if it's registered as an active tracker in the app. @@ -269,13 +281,14 @@ public class Snowplow: NSObject { /// - Returns: Whether the tracker passed is registered among the active trackers of the app. @objc public class func setAsDefault(tracker trackerController: TrackerController?) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - if let namespace = trackerController?.namespace, - let serviceProvider = serviceProviderInstances[namespace] { - defaultServiceProvider = serviceProvider - return true + if let namespace = trackerController?.namespace { + return InternalQueue.sync { + if let serviceProvider = serviceProviderInstances[namespace] { + defaultServiceProvider = serviceProvider + return true + } + return false + } } return false } @@ -291,20 +304,12 @@ public class Snowplow: NSObject { /// - Returns: Whether it has been able to remove it. @objc public class func remove(tracker trackerController: TrackerController?) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } - if let namespace = trackerController?.namespace, - let serviceProvider = (serviceProviderInstances)[namespace] { - serviceProvider.shutdown() - serviceProviderInstances.removeValue(forKey: namespace) - if serviceProvider == defaultServiceProvider { - defaultServiceProvider = nil - } - return true + if let namespace = trackerController?.namespace { + return remove(namespace: namespace) } return false } - + /// Remove all the trackers. /// /// The removed tracker is always stopped. @@ -312,20 +317,22 @@ public class Snowplow: NSObject { /// See ``remove(tracker:)`` to remove a specific tracker. @objc public class func removeAllTrackers() { - objc_sync_enter(self) - defaultServiceProvider = nil - let serviceProviders = serviceProviderInstances.values - serviceProviderInstances.removeAll() - for sp in serviceProviders { - sp.shutdown() + InternalQueue.sync { + defaultServiceProvider = nil + let serviceProviders = serviceProviderInstances.values + serviceProviderInstances.removeAll() + for sp in serviceProviders { + sp.shutdown() + } } - objc_sync_exit(self) } /// - Returns: Set of namespace of the active trackers in the app. @objc class public var instancedTrackerNamespaces: [String] { - return Array(serviceProviderInstances.keys) + InternalQueue.sync { + return Array(serviceProviderInstances.keys) + } } #if os(iOS) || os(macOS) @@ -345,8 +352,6 @@ public class Snowplow: NSObject { // MARK: - Private methods private class func registerInstance(_ serviceProvider: ServiceProvider) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } let namespace = serviceProvider.namespace let isOverriding = serviceProviderInstances[namespace] != nil serviceProviderInstances[namespace] = serviceProvider @@ -359,18 +364,29 @@ public class Snowplow: NSObject { private class func createTrackers(configurationBundles bundles: [ConfigurationBundle]) -> [String] { var namespaces: [String]? = [] for bundle in bundles { - objc_sync_enter(self) if let networkConfiguration = bundle.networkConfiguration { _ = createTracker(namespace: bundle.namespace, network: networkConfiguration, configurations: bundle.configurations) namespaces?.append(bundle.namespace) } else { // remove tracker if it exists - if let tracker = tracker(namespace: bundle.namespace) { - let _ = remove(tracker: tracker) - } + _ = remove(namespace: bundle.namespace) } - objc_sync_exit(self) } return namespaces ?? [] } + + private class func remove(namespace: String) -> Bool { + InternalQueue.sync { + if let serviceProvider = (serviceProviderInstances)[namespace] { + serviceProvider.shutdown() + serviceProviderInstances.removeValue(forKey: namespace) + if serviceProvider == defaultServiceProvider { + defaultServiceProvider = nil + } + return true + } + return false + } + } + } diff --git a/Tests/Configurations/TestMultipleInstances.swift b/Tests/Configurations/TestMultipleInstances.swift index 5a7a4e400..6a6bbeddd 100644 --- a/Tests/Configurations/TestMultipleInstances.swift +++ b/Tests/Configurations/TestMultipleInstances.swift @@ -29,7 +29,7 @@ class TestMultipleInstances: XCTestCase { let t2 = Snowplow.createTracker(namespace: "t1", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake2")) XCTAssertEqual(t2.network?.endpoint, "https://snowplowanalytics.fake2/com.snowplowanalytics.snowplow/tp2") XCTAssertEqual(["t1"], Snowplow.instancedTrackerNamespaces) - XCTAssertTrue(t1 === t2) + XCTAssertTrue(t1.network?.endpoint == t2.network?.endpoint) } func testMultipleInstances() { diff --git a/Tests/Configurations/TestTrackerConfiguration.swift b/Tests/Configurations/TestTrackerConfiguration.swift index d84dbfa84..bbbc59d01 100644 --- a/Tests/Configurations/TestTrackerConfiguration.swift +++ b/Tests/Configurations/TestTrackerConfiguration.swift @@ -389,6 +389,6 @@ class TestTrackerConfiguration: XCTestCase { // Check eid field let trackedEventId = payload?["eid"] as? String - XCTAssertTrue((eventId?.uuidString == trackedEventId)) + XCTAssertTrue((eventId.uuidString == trackedEventId)) } } diff --git a/Tests/Ecommerce/TestEcommerceController.swift b/Tests/Ecommerce/TestEcommerceController.swift index b8ff6c742..4bb6c273c 100644 --- a/Tests/Ecommerce/TestEcommerceController.swift +++ b/Tests/Ecommerce/TestEcommerceController.swift @@ -16,7 +16,8 @@ import XCTest class TestEcommerceController: XCTestCase { - var trackedEvents: [InspectableEvent] = [] + var eventSink: EventSink? + var trackedEvents: [InspectableEvent] { return eventSink?.trackedEvents ?? [] } var tracker: TrackerController? override func setUp() { @@ -25,7 +26,7 @@ class TestEcommerceController: XCTestCase { override func tearDown() { Snowplow.removeAllTrackers() - trackedEvents.removeAll() + eventSink = nil } func testAddScreenEntity() { @@ -103,16 +104,10 @@ class TestEcommerceController: XCTestCase { trackerConfig.lifecycleAutotracking = false let namespace = "testEcommerce" + String(describing: Int.random(in: 0..<100)) - let plugin = PluginConfiguration(identifier: "testPlugin" + namespace) - .afterTrack { event in - if namespace == self.tracker?.namespace { - self.trackedEvents.append(event) - } - } - + eventSink = EventSink() return Snowplow.createTracker(namespace: namespace, network: networkConfig, - configurations: [trackerConfig, plugin]) + configurations: [trackerConfig, eventSink!]) } private func waitForEventsToBeTracked() { diff --git a/Tests/Ecommerce/TestEcommerceEvents.swift b/Tests/Ecommerce/TestEcommerceEvents.swift index 8f03da368..d75628a3c 100644 --- a/Tests/Ecommerce/TestEcommerceEvents.swift +++ b/Tests/Ecommerce/TestEcommerceEvents.swift @@ -16,7 +16,8 @@ import XCTest class TestEcommerceEvents: XCTestCase { - var trackedEvents: [InspectableEvent] = [] + var eventSink: EventSink? + var trackedEvents: [InspectableEvent] { return eventSink?.trackedEvents ?? [] } var tracker: TrackerController? override func setUp() { @@ -25,7 +26,7 @@ class TestEcommerceEvents: XCTestCase { override func tearDown() { Snowplow.removeAllTrackers() - trackedEvents.removeAll() + eventSink = nil } func testAddToCart() { @@ -309,16 +310,11 @@ class TestEcommerceEvents: XCTestCase { trackerConfig.lifecycleAutotracking = false let namespace = "testEcommerce" + String(describing: Int.random(in: 0..<100)) - let plugin = PluginConfiguration(identifier: "testPlugin" + namespace) - .afterTrack { event in - if namespace == self.tracker?.namespace { - self.trackedEvents.append(event) - } - } + eventSink = EventSink() return Snowplow.createTracker(namespace: namespace, network: networkConfig, - configurations: [trackerConfig, plugin]) + configurations: [trackerConfig, eventSink!]) } private func waitForEventsToBeTracked() { diff --git a/Tests/Global Contexts/TestGlobalContexts.swift b/Tests/Global Contexts/TestGlobalContexts.swift index f50a124c2..ac058fcb9 100644 --- a/Tests/Global Contexts/TestGlobalContexts.swift +++ b/Tests/Global Contexts/TestGlobalContexts.swift @@ -48,13 +48,14 @@ class TestGlobalContexts: XCTestCase { ] }) - var generators = [ + let generators = [ "static": staticGC, "generator": generatorGC, "block": blockGC ] - let serviceProvider = getServiceProviderWithGlobalContextGenerators(&generators) - let controller = serviceProvider.globalContextsController + let tracker = createTracker(generators: generators) { _ in + } + guard let controller = tracker?.globalContexts else { XCTFail(); return } var result = Set(controller.tags) var expected = Set(["static", "generator", "block"]) @@ -93,9 +94,9 @@ class TestGlobalContexts: XCTestCase { "key": "value" ]) ]) - var generators: [String : GlobalContext] = [:] - let serviceProvider = getServiceProviderWithGlobalContextGenerators(&generators) - let controller = serviceProvider.globalContextsController + let tracker = createTracker(generators: [:]) { _ in + } + guard let controller = tracker?.globalContexts else { XCTFail(); return } var result = Set(controller.tags) var expected = Set([]) @@ -125,17 +126,20 @@ class TestGlobalContexts: XCTestCase { "key": "value" ]) ]) - var globalContexts = [ + let globalContexts = [ "static": staticGC ] - let serviceProvider = getServiceProviderWithGlobalContextGenerators(&globalContexts) + let expectation = expectation(description: "Received event") + let tracker = createTracker(generators: globalContexts) { event in + XCTAssertTrue(event.entities.count == 1) + XCTAssertEqual(event.entities[0].schema, "schema") + expectation.fulfill() + } let event = Structured(category: "Category", action: "Action") - let trackerEvent = TrackerEvent(event: event, state: nil) + _ = tracker?.track(event) - serviceProvider.tracker.addStateMachineEntities(event: trackerEvent) - XCTAssertTrue(trackerEvent.entities.count == 1) - XCTAssertEqual(trackerEvent.entities[0].schema, "schema") + wait(for: [expectation], timeout: 1) } func testStaticGeneratortWithFilter() { @@ -156,18 +160,21 @@ class TestGlobalContexts: XCTestCase { ], filter: { event in return false }) - var globalContexts = [ + let globalContexts = [ "matching": filterMatchingGC, "notMatching": filterNotMatchingGC ] - let serviceProvider = getServiceProviderWithGlobalContextGenerators(&globalContexts) + let expectation = expectation(description: "Received event") + let tracker = createTracker(generators: globalContexts) { event in + XCTAssertTrue(event.entities.count == 1) + XCTAssertEqual(event.entities[0].schema, "schema") + expectation.fulfill() + } let event = Structured(category: stringToMatch, action: "Action") - let trackerEvent = TrackerEvent(event: event, state: nil) + _ = tracker?.track(event) - serviceProvider.tracker.addStateMachineEntities(event: trackerEvent) - XCTAssertTrue(trackerEvent.entities.count == 1) - XCTAssertEqual(trackerEvent.entities[0].schema, "schema") + wait(for: [expectation], timeout: 1) } func testStaticGeneratorWithRuleset() { @@ -180,35 +187,42 @@ class TestGlobalContexts: XCTestCase { "key": "value" ]) ], ruleset: ruleset) - var globalContexts = [ + let globalContexts = [ "ruleset": rulesetGC ] - let serviceProvider = getServiceProviderWithGlobalContextGenerators(&globalContexts) + let expectation = expectation(description: "Received events") + var receivedEvents: [InspectableEvent] = [] + let tracker = createTracker(generators: globalContexts) { event in + receivedEvents.append(event) + if receivedEvents.count == 3 { + expectation.fulfill() + } + } // Not matching primitive event let primitiveEvent = Structured(category: "Category", action: "Action") - var trackerEvent = TrackerEvent(event: primitiveEvent, state: nil) - serviceProvider.tracker.addStateMachineEntities(event: trackerEvent) - XCTAssertTrue(trackerEvent.entities.count == 0) + _ = tracker?.track(primitiveEvent) // Not matching self-describing event with mobile schema let screenView = ScreenView(name: "Name", screenId: nil) screenView.type = "Type" - trackerEvent = TrackerEvent(event: screenView, state: nil) - serviceProvider.tracker.addStateMachineEntities(event: trackerEvent) - XCTAssertTrue(trackerEvent.entities.count == 0) + _ = tracker?.track(screenView) // Matching self-describing event with general schema let timing = Timing(category: "Category", variable: "Variable", timing: 123) timing.label = "Label" - trackerEvent = TrackerEvent(event: timing, state: nil) - serviceProvider.tracker.addStateMachineEntities(event: trackerEvent) - XCTAssertTrue(trackerEvent.entities.count == 1) - XCTAssertEqual(trackerEvent.entities[0].schema, "schema") + _ = tracker?.track(timing) + + wait(for: [expectation], timeout: 1) + + XCTAssertTrue(receivedEvents[0].entities.count == 0) + XCTAssertTrue(receivedEvents[1].entities.count == 0) + XCTAssertTrue(receivedEvents[2].entities.count == 1) + XCTAssertEqual(receivedEvents[2].entities[0].schema, "schema") } func testBlockGenerator() { - var generators = [ + let generators = [ "generator": GlobalContext(generator: { event in return [ SelfDescribingJson(schema: "schema", andDictionary: [ @@ -217,50 +231,65 @@ class TestGlobalContexts: XCTestCase { ] }) ] - let serviceProvider = getServiceProviderWithGlobalContextGenerators(&generators) + let expectation = expectation(description: "Received event") + let tracker = createTracker(generators: generators) { event in + XCTAssertTrue(event.entities.count == 1) + XCTAssertEqual(event.entities[0].schema, "schema") + expectation.fulfill() + } let event = Structured(category: "Category", action: "Action") - let trackerEvent = TrackerEvent(event: event, state: nil) + _ = tracker?.track(event) - serviceProvider.tracker.addStateMachineEntities(event: trackerEvent) - XCTAssertTrue(trackerEvent.entities.count == 1) - XCTAssertEqual(trackerEvent.entities[0].schema, "schema") + wait(for: [expectation], timeout: 1) } func testContextGenerator() { let contextGeneratorGC = GlobalContext(contextGenerator: GlobalContextGenerator()) - var generators = [ + let generators = [ "contextGenerator": contextGeneratorGC ] - let serviceProvider = getServiceProviderWithGlobalContextGenerators(&generators) + let expectation = expectation(description: "Received event") + let tracker = createTracker(generators: generators) { event in + XCTAssertTrue(event.entities.count == 1) + XCTAssertEqual(event.entities[0].schema, "schema") + expectation.fulfill() + } let event = Structured(category: "StringToMatch", action: "Action") - let trackerEvent = TrackerEvent(event: event, state: nil) + _ = tracker?.track(event) - serviceProvider.tracker.addStateMachineEntities(event: trackerEvent) - XCTAssertTrue(trackerEvent.entities.count == 1) - XCTAssertEqual(trackerEvent.entities[0].schema, "schema") + wait(for: [expectation], timeout: 1) } // MARK: - Utility function - - func getServiceProviderWithGlobalContextGenerators(_ generators: inout [String : GlobalContext]) -> ServiceProvider { - let networkConfig = NetworkConfiguration( - endpoint: "https://com.acme.fake", - method: .post) + private func createTracker(generators: [String : GlobalContext], afterTrack: @escaping (InspectableEvent) -> ()) -> TrackerController? { let trackerConfig = TrackerConfiguration() trackerConfig.appId = "anAppId" - trackerConfig.platformContext = true + trackerConfig.platformContext = false trackerConfig.geoLocationContext = false trackerConfig.base64Encoding = false - trackerConfig.sessionContext = true + trackerConfig.sessionContext = false + trackerConfig.installAutotracking = false + trackerConfig.lifecycleAutotracking = false + trackerConfig.applicationContext = false + trackerConfig.screenContext = false + let gcConfig = GlobalContextsConfiguration() gcConfig.contextGenerators = generators - let serviceProvider = ServiceProvider( - namespace: "aNamespace", + + let networkConfig = NetworkConfiguration(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 200)) + + let namespace = "testGlobalContexts" + UUID().uuidString + + return Snowplow.createTracker( + namespace: namespace, network: networkConfig, - configurations: [gcConfig]) - return serviceProvider + configurations: [ + EventSink(callback: afterTrack), + trackerConfig, + gcConfig + ]) } } diff --git a/Tests/Legacy Tests/LegacyTestEmitter.swift b/Tests/Legacy Tests/LegacyTestEmitter.swift index 255372efc..1477bce80 100644 --- a/Tests/Legacy Tests/LegacyTestEmitter.swift +++ b/Tests/Legacy Tests/LegacyTestEmitter.swift @@ -49,13 +49,16 @@ class LegacyTestEmitter: XCTestCase { func testEmitterBuilderAndOptions() { let `protocol` = "https" - let emitter = Emitter(urlEndpoint: TEST_SERVER_EMITTER) { emitter in - emitter.method = .post - emitter.protocol = .https - emitter.emitThreadPoolSize = 30 + let emitter = Emitter( + namespace: "ns1", + urlEndpoint: TEST_SERVER_EMITTER, + method: .post, + protocol: .https + ) { emitter in emitter.byteLimitGet = 30000 emitter.byteLimitPost = 35000 emitter.emitRange = 500 + emitter.emitThreadPoolSize = 30 } var url = "\(`protocol`)://\(TEST_SERVER_EMITTER)/com.snowplowanalytics.snowplow/tp2" @@ -71,11 +74,13 @@ class LegacyTestEmitter: XCTestCase { XCTAssertEqual(emitter.byteLimitPost, 35000) XCTAssertEqual(emitter.protocol, .https) - let customPathEmitter = Emitter(urlEndpoint: TEST_SERVER_EMITTER) { emitter in - emitter.method = .post - emitter.protocol = .https - emitter.customPostPath = "/com.acme.company/tpx" - emitter.emitThreadPoolSize = 30 + let customPathEmitter = Emitter( + namespace: "ns2", + urlEndpoint: TEST_SERVER_EMITTER, + method: .post, + protocol: .https, + customPostPath: "/com.acme.company/tpx" + ) { emitter in emitter.byteLimitGet = 30000 emitter.byteLimitPost = 35000 emitter.emitRange = 500 @@ -104,13 +109,13 @@ class LegacyTestEmitter: XCTestCase { // Test extra functions XCTAssertFalse(emitter.isSending) - XCTAssertTrue(emitter.dbCount >= 0) + XCTAssertTrue(dbCount(emitter) >= 0) // Allow timer to be set RunLoop.main.run(until: Date(timeIntervalSinceNow: 1)) emitter.resumeTimer() - emitter.flush() + flush(emitter) } // MARK: - Emitting tests @@ -130,7 +135,7 @@ class LegacyTestEmitter: XCTestCase { func testEmitSingleGetEventWithSuccess() { let networkConnection = MockNetworkConnection(requestOption: .get, statusCode: 200) let emitter = self.emitter(with: networkConnection, bufferOption: .single) - emitter.addPayload(toBuffer: generatePayloads(1).first!) + addPayload(generatePayloads(1).first!, emitter) for _ in 0..<10 { Thread.sleep(forTimeInterval: 1) @@ -139,15 +144,15 @@ class LegacyTestEmitter: XCTestCase { XCTAssertEqual(1, networkConnection.previousResults.count) XCTAssertEqual(1, networkConnection.previousResults.first!.count) XCTAssertTrue(networkConnection.previousResults.first!.first!.isSuccessful) - XCTAssertEqual(0, emitter.dbCount) + XCTAssertEqual(0, dbCount(emitter)) - emitter.flush() + flush(emitter) } func testEmitSingleGetEventWithNoSuccess() { let networkConnection = MockNetworkConnection(requestOption: .get, statusCode: 500) let emitter = self.emitter(with: networkConnection, bufferOption: .single) - emitter.addPayload(toBuffer: generatePayloads(1).first!) + addPayload(generatePayloads(1).first!, emitter) for _ in 0..<10 { Thread.sleep(forTimeInterval: 1) @@ -156,9 +161,9 @@ class LegacyTestEmitter: XCTestCase { XCTAssertEqual(1, networkConnection.previousResults.count) XCTAssertEqual(1, networkConnection.previousResults.first!.count) XCTAssertFalse(networkConnection.previousResults.first!.first!.isSuccessful) - XCTAssertEqual(1, emitter.dbCount) + XCTAssertEqual(1, dbCount(emitter)) - emitter.flush() + flush(emitter) } func testEmitTwoGetEventsWithSuccess() { @@ -166,14 +171,14 @@ class LegacyTestEmitter: XCTestCase { let emitter = self.emitter(with: networkConnection, bufferOption: .single) for payload in generatePayloads(2) { - emitter.addPayload(toBuffer: payload) + addPayload(payload, emitter) } for _ in 0..<10 { Thread.sleep(forTimeInterval: 1) } - XCTAssertEqual(0, emitter.dbCount) + XCTAssertEqual(0, dbCount(emitter)) var totEvents = 0 for results in networkConnection.previousResults { for result in results { @@ -183,7 +188,7 @@ class LegacyTestEmitter: XCTestCase { } XCTAssertEqual(2, totEvents) - emitter.flush() + flush(emitter) } func testEmitTwoGetEventsWithNoSuccess() { @@ -191,28 +196,28 @@ class LegacyTestEmitter: XCTestCase { let emitter = self.emitter(with: networkConnection, bufferOption: .single) for payload in generatePayloads(2) { - emitter.addPayload(toBuffer: payload) + addPayload(payload, emitter) } for _ in 0..<10 { Thread.sleep(forTimeInterval: 1) } - XCTAssertEqual(2, emitter.dbCount) + XCTAssertEqual(2, dbCount(emitter)) for results in networkConnection.previousResults { for result in results { XCTAssertFalse(result.isSuccessful) } } - emitter.flush() + flush(emitter) } func testEmitSinglePostEventWithSuccess() { let networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 200) let emitter = self.emitter(with: networkConnection, bufferOption: .single) - emitter.addPayload(toBuffer: generatePayloads(1).first!) + addPayload(generatePayloads(1).first!, emitter) for _ in 0..<10 { Thread.sleep(forTimeInterval: 1) @@ -221,34 +226,35 @@ class LegacyTestEmitter: XCTestCase { XCTAssertEqual(1, networkConnection.previousResults.count) XCTAssertEqual(1, networkConnection.previousResults.first?.count) XCTAssertTrue(networkConnection.previousResults.first!.first!.isSuccessful) - XCTAssertEqual(0, emitter.dbCount) + XCTAssertEqual(0, dbCount(emitter)) - emitter.flush() + flush(emitter) } func testEmitEventsPostAsGroup() { + let payloads = generatePayloads(15) + let networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 500) let emitter = self.emitter(with: networkConnection, bufferOption: .defaultGroup) - - let payloads = generatePayloads(15) + for i in 0..<14 { - emitter.addPayload(toBuffer: payloads[i]) + addPayload(payloads[i], emitter) } for _ in 0..<10 { Thread.sleep(forTimeInterval: 1) } - XCTAssertEqual(14, emitter.dbCount) + XCTAssertEqual(14, dbCount(emitter)) networkConnection.statusCode = 200 let prevSendingCount = networkConnection.sendingCount - emitter.addPayload(toBuffer: payloads[14]) + addPayload(payloads[14], emitter) for _ in 0..<10 { Thread.sleep(forTimeInterval: 1) } - XCTAssertEqual(0, emitter.dbCount) + XCTAssertEqual(0, dbCount(emitter)) var totEvents = 0 var areGrouped = false let prevResults = networkConnection.previousResults[prevSendingCount.. Emitter { - let emitter = Emitter(networkConnection: networkConnection) { emitter in + let emitter = Emitter(networkConnection: networkConnection, namespace: "ns1", eventStore: MockEventStore()) { emitter in emitter.bufferOption = bufferOption emitter.emitRange = 200 emitter.byteLimitGet = 20000 emitter.byteLimitPost = 25000 - emitter.eventStore = MockEventStore() } return emitter } @@ -388,5 +393,23 @@ class LegacyTestEmitter: XCTestCase { } return payloads } + + private func addPayload(_ eventPayload: Payload, _ emitter: Emitter) { + InternalQueue.sync { + emitter.addPayload(toBuffer: eventPayload) + } + } + + private func flush(_ emitter: Emitter) { + InternalQueue.sync { + emitter.flush() + } + } + + private func dbCount(_ emitter: Emitter) -> Int { + return InternalQueue.sync { + emitter.dbCount + } + } } //#pragma clang diagnostic pop diff --git a/Tests/Media/TestMediaController.swift b/Tests/Media/TestMediaController.swift index d3dd0ffb4..405c3e83f 100644 --- a/Tests/Media/TestMediaController.swift +++ b/Tests/Media/TestMediaController.swift @@ -16,7 +16,8 @@ import XCTest class TestMediaController: XCTestCase { - var trackedEvents: [InspectableEvent] = [] + var eventSink: EventSink? + var trackedEvents: [InspectableEvent] { return eventSink?.trackedEvents ?? [] } var tracker: TrackerController? var mediaController: MediaController? { tracker?.media } var firstEvent: InspectableEvent? { trackedEvents.first } @@ -31,7 +32,7 @@ class TestMediaController: XCTestCase { override func tearDown() { Snowplow.removeAllTrackers() - trackedEvents.removeAll() + eventSink = nil } // MARK: Media player event tests @@ -239,10 +240,14 @@ class TestMediaController: XCTestCase { waitForEventsToBeTracked() - XCTAssertEqual(15, firstEvent?.payload["percentProgress"] as? Int) - XCTAssertEqual(30, secondEvent?.payload["percentProgress"] as? Int) - XCTAssertEqual(40, trackedEvents[2].payload["percentProgress"] as? Int) - XCTAssertEqual(50, trackedEvents[3].payload["percentProgress"] as? Int) + let adClickEvent = trackedEvents.first { $0.schema == MediaSchemata.eventSchema("ad_click") } + XCTAssertEqual(15, adClickEvent?.payload["percentProgress"] as? Int) + let adSkipEvent = trackedEvents.first { $0.schema == MediaSchemata.eventSchema("ad_skip") } + XCTAssertEqual(30, adSkipEvent?.payload["percentProgress"] as? Int) + let adResumeEvent = trackedEvents.first { $0.schema == MediaSchemata.eventSchema("ad_resume") } + XCTAssertEqual(40, adResumeEvent?.payload["percentProgress"] as? Int) + let adPauseEvent = trackedEvents.first { $0.schema == MediaSchemata.eventSchema("ad_pause") } + XCTAssertEqual(50, adPauseEvent?.payload["percentProgress"] as? Int) } func testSetsQualityPropertiesInQualityChangeEvent() { @@ -316,10 +321,10 @@ class TestMediaController: XCTestCase { player: MediaPlayerEntity(duration: 10), session: session) - media.track(MediaPlayEvent()) + track(MediaPlayEvent(), media: media) timeTraveler.travel(by: 10.0) - media.update(player: MediaPlayerEntity(currentTime: 10.0)) - media.track(MediaEndEvent()) + update(player: MediaPlayerEntity(currentTime: 10.0), media: media) + track(MediaEndEvent(), media: media) waitForEventsToBeTracked() @@ -332,7 +337,7 @@ class TestMediaController: XCTestCase { // MARK: Ping events func testStartsSendingPingEventsAfterSessionStarts() { - let pingInterval = MediaPingInterval(timerProvider: MockTimer.self) + let pingInterval = MediaPingInterval(startTimer: MockTimer.startTimer) _ = MediaTrackingImpl(id: "media1", tracker: tracker!, pingInterval: pingInterval) MockTimer.currentTimer.fire() @@ -344,12 +349,12 @@ class TestMediaController: XCTestCase { } func testShouldSendPingEventsRegardlessOfOtherEvents() { - let pingInterval = MediaPingInterval(timerProvider: MockTimer.self) + let pingInterval = MediaPingInterval(startTimer: MockTimer.startTimer) let media = MediaTrackingImpl(id: "media1", tracker: tracker!, pingInterval: pingInterval) - media.track(MediaPlayEvent()) + track(MediaPlayEvent(), media: media) MockTimer.currentTimer.fire() - media.track(MediaPauseEvent()) + track(MediaPauseEvent(), media: media) MockTimer.currentTimer.fire() waitForEventsToBeTracked() @@ -358,10 +363,10 @@ class TestMediaController: XCTestCase { } func testShouldStopSendingPingEventsWhenPaused() { - let pingInterval = MediaPingInterval(maxPausedPings: 2, timerProvider: MockTimer.self) + let pingInterval = MediaPingInterval(maxPausedPings: 2, startTimer: MockTimer.startTimer) let media = MediaTrackingImpl(id: "media1", tracker: tracker!, pingInterval: pingInterval) - media.update(player: MediaPlayerEntity(paused: true)) + update(player: MediaPlayerEntity(paused: true), media: media) for _ in 0..<5 { MockTimer.currentTimer.fire() } @@ -372,10 +377,10 @@ class TestMediaController: XCTestCase { } func testShouldNotStopSendingPingEventsWhenPlaying() { - let pingInterval = MediaPingInterval(maxPausedPings: 2, timerProvider: MockTimer.self) + let pingInterval = MediaPingInterval(maxPausedPings: 2, startTimer: MockTimer.startTimer) let media = MediaTrackingImpl(id: "media1", tracker: tracker!, pingInterval: pingInterval) - media.update(player: MediaPlayerEntity(paused: false)) + update(player: MediaPlayerEntity(paused: false), media: media) for _ in 0..<5 { MockTimer.currentTimer.fire() } @@ -450,25 +455,27 @@ class TestMediaController: XCTestCase { trackerConfig.lifecycleAutotracking = false let namespace = "testMedia" + String(describing: Int.random(in: 0..<100)) - let plugin = PluginConfiguration(identifier: "testPlugin" + namespace) - .afterTrack { event in - if namespace == self.tracker?.namespace { - self.trackedEvents.append(event) - } - } - + self.eventSink = EventSink() return Snowplow.createTracker(namespace: namespace, network: networkConfig, - configurations: [trackerConfig, plugin]) + configurations: [trackerConfig, eventSink!]) } private func waitForEventsToBeTracked() { let expect = expectation(description: "Wait for events to be tracked") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { () -> Void in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { () -> Void in expect.fulfill() } wait(for: [expect], timeout: 1) } + + private func track(_ event: Event, player: MediaPlayerEntity? = nil, ad: MediaAdEntity? = nil, adBreak: MediaAdBreakEntity? = nil, media: MediaTracking?) { + InternalQueue.sync { media?.track(event, player: player, ad: ad, adBreak: adBreak) } + } + + private func update(player: MediaPlayerEntity? = nil, ad: MediaAdEntity? = nil, adBreak: MediaAdBreakEntity? = nil, media: MediaTracking?) { + InternalQueue.sync { media?.update(player: player, ad: ad, adBreak: adBreak) } + } } extension InspectableEvent { @@ -480,4 +487,3 @@ extension InspectableEvent { return entities.first { $0.schema == MediaSchemata.sessionSchema }?.data } } - diff --git a/Tests/Storage/TestSQLiteEventStore.swift b/Tests/Storage/TestSQLiteEventStore.swift index 354fdf41b..074d1b322 100644 --- a/Tests/Storage/TestSQLiteEventStore.swift +++ b/Tests/Storage/TestSQLiteEventStore.swift @@ -20,7 +20,7 @@ class TestSQLiteEventStore: XCTestCase { func testInsertPayload() { let eventStore = createEventStore("aNamespace") - _ = eventStore.removeAllEvents() + removeAllEvents(eventStore) // Build an event let payload = Payload() @@ -30,20 +30,20 @@ class TestSQLiteEventStore: XCTestCase { payload.addValueToPayload("MEEEE", forKey: "refr") // Insert an event - eventStore.addEvent(payload) + addEvent(payload, eventStore) - XCTAssertEqual(eventStore.count(), 1) - let emittableEvents = eventStore.emittableEvents(withQueryLimit: 10) + XCTAssertEqual(count(eventStore), 1) + let emittableEvents = emittableEvents(withQueryLimit: 10, eventStore) XCTAssertEqual(emittableEvents.first?.payload.dictionary as! [String : String], payload.dictionary as! [String : String]) - _ = eventStore.removeEvent(withId: emittableEvents.first?.storeId ?? 0) + removeEvent(withId: emittableEvents.first?.storeId ?? 0, eventStore) - XCTAssertEqual(eventStore.count(), 0) + XCTAssertEqual(count(eventStore), 0) } func testInsertManyPayloads() { let eventStore = createEventStore("aNamespace") - _ = eventStore.removeAllEvents() + removeAllEvents(eventStore) // Build an event let payload = Payload() @@ -52,30 +52,16 @@ class TestSQLiteEventStore: XCTestCase { payload.addValueToPayload("Welcome to foobar!", forKey: "page") payload.addValueToPayload("MEEEE", forKey: "refr") - let dispatchQueue = DispatchQueue(label: "Save events", attributes: .concurrent) - let expectations = [ - XCTestExpectation(), - XCTestExpectation(), - XCTestExpectation(), - XCTestExpectation(), - XCTestExpectation() - ] - for i in 0..<5 { - dispatchQueue.async { - for _ in 0..<250 { - eventStore.addEvent(payload) - } - expectations[i].fulfill() - } + for _ in 0..<250 { + addEvent(payload, eventStore) } - wait(for: expectations, timeout: 10) - XCTAssertEqual(eventStore.count(), 1250) - XCTAssertEqual(eventStore.emittableEvents(withQueryLimit: 600).count, 250) - XCTAssertEqual(eventStore.emittableEvents(withQueryLimit: 150).count, 150) + XCTAssertEqual(count(eventStore), 250) + XCTAssertEqual(emittableEvents(withQueryLimit: 600, eventStore).count, 250) + XCTAssertEqual(emittableEvents(withQueryLimit: 150, eventStore).count, 150) - _ = eventStore.removeAllEvents() - XCTAssertEqual(eventStore.count(), 0) + removeAllEvents(eventStore) + XCTAssertEqual(count(eventStore), 0) } func testSQLiteEventStoreCreateSQLiteFile() { @@ -98,10 +84,10 @@ class TestSQLiteEventStore: XCTestCase { func testMigrationFromLegacyToNamespacedEventStore() { var eventStore = self.createEventStore("aNamespace") - eventStore.addEvent(Payload(dictionary: [ + addEvent(Payload(dictionary: [ "key": "value" - ])) - XCTAssertEqual(1, eventStore.count()) + ]), eventStore) + XCTAssertEqual(1, count(eventStore)) // Create fake legacy database let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0] @@ -116,33 +102,53 @@ class TestSQLiteEventStore: XCTestCase { // Migrate database when SQLiteEventStore is launched the first time eventStore = createEventStore("aNewNamespace") - XCTAssertEqual(1, eventStore.count()) + XCTAssertEqual(1, count(eventStore)) newDbPath = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent("snowplowEvents-aNewNamespace.sqlite").path XCTAssertFalse(FileManager.default.fileExists(atPath: oldDbPath)) XCTAssertTrue(FileManager.default.fileExists(atPath: newDbPath)) - for event in eventStore.emittableEvents(withQueryLimit: 100) { + for event in emittableEvents(withQueryLimit: 100, eventStore) { XCTAssertEqual("value", event.payload.dictionary["key"] as? String) } } func testMultipleAccessToSameSQLiteFile() { let eventStore1 = createEventStore("aNamespace") - eventStore1.addEvent(Payload(dictionary: [ + addEvent(Payload(dictionary: [ "key1": "value1" - ])) - XCTAssertEqual(1, eventStore1.count()) + ]), eventStore1) + XCTAssertEqual(1, count(eventStore1)) let eventStore2 = SQLiteEventStore(namespace: "aNamespace") - eventStore2.addEvent(Payload(dictionary: [ + addEvent(Payload(dictionary: [ "key2": "value2" - ])) - XCTAssertEqual(2, eventStore2.count()) + ]), eventStore2) + XCTAssertEqual(2, count(eventStore2)) } private func createEventStore(_ namespace: String, limit: Int = 250) -> SQLiteEventStore { DatabaseHelpers.clearPreviousDatabase(namespace) return SQLiteEventStore(namespace: namespace, limit: limit) } + + private func addEvent(_ payload: Payload, _ eventStore: EventStore) { + InternalQueue.sync { eventStore.addEvent(payload) } + } + + private func removeAllEvents(_ eventStore: EventStore) { + InternalQueue.sync { _ = eventStore.removeAllEvents() } + } + + private func removeEvent(withId: Int64, _ eventStore: EventStore) { + InternalQueue.sync { _ = eventStore.removeEvent(withId: withId) } + } + + private func count(_ eventStore: EventStore) -> UInt { + InternalQueue.sync { return eventStore.count() } + } + + private func emittableEvents(withQueryLimit: UInt, _ eventStore: EventStore) -> [EmitterEvent] { + InternalQueue.sync { return eventStore.emittableEvents(withQueryLimit: withQueryLimit) } + } } #endif diff --git a/Tests/TestEvents.swift b/Tests/TestEvents.swift index a81a53608..453b4e377 100644 --- a/Tests/TestEvents.swift +++ b/Tests/TestEvents.swift @@ -123,7 +123,7 @@ class TestEvents: XCTestCase { var screenViewPayload: Payload? = nil for event in events { - if (event.payload.dictionary["eid"] as? String) == screenViewId?.uuidString { + if (event.payload.dictionary["eid"] as? String) == screenViewId.uuidString { screenViewPayload = event.payload } } diff --git a/Tests/TestLifecycleState.swift b/Tests/TestLifecycleState.swift index 024e7e3c0..2914ed94e 100644 --- a/Tests/TestLifecycleState.swift +++ b/Tests/TestLifecycleState.swift @@ -25,17 +25,18 @@ class TestLifecycleState: XCTestCase { func testLifecycleStateMachine() { let eventStore = MockEventStore() - let emitter = Emitter(urlEndpoint: "http://snowplow-fake-url.com") { emitter in - emitter.eventStore = eventStore - } + let emitter = Emitter( + networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), + namespace: "namespace", + eventStore: eventStore + ) let tracker = Tracker(trackerNamespace: "namespace", appId: nil, emitter: emitter) { tracker in tracker.base64Encoded = false tracker.lifecycleEvents = true } // Send events - _ = tracker.track(Timing(category: "category", variable: "variable", timing: 123)) - Thread.sleep(forTimeInterval: 1) + track(Timing(category: "category", variable: "variable", timing: 123), tracker) if eventStore.lastInsertedRow == -1 { XCTFail() } @@ -45,8 +46,7 @@ class TestLifecycleState: XCTestCase { XCTAssertNotNil(entities) XCTAssertTrue(entities!.contains("\"isVisible\":true")) - _ = tracker.track(Background(index: 1)) - Thread.sleep(forTimeInterval: 1) + track(Background(index: 1), tracker) if eventStore.lastInsertedRow == -1 { XCTFail() } @@ -56,8 +56,7 @@ class TestLifecycleState: XCTestCase { XCTAssertNotNil(entities) XCTAssertTrue(entities!.contains("\"isVisible\":false")) - _ = tracker.track(Timing(category: "category", variable: "variable", timing: 123)) - Thread.sleep(forTimeInterval: 1) + track(Timing(category: "category", variable: "variable", timing: 123), tracker) if eventStore.lastInsertedRow == -1 { XCTFail() } @@ -66,8 +65,7 @@ class TestLifecycleState: XCTestCase { entities = (payload?["co"]) as? String XCTAssertTrue(entities!.contains("\"isVisible\":false")) - _ = tracker.track(Foreground(index: 1)) - Thread.sleep(forTimeInterval: 1) + track(Foreground(index: 1), tracker) if eventStore.lastInsertedRow == -1 { XCTFail() } @@ -78,8 +76,7 @@ class TestLifecycleState: XCTestCase { XCTAssertTrue(entities!.contains("\"isVisible\":true")) let uuid = UUID() - _ = tracker.track(ScreenView(name: "screen1", screenId: uuid)) - Thread.sleep(forTimeInterval: 1) + track(ScreenView(name: "screen1", screenId: uuid), tracker) if eventStore.lastInsertedRow == -1 { XCTFail() } @@ -89,4 +86,10 @@ class TestLifecycleState: XCTestCase { XCTAssertNotNil(entities) XCTAssertTrue(entities!.contains("\"isVisible\":true")) } + + private func track(_ event: Event, _ tracker: Tracker) { + InternalQueue.sync { + _ = tracker.track(event) + } + } } diff --git a/Tests/TestMemoryEventStore.swift b/Tests/TestMemoryEventStore.swift index cd6868649..dbbb8436c 100644 --- a/Tests/TestMemoryEventStore.swift +++ b/Tests/TestMemoryEventStore.swift @@ -22,7 +22,7 @@ class TestMemoryEventStore: XCTestCase { func testInsertPayload() { let eventStore = MemoryEventStore() - _ = eventStore.removeAllEvents() + removeAllEvents(eventStore) // Build an event let payload = Payload() @@ -32,20 +32,20 @@ class TestMemoryEventStore: XCTestCase { payload.addValueToPayload("MEEEE", forKey: "refr") // Insert an event - eventStore.addEvent(payload) + addEvent(payload, eventStore) - XCTAssertEqual(eventStore.count(), 1) - let events = eventStore.emittableEvents(withQueryLimit: 1) + XCTAssertEqual(count(eventStore), 1) + let events = emittableEvents(withQueryLimit: 1, eventStore) XCTAssertEqual(events[0].payload.dictionary as! [String : String], payload.dictionary as! [String : String]) - _ = eventStore.removeEvent(withId: 0) + removeEvent(withId: 0, eventStore) - XCTAssertEqual(eventStore.count(), 0) + XCTAssertEqual(count(eventStore), 0) } func testInsertManyPayloads() { let eventStore = MemoryEventStore() - _ = eventStore.removeAllEvents() + removeAllEvents(eventStore) // Build an event let payload = Payload() @@ -55,14 +55,34 @@ class TestMemoryEventStore: XCTestCase { payload.addValueToPayload("MEEEE", forKey: "refr") for _ in 0..<250 { - eventStore.addEvent(payload) + addEvent(payload, eventStore) } - XCTAssertEqual(eventStore.count(), 250) - XCTAssertEqual(eventStore.emittableEvents(withQueryLimit: 600).count, 250) - XCTAssertEqual(eventStore.emittableEvents(withQueryLimit: 150).count, 150) + XCTAssertEqual(count(eventStore), 250) + XCTAssertEqual(emittableEvents(withQueryLimit: 600, eventStore).count, 250) + XCTAssertEqual(emittableEvents(withQueryLimit: 150, eventStore).count, 150) - _ = eventStore.removeAllEvents() - XCTAssertEqual(eventStore.count(), 0) + removeAllEvents(eventStore) + XCTAssertEqual(count(eventStore), 0) + } + + private func addEvent(_ payload: Payload, _ eventStore: EventStore) { + InternalQueue.sync { eventStore.addEvent(payload) } + } + + private func removeAllEvents(_ eventStore: EventStore) { + InternalQueue.sync { _ = eventStore.removeAllEvents() } + } + + private func removeEvent(withId: Int64, _ eventStore: EventStore) { + InternalQueue.sync { _ = eventStore.removeEvent(withId: withId) } + } + + private func count(_ eventStore: EventStore) -> UInt { + InternalQueue.sync { return eventStore.count() } + } + + private func emittableEvents(withQueryLimit: UInt, _ eventStore: EventStore) -> [EmitterEvent] { + InternalQueue.sync { return eventStore.emittableEvents(withQueryLimit: withQueryLimit) } } } diff --git a/Tests/TestPlugins.swift b/Tests/TestPlugins.swift index 34878e02b..4bf59a1a2 100644 --- a/Tests/TestPlugins.swift +++ b/Tests/TestPlugins.swift @@ -25,17 +25,16 @@ class TestPlugins: XCTestCase { .entities { [SelfDescribingJson(schema: "schema", andData: ["val": $0.payload["se_ca"]!])] } let expect = expectation(description: "Has context entity on event") - let testPlugin = PluginConfiguration(identifier: "test") - .afterTrack { event in - XCTAssertTrue( - event.entities.filter({ entity in - entity.schema == "schema" && entity.data["val"] as? String == "cat" - }).count == 1 - ) - expect.fulfill() - } + let eventSink = EventSink { event in + XCTAssertTrue( + event.entities.filter({ entity in + entity.schema == "schema" && entity.data["val"] as? String == "cat" + }).count == 1 + ) + expect.fulfill() + } - let tracker = createTracker([plugin, testPlugin]) + let tracker = createTracker([plugin, eventSink]) _ = tracker.track(Structured(category: "cat", action: "act")) wait(for: [expect], timeout: 10) @@ -49,18 +48,17 @@ class TestPlugins: XCTestCase { .entities { _ in [SelfDescribingJson(schema: "schema2", andData: [:])] } let expect = expectation(description: "Has both context entities on event") - let testPlugin = PluginConfiguration(identifier: "test") - .afterTrack { event in - XCTAssertTrue( - event.entities.filter({ $0.schema == "schema1" }).count == 1 - ) - XCTAssertTrue( - event.entities.filter({ $0.schema == "schema2" }).count == 1 - ) - expect.fulfill() - } + let eventSink = EventSink { event in + XCTAssertTrue( + event.entities.filter({ $0.schema == "schema1" }).count == 1 + ) + XCTAssertTrue( + event.entities.filter({ $0.schema == "schema2" }).count == 1 + ) + expect.fulfill() + } - let tracker = createTracker([plugin1, plugin2, testPlugin]) + let tracker = createTracker([plugin1, plugin2, eventSink]) _ = tracker.track(ScreenView(name: "sv")) wait(for: [expect], timeout: 1) @@ -73,17 +71,16 @@ class TestPlugins: XCTestCase { var event1HasEntity: Bool? = nil var event2HasEntity: Bool? = nil - let testPlugin = PluginConfiguration(identifier: "test") - .afterTrack { event in - if event.schema == "schema1" { - event1HasEntity = event.entities.contains(where: { $0.schema == "xx" }) - } - if event.schema == "schema2" { - event2HasEntity = event.entities.contains(where: { $0.schema == "xx" }) - } + let eventSink = EventSink { event in + if event.schema == "schema1" { + event1HasEntity = event.entities.contains(where: { $0.schema == "xx" }) } + if event.schema == "schema2" { + event2HasEntity = event.entities.contains(where: { $0.schema == "xx" }) + } + } - let tracker = createTracker([plugin, testPlugin]) + let tracker = createTracker([plugin, eventSink]) _ = tracker.track(SelfDescribing(schema: "schema1", payload: [:])) _ = tracker.track(SelfDescribing(schema: "schema2", payload: [:])) diff --git a/Tests/TestRequest.swift b/Tests/TestRequest.swift index 0d3e9c225..330c6932a 100644 --- a/Tests/TestRequest.swift +++ b/Tests/TestRequest.swift @@ -124,7 +124,9 @@ class TestRequest: XCTestCase, RequestCallback { if emitter?.dbCount == 0 { break } - emitter?.flush() + InternalQueue.sync { + emitter?.flush() + } Thread.sleep(forTimeInterval: 5) } Thread.sleep(forTimeInterval: 3) @@ -153,7 +155,7 @@ class TestRequest: XCTestCase, RequestCallback { event.property = "DemoProperty" event.value = NSNumber(value: 5) event.entities = customContext() - _ = tracker_.track(event) + track(event, tracker_) return 1 } @@ -166,7 +168,7 @@ class TestRequest: XCTestCase, RequestCallback { andDictionary: data) let event = SelfDescribing(eventData: sdj) event.entities = customContext() - _ = tracker_.track(event) + track(event, tracker_) return 1 } @@ -175,14 +177,14 @@ class TestRequest: XCTestCase, RequestCallback { event.pageTitle = "DemoPageTitle" event.referrer = "DemoPageReferrer" event.entities = customContext() - _ = tracker_.track(event) + track(event, tracker_) return 1 } func trackScreenView(with tracker_: Tracker) -> Int { let event = ScreenView(name: "DemoScreenName", screenId: nil) event.entities = customContext() - _ = tracker_.track(event) + track(event, tracker_) return 1 } @@ -190,7 +192,7 @@ class TestRequest: XCTestCase, RequestCallback { let event = Timing(category: "DemoTimingCategory", variable: "DemoTimingVariable", timing: 5) event.label = "DemoTimingLabel" event.entities = customContext() - _ = tracker_.track(event) + track(event, tracker_) return 1 } @@ -212,7 +214,7 @@ class TestRequest: XCTestCase, RequestCallback { event.country = "USA" event.currency = "USD" event.entities = customContext() - _ = tracker_.track(event) + track(event, tracker_) return 2 } @@ -225,4 +227,10 @@ class TestRequest: XCTestCase, RequestCallback { andDictionary: data) return [context] } + + private func track(_ event: Event, _ tracker: Tracker) { + InternalQueue.sync { + _ = tracker.track(event) + } + } } diff --git a/Tests/TestScreenState.swift b/Tests/TestScreenState.swift index 30ceeb8bd..d7218489b 100644 --- a/Tests/TestScreenState.swift +++ b/Tests/TestScreenState.swift @@ -59,9 +59,7 @@ class TestScreenState: XCTestCase { func testScreenStateMachine() { let eventStore = MockEventStore() - let emitter = Emitter(urlEndpoint: "http://snowplow-fake-url.com") { emitter in - emitter.eventStore = eventStore - } + let emitter = Emitter(namespace: "namespace", urlEndpoint: "http://snowplow-fake-url.com", eventStore: eventStore) let tracker = Tracker(trackerNamespace: "namespace", appId: nil, emitter: emitter) { tracker in tracker.base64Encoded = false tracker.screenContext = true @@ -70,7 +68,7 @@ class TestScreenState: XCTestCase { emitter.pauseEmit() // Send events - _ = tracker.track(Timing(category: "category", variable: "variable", timing: 123)) + track(Timing(category: "category", variable: "variable", timing: 123), tracker) Thread.sleep(forTimeInterval: 1) if eventStore.lastInsertedRow == -1 { XCTFail() @@ -81,7 +79,7 @@ class TestScreenState: XCTestCase { XCTAssertNil(entities) let uuid = UUID() - _ = tracker.track(ScreenView(name: "screen1", screenId: uuid)) + track(ScreenView(name: "screen1", screenId: uuid), tracker) Thread.sleep(forTimeInterval: 1) if eventStore.lastInsertedRow == -1 { XCTFail() @@ -92,7 +90,7 @@ class TestScreenState: XCTestCase { XCTAssertNotNil(entities) XCTAssertTrue(entities!.contains(uuid.uuidString)) - _ = tracker.track(Timing(category: "category", variable: "variable", timing: 123)) + track(Timing(category: "category", variable: "variable", timing: 123), tracker) Thread.sleep(forTimeInterval: 1) if eventStore.lastInsertedRow == -1 { XCTFail() @@ -104,7 +102,7 @@ class TestScreenState: XCTestCase { XCTAssertTrue(entities!.contains(uuid.uuidString)) let uuid2 = UUID() - _ = tracker.track(ScreenView(name: "screen2", screenId: uuid2)) + track(ScreenView(name: "screen2", screenId: uuid2), tracker) Thread.sleep(forTimeInterval: 1) if eventStore.lastInsertedRow == -1 { XCTFail() @@ -119,7 +117,7 @@ class TestScreenState: XCTestCase { XCTAssertTrue(eventPayload!.contains(uuid.uuidString)) XCTAssertTrue(eventPayload!.contains(uuid2.uuidString)) - _ = tracker.track(Timing(category: "category", variable: "variable", timing: 123)) + track(Timing(category: "category", variable: "variable", timing: 123), tracker) Thread.sleep(forTimeInterval: 1) if eventStore.lastInsertedRow == -1 { XCTFail() @@ -130,4 +128,10 @@ class TestScreenState: XCTestCase { XCTAssertNotNil(entities) XCTAssertTrue(entities!.contains(uuid2.uuidString)) } + + private func track(_ event: Event, _ tracker: Tracker) { + InternalQueue.sync { + _ = tracker.track(event) + } + } } diff --git a/Tests/TestScreenViewModifier.swift b/Tests/TestScreenViewModifier.swift index 82743a788..e9c84e32d 100644 --- a/Tests/TestScreenViewModifier.swift +++ b/Tests/TestScreenViewModifier.swift @@ -58,8 +58,7 @@ class TestScreenViewModifier: XCTestCase { namespace: "screenViewTracker", network: networkConfig, configurations: [ - PluginConfiguration(identifier: "screenViewPlugin") - .afterTrack(closure: afterTrack), + EventSink(callback: afterTrack), TrackerConfiguration() .installAutotracking(false) .lifecycleAutotracking(false) diff --git a/Tests/TestServiceProvider.swift b/Tests/TestServiceProvider.swift index 26617a807..39c5997da 100644 --- a/Tests/TestServiceProvider.swift +++ b/Tests/TestServiceProvider.swift @@ -48,16 +48,20 @@ class TestServiceProvider: XCTestCase { serviceProvider.reset(configurations: [EmitterConfiguration()]) // track event and check that emitter is paused - _ = serviceProvider.trackerController.track(Structured(category: "cat", action: "act")) + InternalQueue.sync { + _ = serviceProvider.trackerController.track(Structured(category: "cat", action: "act")) + } Thread.sleep(forTimeInterval: 3) - XCTAssertEqual(1, serviceProvider.emitter.dbCount) + InternalQueue.sync { XCTAssertEqual(1, serviceProvider.emitter.dbCount) } XCTAssertEqual(0, networkConnection.sendingCount) // resume emitting - serviceProvider.emitterController.resume() + InternalQueue.sync { + serviceProvider.emitterController.resume() + } Thread.sleep(forTimeInterval: 3) XCTAssertEqual(1, networkConnection.sendingCount) - XCTAssertEqual(0, serviceProvider.emitter.dbCount) + InternalQueue.sync { XCTAssertEqual(0, serviceProvider.emitter.dbCount) } } // TODO: fix logging and handle the case //- (void)testLogsErrorWhenAccessingShutDownTracker { diff --git a/Tests/TestSession.swift b/Tests/TestSession.swift index 0226350ef..cce57c780 100644 --- a/Tests/TestSession.swift +++ b/Tests/TestSession.swift @@ -26,17 +26,16 @@ class TestSession: XCTestCase { } func testInit() { - let session = Session(foregroundTimeout: 600, andBackgroundTimeout: 300) - XCTAssertNil(session.tracker) + let session = Session(foregroundTimeout: 600, backgroundTimeout: 300) XCTAssertTrue(!session.inBackground) XCTAssertNotNil(session.getDictWithEventId("eventid-1", eventTimestamp: 1654496481346, userAnonymisation: false)) - XCTAssertTrue(session.state!.sessionIndex >= 1) + XCTAssertTrue(session.sessionIndex ?? 0 >= 1) XCTAssertEqual(session.foregroundTimeout, 600000) XCTAssertEqual(session.backgroundTimeout, 300000) } func testInitWithOptions() { - let session = Session(foregroundTimeout: 5, andBackgroundTimeout: 300, andTracker: nil) + let session = Session(foregroundTimeout: 5, backgroundTimeout: 300) XCTAssertEqual(session.foregroundTimeout, 5000) XCTAssertEqual(session.backgroundTimeout, 300000) @@ -48,10 +47,10 @@ class TestSession: XCTestCase { } func testFirstSession() { - let session = Session(foregroundTimeout: 3, andBackgroundTimeout: 3, andTracker: nil) + let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) let sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) - let sessionIndex = session.state!.sessionIndex + let sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) @@ -59,10 +58,10 @@ class TestSession: XCTestCase { } func testForegroundEventsOnSameSession() { - let session = Session(foregroundTimeout: 3, andBackgroundTimeout: 3, andTracker: nil) + let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) - var sessionIndex = session.state?.sessionIndex + var sessionIndex = session.sessionIndex ?? 0 let sessionId = sessionContext?[kSPSessionId] as? String XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -72,7 +71,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) sessionContext = session.getDictWithEventId("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) - sessionIndex = session.state?.sessionIndex + sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) @@ -82,7 +81,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) sessionContext = session.getDictWithEventId("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) - sessionIndex = session.state?.sessionIndex + sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) @@ -92,7 +91,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 3.1) sessionContext = session.getDictWithEventId("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) - sessionIndex = session.state?.sessionIndex + sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(2, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual("event_4", sessionContext?[kSPSessionFirstEventId] as? String) @@ -103,7 +102,7 @@ class TestSession: XCTestCase { func testBackgroundEventsOnWhenLifecycleEventsDisabled() { cleanFile(withNamespace: "tracker") - let emitter = Emitter(urlEndpoint: "") { emitter in} + let emitter = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "tracker") let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter) { tracker in tracker.lifecycleEvents = false tracker.sessionContext = true @@ -115,7 +114,7 @@ class TestSession: XCTestCase { session?.updateInBackground() let sessionContext = session?.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) - let sessionIndex = session?.state?.sessionIndex ?? 0 + let sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) @@ -127,8 +126,8 @@ class TestSession: XCTestCase { func testBackgroundEventsOnSameSession() { cleanFile(withNamespace: "t1") - let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker = Tracker(trackerNamespace: "t1", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in + let emitter = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "t1") + let tracker = Tracker(trackerNamespace: "t1", appId: nil, emitter: emitter) { tracker in tracker.installEvent = false tracker.lifecycleEvents = true tracker.sessionContext = true @@ -138,11 +137,13 @@ class TestSession: XCTestCase { let session = tracker.session session?.updateInBackground() // It sends a background event + + Thread.sleep(forTimeInterval: 1) - let sessionId = session?.state?.sessionId + let sessionId = session?.sessionId var sessionContext = session?.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) - var sessionIndex = session?.state?.sessionIndex ?? 0 + var sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual(sessionId, sessionContext?[kSPSessionId] as? String) @@ -152,7 +153,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) sessionContext = session?.getDictWithEventId("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) - sessionIndex = session?.state?.sessionIndex ?? 0 + sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual(sessionId, sessionContext?[kSPSessionId] as? String) @@ -162,7 +163,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) sessionContext = session?.getDictWithEventId("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) - sessionIndex = session?.state?.sessionIndex ?? 0 + sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual(sessionId, sessionContext?[kSPSessionId] as? String) @@ -172,7 +173,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 2.1) sessionContext = session?.getDictWithEventId("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) - sessionIndex = session?.state?.sessionIndex ?? 0 + sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(2, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual("event_4", sessionContext?[kSPSessionFirstEventId] as? String) @@ -185,7 +186,7 @@ class TestSession: XCTestCase { func testMixedEventsOnManySessions() { cleanFile(withNamespace: "t2") - let emitter = Emitter(urlEndpoint: "") { emitter in} + let emitter = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "t2") let tracker = Tracker(trackerNamespace: "t2", appId: nil, emitter: emitter) { tracker in tracker.lifecycleEvents = true tracker.sessionContext = true @@ -239,7 +240,7 @@ class TestSession: XCTestCase { } func testTimeoutSessionWhenPauseAndResume() { - let session = Session(foregroundTimeout: 1, andBackgroundTimeout: 1, andTracker: nil) + let session = Session(foregroundTimeout: 1, backgroundTimeout: 1) var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481355, userAnonymisation: false) var prevSessionId = sessionContext?[kSPSessionId] as? String @@ -268,8 +269,8 @@ class TestSession: XCTestCase { func testBackgroundTimeBiggerThanBackgroundTimeoutCausesNewSession() { cleanFile(withNamespace: "tracker") - let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in + let emitter = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "tracker") + let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter) { tracker in tracker.lifecycleEvents = true tracker.sessionContext = true tracker.foregroundTimeout = 100 @@ -288,9 +289,10 @@ class TestSession: XCTestCase { session?.updateInBackground() // Sends a background event Thread.sleep(forTimeInterval: 3) // Bigger than background timeout session?.updateInForeground() // Sends a foreground event + Thread.sleep(forTimeInterval: 1) - XCTAssertEqual(oldSessionId, session?.state?.previousSessionId) - XCTAssertEqual(2, session?.state?.sessionIndex) + XCTAssertEqual(oldSessionId, session?.previousSessionId) + XCTAssertEqual(2, session?.sessionIndex) XCTAssertFalse(session!.inBackground) XCTAssertEqual(1, session?.backgroundIndex) XCTAssertEqual(1, session?.foregroundIndex) @@ -299,8 +301,8 @@ class TestSession: XCTestCase { func testBackgroundTimeSmallerThanBackgroundTimeoutDoesntCauseNewSession() { cleanFile(withNamespace: "tracker") - let emitter = Emitter(urlEndpoint: "") { emitter in} - let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in + let emitter = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "tracker") + let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter) { tracker in tracker.lifecycleEvents = true tracker.sessionContext = true tracker.foregroundTimeout = 100 @@ -319,16 +321,17 @@ class TestSession: XCTestCase { session?.updateInBackground() // Sends a background event Thread.sleep(forTimeInterval: 1) // Smaller than background timeout session?.updateInForeground() // Sends a foreground event + Thread.sleep(forTimeInterval: 1) - XCTAssertEqual(oldSessionId, session?.state?.sessionId) - XCTAssertEqual(1, session?.state?.sessionIndex) + XCTAssertEqual(oldSessionId, session?.sessionId) + XCTAssertEqual(1, session?.sessionIndex) XCTAssertFalse(session!.inBackground) XCTAssertEqual(1, session?.backgroundIndex) XCTAssertEqual(1, session?.foregroundIndex) } func testNoEventsForLongTimeDontIncreaseIndexMultipleTimes() { - let session = Session(foregroundTimeout: 1, andBackgroundTimeout: 1, andTracker: nil) + let session = Session(foregroundTimeout: 1, backgroundTimeout: 1) var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481359, userAnonymisation: false) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) @@ -344,49 +347,49 @@ class TestSession: XCTestCase { cleanFile(withNamespace: "tracker1") cleanFile(withNamespace: "tracker2") - let emitter = Emitter(urlEndpoint: "") { emitter in} - let queue2 = MockDispatchQueueWrapper(label: "test2") - let tracker1 = Tracker(trackerNamespace: "tracker1", appId: nil, emitter: emitter, dispatchQueue: MockDispatchQueueWrapper(label: "test1")) { tracker in + let emitter1 = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "tracker1") + let tracker1 = Tracker(trackerNamespace: "tracker1", appId: nil, emitter: emitter1) { tracker in tracker.sessionContext = true tracker.foregroundTimeout = 10 tracker.backgroundTimeout = 10 } - let tracker2 = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter, dispatchQueue: queue2) { tracker in + let emitter2 = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "tracker2") + let tracker2 = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter2) { tracker in tracker.sessionContext = true tracker.foregroundTimeout = 10 tracker.backgroundTimeout = 10 } let event = Structured(category: "c", action: "a") - _ = tracker1.track(event) - _ = tracker2.track(event) + track(event, tracker1) + track(event, tracker2) - guard let initialValue1 = tracker1.session?.state?.sessionIndex else { return XCTFail() } - guard let id1 = tracker1.session?.state?.sessionId else { return XCTFail() } - guard let initialValue2 = tracker2.session?.state?.sessionIndex else { return XCTFail() } - guard var id2 = tracker2.session?.state?.sessionId else { return XCTFail() } + guard let initialValue1 = tracker1.session?.sessionIndex else { return XCTFail() } + guard let id1 = tracker1.session?.sessionId else { return XCTFail() } + guard let initialValue2 = tracker2.session?.sessionIndex else { return XCTFail() } + guard var id2 = tracker2.session?.sessionId else { return XCTFail() } // Retrigger session in tracker1 Thread.sleep(forTimeInterval: 7) - _ = tracker1.track(event) + track(event, tracker1) Thread.sleep(forTimeInterval: 5) // Send event to force update of session on tracker2 - _ = tracker2.track(event) - id2 = tracker2.session!.state!.sessionId + track(event, tracker2) + id2 = tracker2.session!.sessionId! // Check sessions have the correct state - XCTAssertEqual(0, tracker1.session!.state!.sessionIndex - initialValue1) // retriggered - XCTAssertEqual(1, tracker2.session!.state!.sessionIndex - initialValue2) // timed out + XCTAssertEqual(0, tracker1.session!.sessionIndex! - initialValue1) // retriggered + XCTAssertEqual(1, tracker2.session!.sessionIndex! - initialValue2) // timed out //Recreate tracker2 - let tracker2b = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter, dispatchQueue: queue2) { tracker in + let tracker2b = Tracker(trackerNamespace: "tracker2", appId: nil, emitter: emitter2) { tracker in tracker.sessionContext = true tracker.foregroundTimeout = 5 tracker.backgroundTimeout = 5 } - _ = tracker2b.track(event) - guard let initialValue2b = tracker2b.session?.state?.sessionIndex else { return XCTFail() } - guard let previousId2b = tracker2b.session?.state?.previousSessionId else { return XCTFail() } + track(event, tracker2b) + guard let initialValue2b = tracker2b.session?.sessionIndex else { return XCTFail() } + guard let previousId2b = tracker2b.session?.previousSessionId else { return XCTFail() } // Check the new tracker session gets the data from the old tracker2 session XCTAssertEqual(initialValue2 + 2, initialValue2b) @@ -398,22 +401,22 @@ class TestSession: XCTestCase { cleanFile(withNamespace: "tracker") 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, dispatchQueue: MockDispatchQueueWrapper(label: "test")) { tracker in + let emitter = Emitter(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 500), namespace: "tracker") + let tracker = Tracker(trackerNamespace: "tracker", appId: nil, emitter: emitter) { tracker in tracker.sessionContext = true } let event = Structured(category: "c", action: "a") - _ = tracker.track(event) + track(event, tracker) - guard let sessionState = tracker.session?.state else { return XCTFail() } - XCTAssertEqual("sessionId", sessionState.previousSessionId) - XCTAssertEqual(124, sessionState.sessionIndex) - XCTAssertEqual("userId", sessionState.userId) - XCTAssertNotEqual("eventId", sessionState.firstEventId) + guard let session = tracker.session else { return XCTFail() } + XCTAssertEqual("sessionId", session.previousSessionId!) + XCTAssertEqual(124, session.sessionIndex!) + XCTAssertEqual("userId", session.userId) + XCTAssertNotEqual("eventId", session.firstEventId!) } func testIncrementsEventIndex() { - let session = Session(foregroundTimeout: 3, andBackgroundTimeout: 3, andTracker: nil) + let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) XCTAssertEqual(1, sessionContext?[kSPSessionEventIndex] as? Int) @@ -435,7 +438,7 @@ class TestSession: XCTestCase { } func testAnonymisesUserIdentifiers() { - let session = Session(foregroundTimeout: 3, andBackgroundTimeout: 3, andTracker: nil) + let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) _ = session.getDictWithEventId("event_1", eventTimestamp: 1654496481345, userAnonymisation: false) session.startNewSession() // create previous session ID reference @@ -468,4 +471,10 @@ class TestSession: XCTestCase { let userDefaults = UserDefaults.standard userDefaults.set(userId, forKey: kSPInstallationUserId) } + + private func track(_ event: Event, _ tracker: Tracker) { + InternalQueue.sync { + _ = tracker.track(event) + } + } } diff --git a/Tests/TestStateManager.swift b/Tests/TestStateManager.swift index 83d3690c2..10bad847e 100644 --- a/Tests/TestStateManager.swift +++ b/Tests/TestStateManager.swift @@ -244,22 +244,4 @@ class TestStateManager: XCTestCase { ) ) } - - @available(iOS 13, macOS 10.15, watchOS 6, tvOS 13, *) - func testConcurrentRemoveStateMachineWithAddOrReplaceStateMachine() async throws { - let stateManager = StateManager() - await withTaskGroup(of: Task.self) { group in - (1...100).forEach { element in - group.addTask { - Task.detached { - if Int(element).isMultiple(of: 2) { - _ = stateManager.removeStateMachine("MockStateMachine-»\(element-1)") - } else { - stateManager.addOrReplaceStateMachine(MockStateMachine("MockStateMachine-»\(element)")) - } - } - } - } - } - } } diff --git a/Tests/Legacy Tests/LegacyTestTracker.swift b/Tests/Tracker/TestTracker.swift similarity index 76% rename from Tests/Legacy Tests/LegacyTestTracker.swift rename to Tests/Tracker/TestTracker.swift index 695db9db5..0cd71644c 100644 --- a/Tests/Legacy Tests/LegacyTestTracker.swift +++ b/Tests/Tracker/TestTracker.swift @@ -11,17 +11,14 @@ // express or implied. See the Apache License Version 2.0 for the specific // language governing permissions and limitations there under. -//#pragma clang diagnostic push -//#pragma clang diagnostic ignored "-Wdeprecated-declarations" - import XCTest @testable import SnowplowTracker let TEST_SERVER_TRACKER = "http://www.notarealurl.com" -class LegacyTestTracker: XCTestCase { +class TestTracker: XCTestCase { func testTrackerSetup() { - let emitter = Emitter(urlEndpoint: "not-real.com") { emitter in } + let emitter = Emitter(namespace: "aNamespace", urlEndpoint: "not-real.com") let subject = Subject(platformContext: true, geoLocationContext: true) @@ -31,9 +28,46 @@ class LegacyTestTracker: XCTestCase { tracker.sessionContext = true } } + + func testTrackerPayload() { + let subject = Subject(platformContext: true, geoLocationContext: true) + let emitter = Emitter(namespace: "aNamespace", urlEndpoint: "not-real.com") + + let tracker = Tracker(trackerNamespace: "aNamespace", appId: "anAppId", emitter: emitter) { tracker in + tracker.subject = subject + tracker.devicePlatform = .general + tracker.base64Encoded = false + tracker.sessionContext = true + tracker.foregroundTimeout = 300 + tracker.backgroundTimeout = 150 + } + + let event = Structured(category: "Category", action: "Action") + let trackerEvent = TrackerEvent(event: event, state: nil) + + var payload = tracker.payload(with: trackerEvent) + + var payloadDict = payload!.dictionary + + XCTAssertEqual(payloadDict[kSPPlatform] as? String, devicePlatformToString(.general)) + XCTAssertEqual(payloadDict[kSPAppId] as? String, "anAppId") + XCTAssertEqual(payloadDict[kSPNamespace] as? String, "aNamespace") + + // Test setting variables to new values + + tracker.devicePlatform = .desktop + tracker.appId = "newAppId" + + payload = tracker.payload(with: trackerEvent) + payloadDict = payload!.dictionary + + XCTAssertEqual(payloadDict[kSPPlatform] as? String, "pc") + XCTAssertEqual(payloadDict[kSPAppId] as? String, "newAppId") + } func testTrackerBuilderAndOptions() { - let emitter = Emitter(urlEndpoint: TEST_SERVER_TRACKER) { emitter in} + let eventSink = EventSink() + let emitter = Emitter(namespace: "aNamespace", urlEndpoint: "http://localhost") let subject = Subject(platformContext: true, geoLocationContext: true) @@ -44,11 +78,12 @@ class LegacyTestTracker: XCTestCase { tracker.foregroundTimeout = 300 tracker.backgroundTimeout = 150 } + tracker.addOrReplace(stateMachine: eventSink.toStateMachine()) // Test builder setting properly XCTAssertNotNil(tracker.emitter) - XCTAssertEqual(tracker.emitter, emitter) + XCTAssertEqual(tracker.emitter.namespace, tracker.trackerNamespace) XCTAssertNotNil(tracker.subject) XCTAssertEqual(tracker.subject, subject) XCTAssertEqual(tracker.devicePlatform, Utilities.platform) @@ -62,16 +97,23 @@ class LegacyTestTracker: XCTestCase { tracker.pauseEventTracking() XCTAssertEqual(tracker.isTracking, false) - XCTAssertNil(tracker.track(Structured(category: "c", action: "a"))) + track(Structured(category: "c", action: "a"), tracker) tracker.resumeEventTracking() XCTAssertEqual(tracker.isTracking, true) + + // check that no events were tracked + Thread.sleep(forTimeInterval: 0.5) + XCTAssertEqual(eventSink.trackedEvents.count, 0) + + // tracks event after tracking resumed + track(Structured(category: "c", action: "a"), tracker) + Thread.sleep(forTimeInterval: 0.5) + XCTAssertEqual(eventSink.trackedEvents.count, 1) // Test setting variables to new values tracker.appId = "newAppId" XCTAssertEqual(tracker.appId, "newAppId") - tracker.trackerNamespace = "newNamespace" - XCTAssertEqual(tracker.trackerNamespace, "newNamespace") tracker.base64Encoded = true XCTAssertEqual(tracker.base64Encoded, true) tracker.devicePlatform = .general @@ -82,11 +124,6 @@ class LegacyTestTracker: XCTestCase { XCTAssertNotEqual(tracker.subject, subject) XCTAssertEqual(tracker.subject, subject2) - let emitter2 = Emitter(urlEndpoint: TEST_SERVER_TRACKER) { emitter in} - tracker.emitter = emitter2 - XCTAssertNotEqual(tracker.emitter, emitter) - XCTAssertEqual(tracker.emitter, emitter2) - // Test Session Switch on/off let oldSessionManager = tracker.session @@ -97,52 +134,11 @@ class LegacyTestTracker: XCTestCase { XCTAssertNotNil(tracker.session) XCTAssertFalse(oldSessionManager === tracker.session) } - - func testTrackerPayload() { - let emitter = Emitter(urlEndpoint: TEST_SERVER_TRACKER) { emitter in} - - let subject = Subject(platformContext: true, geoLocationContext: true) - - let tracker = Tracker(trackerNamespace: "aNamespace", appId: "anAppId", emitter: emitter) { tracker in - tracker.subject = subject - tracker.devicePlatform = .general - tracker.appId = "anAppId" - tracker.base64Encoded = false - tracker.sessionContext = true - tracker.foregroundTimeout = 300 - tracker.backgroundTimeout = 150 + + private func track(_ event: Event, _ tracker: Tracker) { + InternalQueue.sync { + _ = tracker.track(event) } - - let event = Structured(category: "Category", action: "Action") - let trackerEvent = TrackerEvent(event: event, state: nil) - var payload = tracker.payload(with: trackerEvent) - var payloadDict = payload!.dictionary - - XCTAssertEqual(payloadDict[kSPPlatform] as? String, devicePlatformToString(.general)) - XCTAssertEqual(payloadDict[kSPAppId] as? String, "anAppId") - XCTAssertEqual(payloadDict[kSPNamespace] as? String, "aNamespace") - - // Test setting variables to new values - - tracker.devicePlatform = .desktop - tracker.appId = "newAppId" - tracker.trackerNamespace = "newNamespace" - - payload = tracker.payload(with: trackerEvent) - payloadDict = payload!.dictionary - - XCTAssertEqual(payloadDict[kSPPlatform] as? String, "pc") - XCTAssertEqual(payloadDict[kSPAppId] as? String, "newAppId") - XCTAssertEqual(payloadDict[kSPNamespace] as? String, "newNamespace") } - func testEventIdNotDuplicated() { - let event = Structured(category: "Category", action: "Action") - let eventId = TrackerEvent(event: event, state: nil).eventId - XCTAssertNotNil(eventId) - let newEventId = TrackerEvent(event: event, state: nil).eventId - XCTAssertNotNil(newEventId) - XCTAssertNotEqual(eventId, newEventId) - } } -//#pragma clang diagnostic pop diff --git a/Tests/Utils/MockDispatchQueueWrapper.swift b/Tests/Tracker/TestTrackerEvent.swift similarity index 63% rename from Tests/Utils/MockDispatchQueueWrapper.swift rename to Tests/Tracker/TestTrackerEvent.swift index b4d39eee0..d187dacde 100644 --- a/Tests/Utils/MockDispatchQueueWrapper.swift +++ b/Tests/Tracker/TestTrackerEvent.swift @@ -11,22 +11,21 @@ // express or implied. See the Apache License Version 2.0 for the specific // language governing permissions and limitations there under. -import Foundation +import XCTest @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) +class TestTrackerEvent: XCTestCase { + + func testEventIdNotDuplicated() { + let event = Structured(category: "Category", action: "Action") + + let eventId = TrackerEvent(event: event, state: nil).eventId + XCTAssertNotNil(eventId) + + let newEventId = TrackerEvent(event: event, state: nil).eventId + XCTAssertNotNil(newEventId) + + XCTAssertNotEqual(eventId, newEventId) } - func async(_ callback: @escaping () -> Void) { - // execute synchronously! - queue.sync(execute: callback) - } } diff --git a/Tests/Utils/EventSink.swift b/Tests/Utils/EventSink.swift new file mode 100644 index 000000000..db78e8020 --- /dev/null +++ b/Tests/Utils/EventSink.swift @@ -0,0 +1,41 @@ +// 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 EventSink: ConfigurationProtocol, PluginIdentifiable, PluginFilterCallable { + + var identifier = "EventSink" + var filterConfiguration: SnowplowTracker.PluginFilterConfiguration? + private(set) var trackedEvents: [InspectableEvent] = [] + + init(callback: ((InspectableEvent) -> Void)? = nil) { + filterConfiguration = PluginFilterConfiguration { event in + self.trackedEvents.append(event) + if let callback = callback { + callback(event) + } + return false + } + } + + func toStateMachine() -> StateMachineProtocol { + return PluginStateMachine( + identifier: identifier, + entitiesConfiguration: nil, + afterTrackConfiguration: nil, + filterConfiguration: (schemas: nil, closure: filterConfiguration!.closure) + ) + } +} diff --git a/Tests/Utils/MockEventStore.swift b/Tests/Utils/MockEventStore.swift index 9c5053e60..eb7a93600 100644 --- a/Tests/Utils/MockEventStore.swift +++ b/Tests/Utils/MockEventStore.swift @@ -26,16 +26,12 @@ class MockEventStore: NSObject, EventStore { } func addEvent(_ payload: Payload) { - objc_sync_enter(self) lastInsertedRow += 1 logVerbose(message: "Add \(payload)") db[Int64(lastInsertedRow)] = payload - objc_sync_exit(self) } func removeEvent(withId storeId: Int64) -> Bool { - objc_sync_enter(self) - defer { objc_sync_exit(self) } logVerbose(message: "Remove \(storeId)") return db.removeValue(forKey: storeId) != nil } @@ -49,22 +45,16 @@ class MockEventStore: NSObject, EventStore { } func removeAllEvents() -> Bool { - objc_sync_enter(self) db.removeAll() lastInsertedRow = -1 - objc_sync_exit(self) return true } func count() -> UInt { - objc_sync_enter(self) - defer { objc_sync_exit(self) } return UInt(db.count) } func emittableEvents(withQueryLimit queryLimit: UInt) -> [EmitterEvent] { - objc_sync_enter(self) - defer { objc_sync_exit(self) } var eventIds: [Int64] = [] var events: [EmitterEvent] = [] for (key, obj) in db { diff --git a/Tests/Utils/MockTimer.swift b/Tests/Utils/MockTimer.swift index 47da12c45..78b622478 100644 --- a/Tests/Utils/MockTimer.swift +++ b/Tests/Utils/MockTimer.swift @@ -12,21 +12,27 @@ // language governing permissions and limitations there under. import Foundation +@testable import SnowplowTracker -class MockTimer: Timer { +class MockTimer: InternalQueueTimer { - var block: ((Timer) -> Void)! + var block: (() -> Void) + + init(block: @escaping () -> Void) { + self.block = block + } static var currentTimer: MockTimer! - override func fire() { - block(self) + func fire() { + InternalQueue.sync { + block() + } } - override open class func scheduledTimer(withTimeInterval interval: TimeInterval, - repeats: Bool, - block: @escaping (Timer) -> Void) -> Timer { - let mockTimer = MockTimer() + static func startTimer(_ interval: TimeInterval, + _ block: @escaping () -> Void) -> InternalQueueTimer { + let mockTimer = MockTimer(block: block) mockTimer.block = block MockTimer.currentTimer = mockTimer