diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 38d8a174a..2f5d20219 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,11 +5,14 @@ on: [push]
jobs:
podspec:
name: Lint Podspec for ${{ matrix.platform }}
- runs-on: macos-11
+ runs-on: macos-13
strategy:
matrix:
platform: [ios, ios, osx, tvos, watchos]
steps:
+ - uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: latest-stable
- uses: actions/checkout@v3
- name: Lint Podspec
run: pod lib lint --platforms=${{ matrix.platform }}
@@ -21,36 +24,36 @@ jobs:
fail-fast: false
matrix:
include:
- - name: "Integration (iOS 15.2, Xcode 13.2.1)"
+ - name: "Integration (iOS, Xcode 13.4)"
os: macos-12
- xcode-version: "13.2.1"
- sdk: iphonesimulator15.2
- destination: "platform=iOS Simulator,OS=15.2,name=iPhone 13"
+ xcode-version: 13.4
+ destination: "platform=iOS Simulator,name=iPhone 13"
target: IntegrationTests
- - name: "Unit (iOS 15.2, Xcode 13.2.1)"
+
+ - name: "Unit (iOS, Xcode 13.4)"
os: macos-12
- xcode-version: "13.2.1"
- sdk: iphonesimulator15.2
- destination: "platform=iOS Simulator,OS=15.2,name=iPhone 13"
+ xcode-version: 13.4
+ destination: "platform=iOS Simulator,name=iPhone 13"
target: Tests
- - name: "Unit (macOS 12.1, Xcode 13.2.1)"
+
+ - name: "Unit (macOS, Xcode 13.4)"
os: macos-12
- xcode-version: "13.2.1"
- sdk: macosx12.1
- destination: "platform=OS X"
+ xcode-version: 13.4
+ destination: "platform=macOS"
target: Tests
- - name: "Unit (watchOS 8.3, Xcode 13.2.1)"
+
+ - name: "Unit (watchOS, Xcode 13.4)"
os: macos-12
- xcode-version: "13.2.1"
- sdk: watchos8.3
- destination: "platform=watchOS Simulator,OS=8.3,name=Apple Watch Series 7 - 45mm"
+ xcode-version: 13.4
+ destination: "platform=watchOS Simulator,name=Apple Watch Series 7 - 45mm"
target: Tests
- - name: "Unit (tvOS 15.2, Xcode 13.2.1)"
+
+ - name: "Unit (tvOS, Xcode 13.4)"
os: macos-12
- xcode-version: "13.2.1"
- sdk: appletvsimulator15.2
- destination: "platform=tvOS Simulator,OS=15.2,name=Apple TV"
+ xcode-version: 13.4
+ destination: "platform=tvOS Simulator,name=Apple TV"
target: Tests
+
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -77,18 +80,26 @@ jobs:
run: while ! nc -z '0.0.0.0' 9090; do sleep 1; done
# -- Micro --
- - name: Select Xcode Version
- run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer
+ - uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: ${{ matrix.xcode-version }}
- name: Build & Test
run: |
set -o pipefail && xcodebuild \
-scheme SnowplowTracker \
- -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
@@ -100,8 +111,22 @@ jobs:
fail-fast: false
matrix:
version:
- - {ios: 15.5, iphone: iPhone 12 Pro, watchos: 8.5, watch: Apple Watch Series 5 - 44mm, macos: '12', xcode: 13.4}
- - {ios: 14.4, iphone: iPhone 8, watchos: 7.2, watch: Apple Watch Series 4 - 40mm, macos: '11', xcode: 12.4}
+ - {
+ ios: 15.5,
+ iphone: iPhone 12 Pro,
+ watchos: 8.5,
+ watch: Apple Watch Series 5 - 44mm,
+ macos: "12",
+ xcode: 13.4,
+ }
+ - {
+ ios: 14.4,
+ iphone: iPhone 8,
+ watchos: 7.2,
+ watch: Apple Watch Series 4 - 40mm,
+ macos: "11",
+ xcode: 12.4,
+ }
steps:
- name: Checkout
@@ -131,7 +156,14 @@ jobs:
fail-fast: false
matrix:
version:
- - {ios: '14.4', iphone: iPhone 12 Pro, watchos: '7.2', watch: Apple Watch Series 5 - 44mm, macos: '11', xcode: 12.4}
+ - {
+ ios: "14.4",
+ iphone: iPhone 12 Pro,
+ watchos: "7.2",
+ watch: Apple Watch Series 5 - 44mm,
+ macos: "11",
+ xcode: 12.4,
+ }
steps:
- name: Checkout
@@ -161,7 +193,14 @@ jobs:
fail-fast: false
matrix:
version:
- - {ios: '14.4', iphone: iPhone 11 Pro, watchos: '7.2', watch: Apple Watch Series 5 - 44mm, macos: '11', xcode: 12.4}
+ - {
+ ios: "14.4",
+ iphone: iPhone 11 Pro,
+ watchos: "7.2",
+ watch: Apple Watch Series 5 - 44mm,
+ macos: "11",
+ xcode: 12.4,
+ }
steps:
- name: Checkout
@@ -207,7 +246,7 @@ jobs:
fail-fast: false
matrix:
version:
- - {ios: 15.5, iphone: iPhone 12 Pro, macos: '12', xcode: 13.4}
+ - { ios: 15.5, iphone: iPhone 12 Pro, macos: "12", xcode: 13.4 }
steps:
- name: Checkout
diff --git a/CHANGELOG b/CHANGELOG
index a7808edb5..acdf8625a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,30 @@
+Version 6.0.0 (2024-02-01)
+--------------------------
+- Add screen engagement tracking of time spent and list items scrolled on a screen (#851)
+- Enable lifecycle autotracking by default (#852)
+- Add support for visionOS (#830)
+- Add VisionOS events and entities (#857)
+- Improve concurrency model using a single internal dispatch queue (#820)
+- Process tracked events on a serial background queue (#822)
+- Add API to decorate link with user/session info (#819)
+- Add configurable limit for the maximum age and number of events in the event store and remove old events before sending (#860)
+- Add request timeout to network connection and configuration (#836) thanks to @danigutierrezayuso
+- Expose event store from emitter controller to be able to remove all events from database (#834) thanks to @danigutierrezayuso
+- Make network requests serially in network connection (#846)
+- Change default buffer option to single (#849)
+- Flush events only when the buffer is full (#827) thanks to @danigutierrezayuso
+- Add SDK privacy manifest file (#811)
+- Remove available storage and total storage from platform context (#824)
+- Add an option to override platform context properties (#865)
+- Remove the use of the FMDB dependency in SQLiteEventStore (#823)
+- Return non-optional TrackerController instance from `createTracker` (#847) thanks to @Kymer
+- Enable representing self-describing data using Codable structs (#844)
+- Match BaseEvent entities API with Android tracker (#867)
+- Fix bundle path check to handle symbolic links (#858) thanks to @mylifeasdog
+- Set the platform event property to tv on tvOS and mobile on watchOS (#872)
+- Update copyright notices (#868)
+- Update CI build (#856)
+
Version 5.6.0 (2023-10-12)
--------------------------
Add configuration to send requests with user ID to a Focal Meter endpoint (#745)
diff --git a/Examples b/Examples
index 3d383f24e..1f45399ac 160000
--- a/Examples
+++ b/Examples
@@ -1 +1 @@
-Subproject commit 3d383f24e908727f709ae45cf408db0929c3c156
+Subproject commit 1f45399ac1076b930d5e0b58a7e82733a9025204
diff --git a/Integrationtests/TestTrackEventsToMicro.swift b/Integrationtests/TestTrackEventsToMicro.swift
index e5e4d8e9d..c57caffc2 100644
--- a/Integrationtests/TestTrackEventsToMicro.swift
+++ b/Integrationtests/TestTrackEventsToMicro.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -21,11 +21,12 @@ class TestTrackEventsToMicro: XCTestCase {
super.setUp()
let trackerConfig = TrackerConfiguration()
+ .screenEngagementAutotracking(false)
.logLevel(.debug)
tracker = Snowplow.createTracker(namespace: "testMicro-" + UUID().uuidString,
network: NetworkConfiguration(endpoint: Micro.endpoint),
- configurations: [trackerConfig])!
+ configurations: [trackerConfig])
wait(for: [Micro.reset()], timeout: Micro.timeout)
}
diff --git a/Integrationtests/Utils/Micro.swift b/Integrationtests/Utils/Micro.swift
index 878f8a629..973011973 100644
--- a/Integrationtests/Utils/Micro.swift
+++ b/Integrationtests/Utils/Micro.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/LICENSE b/LICENSE
index 8e001cd71..2d0a17745 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2023 Snowplow Analytics Ltd.
+ Copyright 2013-present Snowplow Analytics Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -199,4 +199,4 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-
\ No newline at end of file
+
diff --git a/Package.resolved b/Package.resolved
index 90cd82c2e..4fc33c6bc 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -1,34 +1,23 @@
{
- "object": {
- "pins": [
- {
- "package": "FMDB",
- "repositoryURL": "https://github.com/ccgus/fmdb",
- "state": {
- "branch": null,
- "revision": "61e51fde7f7aab6554f30ab061cc588b28a97d04",
- "version": "2.7.7"
- }
- },
- {
- "package": "Mocker",
- "repositoryURL": "https://github.com/WeTransfer/Mocker.git",
- "state": {
- "branch": null,
- "revision": "5d86f27a8f80d4ba388bc1a379a3c2289a1f3d18",
- "version": "2.6.0"
- }
- },
- {
- "package": "SwiftDocCPlugin",
- "repositoryURL": "https://github.com/apple/swift-docc-plugin",
- "state": {
- "branch": null,
- "revision": "3303b164430d9a7055ba484c8ead67a52f7b74f6",
- "version": "1.0.0"
- }
+ "pins" : [
+ {
+ "identity" : "mocker",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/WeTransfer/Mocker.git",
+ "state" : {
+ "revision" : "5d86f27a8f80d4ba388bc1a379a3c2289a1f3d18",
+ "version" : "2.6.0"
}
- ]
- },
- "version": 1
+ },
+ {
+ "identity" : "swift-docc-plugin",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-docc-plugin",
+ "state" : {
+ "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
+ "version" : "1.0.0"
+ }
+ }
+ ],
+ "version" : 2
}
diff --git a/Package.swift b/Package.swift
index e6d82bb48..2d7a1c745 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,9 +1,10 @@
-// swift-tools-version:5.2
+// swift-tools-version:5.3.0
import PackageDescription
let package = Package(
name: "SnowplowTracker",
+ defaultLocalization: "en",
platforms: [
.macOS("10.13"),
.iOS("11.0"),
@@ -16,13 +17,11 @@ let package = Package(
targets: ["SnowplowTracker"]),
],
dependencies: [
- .package(name: "FMDB", url: "https://github.com/ccgus/fmdb", from: "2.7.6"),
.package(name: "Mocker", url: "https://github.com/WeTransfer/Mocker.git", from: "2.5.4"),
],
targets: [
.target(
name: "SnowplowTracker",
- dependencies: ["FMDB"],
path: "./Sources"),
.testTarget(
name: "Tests",
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 000000000..1db62ef7d
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,46 @@
+// swift-tools-version:5.9
+
+import PackageDescription
+
+let package = Package(
+ name: "SnowplowTracker",
+ defaultLocalization: "en",
+ platforms: [
+ .macOS("10.13"),
+ .iOS("11.0"),
+ .tvOS("12.0"),
+ .watchOS("6.0"),
+ .visionOS("1.0")
+ ],
+ products: [
+ .library(
+ name: "SnowplowTracker",
+ targets: ["SnowplowTracker"]),
+ ],
+ dependencies: [
+ .package(name: "Mocker", url: "https://github.com/WeTransfer/Mocker.git", from: "2.5.4"),
+ ],
+ targets: [
+ .target(
+ name: "SnowplowTracker",
+ path: "./Sources"),
+ .testTarget(
+ name: "Tests",
+ dependencies: [
+ "SnowplowTracker",
+ "Mocker"
+ ],
+ path: "Tests"),
+ .testTarget(
+ name: "IntegrationTests",
+ dependencies: [
+ "SnowplowTracker"
+ ],
+ path: "IntegrationTests")
+ ]
+)
+#if swift(>=5.6)
+package.dependencies += [
+ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
+]
+#endif
diff --git a/PrivacyInfo.xcprivacy b/PrivacyInfo.xcprivacy
new file mode 100644
index 000000000..d1aa6938c
--- /dev/null
+++ b/PrivacyInfo.xcprivacy
@@ -0,0 +1,92 @@
+
+
+
+
+ NSPrivacyCollectedDataTypes
+
+
+ NSPrivacyCollectedDataType
+ NSPrivacyCollectedDataTypePreciseLocation
+ NSPrivacyCollectedDataTypeLinked
+
+ NSPrivacyCollectedDataTypeTracking
+
+ NSPrivacyCollectedDataTypePurposes
+
+ NSPrivacyCollectedDataTypePurposeAnalytics
+
+
+
+ NSPrivacyCollectedDataType
+ NSPrivacyCollectedDataTypeUserID
+ NSPrivacyCollectedDataTypeLinked
+
+ NSPrivacyCollectedDataTypeTracking
+
+ NSPrivacyCollectedDataTypePurposes
+
+ NSPrivacyCollectedDataTypePurposeAnalytics
+
+
+
+ NSPrivacyCollectedDataType
+ NSPrivacyCollectedDataTypeDeviceID
+ NSPrivacyCollectedDataTypeLinked
+
+ NSPrivacyCollectedDataTypeTracking
+
+ NSPrivacyCollectedDataTypePurposes
+
+ NSPrivacyCollectedDataTypePurposeAnalytics
+
+
+
+ NSPrivacyCollectedDataType
+ NSPrivacyCollectedDataTypeProductInteraction
+ NSPrivacyCollectedDataTypeLinked
+
+ NSPrivacyCollectedDataTypeTracking
+
+ NSPrivacyCollectedDataTypePurposes
+
+ NSPrivacyCollectedDataTypePurposeAnalytics
+
+
+
+ NSPrivacyCollectedDataType
+ NSPrivacyCollectedDataTypeCrashData
+ NSPrivacyCollectedDataTypeLinked
+
+ NSPrivacyCollectedDataTypeTracking
+
+ NSPrivacyCollectedDataTypePurposes
+
+ NSPrivacyCollectedDataTypePurposeAppFunctionality
+
+
+
+ NSPrivacyCollectedDataType
+ NSPrivacyCollectedDataTypeOtherDiagnosticData
+ NSPrivacyCollectedDataTypeLinked
+
+ NSPrivacyCollectedDataTypeTracking
+
+ NSPrivacyCollectedDataTypePurposes
+
+ NSPrivacyCollectedDataTypePurposeAppFunctionality
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryUserDefaults
+ NSPrivacyAccessedAPITypeReasons
+
+ CA92.1
+
+
+
+
+
diff --git a/README.md b/README.md
index 5b5d507d0..accfc392c 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ Some examples of demo apps instrumented with our iOS Tracker can be found in the
## Copyright and license
-The Snowplow iOS/macOS/tvOS/watchOS Tracker is copyright 2013-2023 Snowplow Analytics Ltd.
+The Snowplow iOS/macOS/tvOS/watchOS Tracker is copyright 2013-present Snowplow Analytics Ltd.
Licensed under the **[Apache License, Version 2.0][license]** (the "License");
you may not use this software except in compliance with the License.
diff --git a/SnowplowTracker.podspec b/SnowplowTracker.podspec
index 1d6f8435e..6703f926d 100644
--- a/SnowplowTracker.podspec
+++ b/SnowplowTracker.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SnowplowTracker"
- s.version = "5.6.0"
+ s.version = "6.0.0"
s.summary = "Snowplow event tracker for iOS, macOS, tvOS, watchOS for apps and games."
s.description = <<-DESC
Snowplow is a mobile and event analytics platform with a difference: rather than tell our users how they should analyze their data, we deliver their event-level data in their own data warehouse, on their own Amazon Redshift or Postgres database, so they can analyze it any way they choose. Snowplow mobile is used by data-savvy games companies and app developers to better understand their users and how they engage with their games and applications. Snowplow is open source using the business-friendly Apache License, Version 2.0 and scales horizontally to many billions of events.
@@ -24,8 +24,11 @@ Pod::Spec.new do |s|
s.ios.frameworks = 'CoreTelephony', 'UIKit', 'Foundation'
s.osx.frameworks = 'AppKit', 'Foundation'
s.tvos.frameworks = 'UIKit', 'Foundation'
+
+ if s.respond_to?(:visionos)
+ s.visionos.deployment_target = '1.0'
+ s.visionos.frameworks = 'UIKit', 'Foundation'
+ end
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
-
- s.dependency 'FMDB', '~> 2.7'
end
diff --git a/Sources/Core/Emitter/Emitter.swift b/Sources/Core/Emitter/Emitter.swift
index 70e6d0add..c3e0218db 100644
--- a/Sources/Core/Emitter/Emitter.swift
+++ b/Sources/Core/Emitter/Emitter.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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,75 @@ class Emitter: NSObject, EmitterEventProcessing {
/// Returns the number of events in the DB.
var dbCount: Int {
- return Int(eventStore?.count() ?? 0)
+ return Int(eventStore.count())
}
+ /// Limit for the maximum number of unsent events to keep in the event store.
+ var maxEventStoreSize: Int64 = EmitterDefaults.maxEventStoreSize
+
+ /// Limit for the maximum duration of how long events should be kept in the event store if they fail to be sent.
+ var maxEventStoreAge: TimeInterval = EmitterDefaults.maxEventStoreAge
+
// 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,
+ timeout: TimeInterval = EmitterDefaults.emitTimeout,
+ 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,
+ timeout: timeout
+ )
+ 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,177 +274,171 @@ 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).
+ /// This method will add the payload to the database and flush (send all events) when the buffer is full.
/// - 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)
+
+ if self.eventStore.count() >= self.bufferOption.rawValue {
+ 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.removeOldEvents()
+ 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 removeOldEvents() {
+ eventStore.removeOldEvents(
+ maxSize: maxEventStoreSize,
+ maxAge: maxEventStoreAge
+ )
+ }
+
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)
}
} else {
- var i = 0
- while i < events.count {
- var eventArray: [Payload] = []
- var indexArray: [Int64] = []
-
- let iUntil = min(i + bufferOption.rawValue, events.count)
- for j in i.. separate requests
+ if isOversize(payload, byteLimit: byteLimit) {
+ let request = Request(payload: payload, emitterEventId: emitterEventId, oversize: true)
+ requests.append(request)
}
-
- // Check if all payloads have been processed
- if eventArray.count != 0 {
- let request = Request(payloads: eventArray, emitterEventIds: indexArray)
+ // Events up to this one are oversize -> create request for them
+ else if isOversize(payload, byteLimit: byteLimit, previousPayloads: eventPayloads) {
+ let request = Request(payloads: eventPayloads, emitterEventIds: eventIds)
requests.append(request)
+
+ // Clear collection and build a new POST
+ eventPayloads = []
+ eventIds = []
+
+ // Build and store the request
+ eventPayloads.append(payload)
+ eventIds.append(emitterEventId)
+ }
+ // Add to the list of events for the request
+ else {
+ eventPayloads.append(payload)
+ eventIds.append(emitterEventId)
}
- i += bufferOption.rawValue
+ }
+
+ // Check if there are any remaining events not in a request
+ if !eventPayloads.isEmpty {
+ let request = Request(payloads: eventPayloads, emitterEventIds: eventIds)
+ requests.append(request)
}
}
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 +447,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/Emitter/EmitterControllerImpl.swift b/Sources/Core/Emitter/EmitterControllerImpl.swift
index 58b9baf56..da9414065 100644
--- a/Sources/Core/Emitter/EmitterControllerImpl.swift
+++ b/Sources/Core/Emitter/EmitterControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -115,6 +115,26 @@ class EmitterControllerImpl: Controller, EmitterController {
emitter.retryFailedRequests = newValue
}
}
+
+ var maxEventStoreSize: Int64 {
+ get { return emitter.maxEventStoreSize }
+ set {
+ dirtyConfig.maxEventStoreSize = newValue
+ emitter.maxEventStoreSize = newValue
+ }
+ }
+
+ var maxEventStoreAge: TimeInterval {
+ get { return emitter.maxEventStoreAge }
+ set {
+ dirtyConfig.maxEventStoreAge = newValue
+ emitter.maxEventStoreAge = newValue
+ }
+ }
+
+ var eventStore: EventStore {
+ return emitter.eventStore
+ }
// MARK: - Methods
diff --git a/Sources/Core/Emitter/EmitterEventProcessing.swift b/Sources/Core/Emitter/EmitterEventProcessing.swift
index c980f696d..a09a4d0b0 100644
--- a/Sources/Core/Emitter/EmitterEventProcessing.swift
+++ b/Sources/Core/Emitter/EmitterEventProcessing.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Events/ScreenEnd.swift b/Sources/Core/Events/ScreenEnd.swift
new file mode 100644
index 000000000..efcde1cf8
--- /dev/null
+++ b/Sources/Core/Events/ScreenEnd.swift
@@ -0,0 +1,26 @@
+// Copyright (c) 2013-present 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 ScreenEnd: SelfDescribingAbstract {
+
+ override var schema: String {
+ return kSPScreenEndSchema
+ }
+
+ override var payload: [String : Any] {
+ return [:]
+ }
+
+}
diff --git a/Sources/Core/GDPR/GDPRContext.swift b/Sources/Core/GDPR/GDPRContext.swift
index a65f5939b..61f086ad7 100644
--- a/Sources/Core/GDPR/GDPRContext.swift
+++ b/Sources/Core/GDPR/GDPRContext.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/GDPR/GDPRControllerImpl.swift b/Sources/Core/GDPR/GDPRControllerImpl.swift
index 47f6b8cc3..50b2ff909 100644
--- a/Sources/Core/GDPR/GDPRControllerImpl.swift
+++ b/Sources/Core/GDPR/GDPRControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/GlobalContexts/GlobalContextPluginConfiguration.swift b/Sources/Core/GlobalContexts/GlobalContextPluginConfiguration.swift
index 86c3c4b07..166ebf811 100644
--- a/Sources/Core/GlobalContexts/GlobalContextPluginConfiguration.swift
+++ b/Sources/Core/GlobalContexts/GlobalContextPluginConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/GlobalContexts/GlobalContextsControllerImpl.swift b/Sources/Core/GlobalContexts/GlobalContextsControllerImpl.swift
index bf60a991f..9986ac787 100644
--- a/Sources/Core/GlobalContexts/GlobalContextsControllerImpl.swift
+++ b/Sources/Core/GlobalContexts/GlobalContextsControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/GlobalContexts/SchemaRule.swift b/Sources/Core/GlobalContexts/SchemaRule.swift
index 286520ddd..31575665d 100644
--- a/Sources/Core/GlobalContexts/SchemaRule.swift
+++ b/Sources/Core/GlobalContexts/SchemaRule.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/InternalQueue/EcommerceControllerIQWrapper.swift b/Sources/Core/InternalQueue/EcommerceControllerIQWrapper.swift
new file mode 100644
index 000000000..6a7ac0604
--- /dev/null
+++ b/Sources/Core/InternalQueue/EcommerceControllerIQWrapper.swift
@@ -0,0 +1,39 @@
+// Copyright (c) 2013-present 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..06af6ca03
--- /dev/null
+++ b/Sources/Core/InternalQueue/EmitterControllerIQWrapper.swift
@@ -0,0 +1,107 @@
+// Copyright (c) 2013-present 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 } }
+ }
+
+ var maxEventStoreSize: Int64 {
+ get { return InternalQueue.sync { controller.maxEventStoreSize } }
+ set { InternalQueue.sync { controller.maxEventStoreSize = newValue } }
+ }
+
+ var maxEventStoreAge: TimeInterval {
+ get { return InternalQueue.sync { controller.maxEventStoreAge } }
+ set { InternalQueue.sync { controller.maxEventStoreAge = newValue } }
+ }
+
+ var eventStore: EventStore {
+ get { return InternalQueue.sync { EventStoreIQWrapper(eventStore: controller.eventStore) } }
+ }
+
+ // 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/EventStoreIQWrapper.swift b/Sources/Core/InternalQueue/EventStoreIQWrapper.swift
new file mode 100644
index 000000000..5a5204e37
--- /dev/null
+++ b/Sources/Core/InternalQueue/EventStoreIQWrapper.swift
@@ -0,0 +1,52 @@
+// Copyright (c) 2013-present 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 EventStoreIQWrapper: NSObject, EventStore {
+
+ private let eventStore: EventStore
+
+ init(eventStore: EventStore) {
+ self.eventStore = eventStore
+ }
+
+ func addEvent(_ payload: Payload) {
+ InternalQueue.sync { eventStore.addEvent(payload) }
+ }
+
+ func removeEvent(withId storeId: Int64) -> Bool {
+ return InternalQueue.sync { eventStore.removeEvent(withId: storeId) }
+ }
+
+ func removeEvents(withIds storeIds: [Int64]) -> Bool {
+ return InternalQueue.sync { eventStore.removeEvents(withIds: storeIds) }
+ }
+
+ func removeAllEvents() -> Bool {
+ return InternalQueue.sync { eventStore.removeAllEvents() }
+ }
+
+ func count() -> UInt {
+ return InternalQueue.sync { eventStore.count() }
+ }
+
+ func emittableEvents(withQueryLimit queryLimit: UInt) -> [EmitterEvent] {
+ return InternalQueue.sync { eventStore.emittableEvents(withQueryLimit: queryLimit) }
+ }
+
+ func removeOldEvents(maxSize: Int64, maxAge: TimeInterval) {
+ return InternalQueue.sync { eventStore.removeOldEvents(maxSize: maxSize, maxAge: maxAge) }
+ }
+
+}
diff --git a/Sources/Core/InternalQueue/GDPRControllerIQWrapper.swift b/Sources/Core/InternalQueue/GDPRControllerIQWrapper.swift
new file mode 100644
index 000000000..8301c4f81
--- /dev/null
+++ b/Sources/Core/InternalQueue/GDPRControllerIQWrapper.swift
@@ -0,0 +1,60 @@
+// Copyright (c) 2013-present 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..0ed4ed609
--- /dev/null
+++ b/Sources/Core/InternalQueue/GlobalContextsControllerIQWrapper.swift
@@ -0,0 +1,41 @@
+// Copyright (c) 2013-present 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..8265c97e6
--- /dev/null
+++ b/Sources/Core/InternalQueue/InternalQueue.swift
@@ -0,0 +1,56 @@
+// Copyright (c) 2013-present 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/InternalQueue/InternalQueueTimer.swift b/Sources/Core/InternalQueue/InternalQueueTimer.swift
new file mode 100644
index 000000000..dc8356f1f
--- /dev/null
+++ b/Sources/Core/InternalQueue/InternalQueueTimer.swift
@@ -0,0 +1,22 @@
+// Copyright (c) 2013-present 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 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..541bcaf22
--- /dev/null
+++ b/Sources/Core/InternalQueue/MediaControllerIQWrapper.swift
@@ -0,0 +1,59 @@
+// Copyright (c) 2013-present 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..4993b04ec
--- /dev/null
+++ b/Sources/Core/InternalQueue/MediaTrackingIQWrapper.swift
@@ -0,0 +1,68 @@
+// Copyright (c) 2013-present 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..e5032f60d
--- /dev/null
+++ b/Sources/Core/InternalQueue/NetworkControllerIQWrapper.swift
@@ -0,0 +1,46 @@
+// Copyright (c) 2013-present 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/InternalQueue/PluginsControllerIQWrapper.swift b/Sources/Core/InternalQueue/PluginsControllerIQWrapper.swift
new file mode 100644
index 000000000..dfce0ebfb
--- /dev/null
+++ b/Sources/Core/InternalQueue/PluginsControllerIQWrapper.swift
@@ -0,0 +1,35 @@
+// Copyright (c) 2013-present 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 PluginsControllerIQWrapper: PluginsController {
+
+ private let controller: PluginsController
+
+ init(controller: PluginsController) {
+ self.controller = controller
+ }
+
+ var identifiers: [String] {
+ return InternalQueue.sync { controller.identifiers }
+ }
+
+ 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..b4917e1da
--- /dev/null
+++ b/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift
@@ -0,0 +1,87 @@
+// Copyright (c) 2013-present 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..75df4a202
--- /dev/null
+++ b/Sources/Core/InternalQueue/SubjectControllerIQWrapper.swift
@@ -0,0 +1,118 @@
+// Copyright (c) 2013-present 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..9ab205425
--- /dev/null
+++ b/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift
@@ -0,0 +1,251 @@
+// Copyright (c) 2013-present 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
+ }
+
+ func decorateLink(_ url: URL) -> URL? {
+ return InternalQueue.sync { controller.decorateLink(url) }
+ }
+
+ func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration) -> URL? {
+ return InternalQueue.sync { controller.decorateLink(url, extendedParameters: extendedParameters) }
+ }
+
+ // 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 platformContextRetriever: PlatformContextRetriever? {
+ get { return InternalQueue.sync { controller.platformContextRetriever } }
+ set { InternalQueue.sync { controller.platformContextRetriever = 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 screenEngagementAutotracking: Bool {
+ get { return InternalQueue.sync { controller.screenEngagementAutotracking } }
+ set { InternalQueue.sync { controller.screenEngagementAutotracking = 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 immersiveSpaceContext: Bool {
+ get { return InternalQueue.sync { controller.immersiveSpaceContext } }
+ set { InternalQueue.sync { controller.immersiveSpaceContext = 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/Logger/Logger.swift b/Sources/Core/Logger/Logger.swift
index c593efc8e..e566b5ff7 100644
--- a/Sources/Core/Logger/Logger.swift
+++ b/Sources/Core/Logger/Logger.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Controllers/AVPlayerSubscription.swift b/Sources/Core/Media/Controllers/AVPlayerSubscription.swift
index a5c319570..dc950f76f 100644
--- a/Sources/Core/Media/Controllers/AVPlayerSubscription.swift
+++ b/Sources/Core/Media/Controllers/AVPlayerSubscription.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/MediaAdTracking.swift b/Sources/Core/Media/Controllers/MediaAdTracking.swift
index d06e69077..bbb00bf8e 100644
--- a/Sources/Core/Media/Controllers/MediaAdTracking.swift
+++ b/Sources/Core/Media/Controllers/MediaAdTracking.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Controllers/MediaControllerImpl.swift b/Sources/Core/Media/Controllers/MediaControllerImpl.swift
index 1f28ea991..f1ee02a5e 100644
--- a/Sources/Core/Media/Controllers/MediaControllerImpl.swift
+++ b/Sources/Core/Media/Controllers/MediaControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Controllers/MediaPingInterval.swift b/Sources/Core/Media/Controllers/MediaPingInterval.swift
index 03e8369b7..ad3236037 100644
--- a/Sources/Core/Media/Controllers/MediaPingInterval.swift
+++ b/Sources/Core/Media/Controllers/MediaPingInterval.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/MediaSessionTracking.swift b/Sources/Core/Media/Controllers/MediaSessionTracking.swift
index ba272bd01..222f10ac5 100644
--- a/Sources/Core/Media/Controllers/MediaSessionTracking.swift
+++ b/Sources/Core/Media/Controllers/MediaSessionTracking.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Controllers/MediaSessionTrackingStats.swift b/Sources/Core/Media/Controllers/MediaSessionTrackingStats.swift
index cff3f79c8..a821ccf2d 100644
--- a/Sources/Core/Media/Controllers/MediaSessionTrackingStats.swift
+++ b/Sources/Core/Media/Controllers/MediaSessionTrackingStats.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Controllers/MediaTrackingImpl.swift b/Sources/Core/Media/Controllers/MediaTrackingImpl.swift
index c71f06429..59079b4e1 100644
--- a/Sources/Core/Media/Controllers/MediaTrackingImpl.swift
+++ b/Sources/Core/Media/Controllers/MediaTrackingImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/Media/Entities/MediaSessionEntity.swift b/Sources/Core/Media/Entities/MediaSessionEntity.swift
index 13bf4797e..b2de7e5c6 100644
--- a/Sources/Core/Media/Entities/MediaSessionEntity.swift
+++ b/Sources/Core/Media/Entities/MediaSessionEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Events/MediaPercentProgressEvent.swift b/Sources/Core/Media/Events/MediaPercentProgressEvent.swift
index 102224836..b46447f1b 100644
--- a/Sources/Core/Media/Events/MediaPercentProgressEvent.swift
+++ b/Sources/Core/Media/Events/MediaPercentProgressEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Events/MediaPingEvent.swift b/Sources/Core/Media/Events/MediaPingEvent.swift
index 8d952c565..103142814 100644
--- a/Sources/Core/Media/Events/MediaPingEvent.swift
+++ b/Sources/Core/Media/Events/MediaPingEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/Events/MediaPlayerUpdatingEvent.swift b/Sources/Core/Media/Events/MediaPlayerUpdatingEvent.swift
index 681e118e5..19796c37a 100644
--- a/Sources/Core/Media/Events/MediaPlayerUpdatingEvent.swift
+++ b/Sources/Core/Media/Events/MediaPlayerUpdatingEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Media/MediaSchemata.swift b/Sources/Core/Media/MediaSchemata.swift
index 1e6338315..4e110c628 100644
--- a/Sources/Core/Media/MediaSchemata.swift
+++ b/Sources/Core/Media/MediaSchemata.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/NetworkConnection/NetworkControllerImpl.swift b/Sources/Core/NetworkConnection/NetworkControllerImpl.swift
index 9d026540a..ac7ec82bc 100644
--- a/Sources/Core/NetworkConnection/NetworkControllerImpl.swift
+++ b/Sources/Core/NetworkConnection/NetworkControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/RemoteConfiguration/RemoteConfigurationBundle.swift b/Sources/Core/RemoteConfiguration/RemoteConfigurationBundle.swift
index eef89efb2..6b9c34a1a 100644
--- a/Sources/Core/RemoteConfiguration/RemoteConfigurationBundle.swift
+++ b/Sources/Core/RemoteConfiguration/RemoteConfigurationBundle.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/RemoteConfiguration/RemoteConfigurationCache.swift b/Sources/Core/RemoteConfiguration/RemoteConfigurationCache.swift
index 6f66ee2af..171c76f5a 100644
--- a/Sources/Core/RemoteConfiguration/RemoteConfigurationCache.swift
+++ b/Sources/Core/RemoteConfiguration/RemoteConfigurationCache.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -68,7 +68,23 @@ class RemoteConfigurationCache: NSObject {
let data = try? Data(contentsOf: cacheFileUrl) else { return }
if #available(iOS 12, tvOS 12, watchOS 5, macOS 10.14, *) {
do {
- configuration = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? RemoteConfigurationBundle
+ configuration = try NSKeyedUnarchiver.unarchivedObject(
+ ofClasses: [
+ ConfigurationBundle.self,
+ RemoteConfigurationBundle.self,
+ NetworkConfiguration.self,
+ TrackerConfiguration.self,
+ SubjectConfiguration.self,
+ SessionConfiguration.self,
+ EmitterConfiguration.self,
+ SPSize.self,
+ NSString.self,
+ NSArray.self,
+ NSDictionary.self,
+ NSNumber.self
+ ],
+ from: data
+ ) as? RemoteConfigurationBundle
} catch let error {
logError(message: String(format: "Exception on getting configuration from cache: %@", error.localizedDescription))
configuration = nil
diff --git a/Sources/Core/RemoteConfiguration/RemoteConfigurationFetcher.swift b/Sources/Core/RemoteConfiguration/RemoteConfigurationFetcher.swift
index 48da6cefc..fda14cf70 100644
--- a/Sources/Core/RemoteConfiguration/RemoteConfigurationFetcher.swift
+++ b/Sources/Core/RemoteConfiguration/RemoteConfigurationFetcher.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/RemoteConfiguration/RemoteConfigurationProvider.swift b/Sources/Core/RemoteConfiguration/RemoteConfigurationProvider.swift
index d2221a1fe..f7dfd13f8 100644
--- a/Sources/Core/RemoteConfiguration/RemoteConfigurationProvider.swift
+++ b/Sources/Core/RemoteConfiguration/RemoteConfigurationProvider.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/ScreenViewTracking/ListItemViewModifier.swift b/Sources/Core/ScreenViewTracking/ListItemViewModifier.swift
new file mode 100644
index 000000000..6dac60176
--- /dev/null
+++ b/Sources/Core/ScreenViewTracking/ListItemViewModifier.swift
@@ -0,0 +1,54 @@
+// Copyright (c) 2013-present 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.
+
+#if canImport(SwiftUI)
+
+import SwiftUI
+import Foundation
+
+@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, *)
+@available(watchOS, unavailable)
+internal struct ListItemViewModifier: ViewModifier {
+ let index: Int
+ let itemsCount: Int?
+ let trackerNamespace: String?
+
+ /// Get tracker by namespace if configured, otherwise return the default tracker
+ private var tracker: TrackerController? {
+ if let namespace = trackerNamespace {
+ return Snowplow.tracker(namespace: namespace)
+ } else {
+ return Snowplow.defaultTracker()
+ }
+ }
+
+ /// Modifies the view to track the list item view when it appears
+ func body(content: Content) -> some View {
+ content.onAppear {
+ trackListItemView()
+ }
+ }
+
+ func trackListItemView() {
+ let event = ListItemView(index: index)
+ event.itemsCount = itemsCount
+
+ if let tracker = tracker {
+ _ = tracker.track(event)
+ } else {
+ logError(message: "List item view not tracked – tracker not initialized.")
+ }
+ }
+}
+
+#endif
diff --git a/Sources/Core/ScreenViewTracking/ScreenState.swift b/Sources/Core/ScreenViewTracking/ScreenState.swift
index 93ae5b4f5..b3178b19c 100644
--- a/Sources/Core/ScreenViewTracking/ScreenState.swift
+++ b/Sources/Core/ScreenViewTracking/ScreenState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/ScreenViewTracking/ScreenStateMachine.swift b/Sources/Core/ScreenViewTracking/ScreenStateMachine.swift
index c6a184b22..a77629df2 100644
--- a/Sources/Core/ScreenViewTracking/ScreenStateMachine.swift
+++ b/Sources/Core/ScreenViewTracking/ScreenStateMachine.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -17,6 +17,10 @@ class ScreenStateMachine: StateMachineProtocol {
static var identifier: String { return "ScreenContext" }
var identifier: String { return ScreenStateMachine.identifier }
+ var subscribedEventSchemasForEventsBefore: [String] {
+ return []
+ }
+
var subscribedEventSchemasForTransitions: [String] {
return [kSPScreenViewSchema]
}
@@ -37,6 +41,10 @@ class ScreenStateMachine: StateMachineProtocol {
return []
}
+ func eventsBefore(event: Event) -> [Event]? {
+ return nil
+ }
+
func transition(from event: Event, state currentState: State?) -> State? {
if let screenView = event as? ScreenView {
let newState: ScreenState = screenState(from: screenView)
diff --git a/Sources/Core/ScreenViewTracking/ScreenSummaryState.swift b/Sources/Core/ScreenViewTracking/ScreenSummaryState.swift
new file mode 100644
index 000000000..2074ca5b2
--- /dev/null
+++ b/Sources/Core/ScreenViewTracking/ScreenSummaryState.swift
@@ -0,0 +1,95 @@
+// Copyright (c) 2013-present 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 ScreenSummaryState: State {
+
+ static var dateGenerator: () -> TimeInterval = { Date().timeIntervalSince1970 }
+
+ private var lastUpdateTimestamp: TimeInterval = ScreenSummaryState.dateGenerator()
+ var foregroundSeconds: TimeInterval = 0
+ var backgroundSeconds: TimeInterval = 0
+ var lastItemIndex: Int?
+ var itemsCount: Int?
+ var minYOffset: Int?
+ var maxYOffset: Int?
+ var minXOffset: Int?
+ var maxXOffset: Int?
+ var contentHeight: Int?
+ var contentWidth: Int?
+
+ var data: [String: Any] {
+ var data: [String: Any] = [
+ "foreground_sec": round(foregroundSeconds * 100) / 100,
+ "background_sec": round(backgroundSeconds * 100) / 100
+ ]
+ if let lastItemIndex = lastItemIndex { data["last_item_index"] = lastItemIndex }
+ if let itemsCount = itemsCount { data["items_count"] = itemsCount }
+ if let minXOffset = minXOffset { data["min_x_offset"] = minXOffset }
+ if let maxXOffset = maxXOffset { data["max_x_offset"] = maxXOffset }
+ if let minYOffset = minYOffset { data["min_y_offset"] = minYOffset }
+ if let maxYOffset = maxYOffset { data["max_y_offset"] = maxYOffset }
+ if let contentHeight = contentHeight { data["content_height"] = contentHeight }
+ if let contentWidth = contentWidth { data["content_width"] = contentWidth }
+ return data
+ }
+
+ func updateTransitionToForeground() {
+ let currentTimestamp = ScreenSummaryState.dateGenerator()
+
+ backgroundSeconds += currentTimestamp - lastUpdateTimestamp
+ lastUpdateTimestamp = currentTimestamp
+ }
+
+ func updateTransitionToBackground() {
+ let currentTimestamp = ScreenSummaryState.dateGenerator()
+
+ foregroundSeconds += currentTimestamp - lastUpdateTimestamp
+ lastUpdateTimestamp = currentTimestamp
+ }
+
+ func updateForScreenEnd() {
+ let currentTimestamp = ScreenSummaryState.dateGenerator()
+
+ foregroundSeconds += currentTimestamp - lastUpdateTimestamp
+ lastUpdateTimestamp = currentTimestamp
+ }
+
+ func updateWithListItemView(_ event: ListItemView) {
+ lastItemIndex = max(event.index, lastItemIndex ?? 0)
+ if let totalItems = event.itemsCount {
+ self.itemsCount = max(totalItems, self.itemsCount ?? 0)
+ }
+ }
+
+ func updateWithScrollChanged(_ event: ScrollChanged) {
+ if let yOffset = event.yOffset {
+ var maxYOffset = yOffset
+ if let viewHeight = event.viewHeight { maxYOffset += viewHeight }
+
+ minYOffset = min(yOffset, minYOffset ?? yOffset)
+ self.maxYOffset = max(maxYOffset, self.maxYOffset ?? maxYOffset)
+ }
+ if let xOffset = event.xOffset {
+ var maxXOffset = xOffset
+ if let viewWidth = event.viewWidth { maxXOffset += viewWidth }
+
+ minXOffset = min(xOffset, minXOffset ?? xOffset)
+ self.maxXOffset = max(maxXOffset, self.maxXOffset ?? maxXOffset)
+ }
+ if let height = event.contentHeight { contentHeight = max(height, contentHeight ?? 0) }
+ if let width = event.contentWidth { contentWidth = max(width, contentWidth ?? 0) }
+ }
+
+}
diff --git a/Sources/Core/ScreenViewTracking/ScreenSummaryStateMachine.swift b/Sources/Core/ScreenViewTracking/ScreenSummaryStateMachine.swift
new file mode 100644
index 000000000..fe3388cf2
--- /dev/null
+++ b/Sources/Core/ScreenViewTracking/ScreenSummaryStateMachine.swift
@@ -0,0 +1,93 @@
+// Copyright (c) 2013-present 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 ScreenSummaryStateMachine: StateMachineProtocol {
+ static var identifier: String { return "ScreenSummaryContext" }
+ var identifier: String { return ScreenSummaryStateMachine.identifier }
+
+ var subscribedEventSchemasForEventsBefore: [String] {
+ return [kSPScreenViewSchema]
+ }
+
+ var subscribedEventSchemasForTransitions: [String] {
+ return [kSPScreenViewSchema, kSPScreenEndSchema, kSPForegroundSchema, kSPBackgroundSchema, kSPListItemViewSchema, kSPScrollChangedSchema]
+ }
+
+ var subscribedEventSchemasForEntitiesGeneration: [String] {
+ return [kSPScreenEndSchema, kSPForegroundSchema, kSPBackgroundSchema]
+ }
+
+ var subscribedEventSchemasForPayloadUpdating: [String] {
+ return []
+ }
+
+ var subscribedEventSchemasForAfterTrackCallback: [String] {
+ return []
+ }
+
+ var subscribedEventSchemasForFiltering: [String] {
+ return [kSPListItemViewSchema, kSPScrollChangedSchema, kSPScreenEndSchema]
+ }
+
+ func eventsBefore(event: Event) -> [Event]? {
+ return [ScreenEnd()]
+ }
+
+ func transition(from event: Event, state currentState: State?) -> State? {
+ if event is ScreenView {
+ return ScreenSummaryState()
+ }
+ else if let state = currentState as? ScreenSummaryState {
+ switch event {
+ case is Foreground:
+ state.updateTransitionToForeground()
+ case is Background:
+ state.updateTransitionToBackground()
+ case is ScreenEnd:
+ state.updateForScreenEnd()
+ case let itemView as ListItemView:
+ state.updateWithListItemView(itemView)
+ case let scrollChanged as ScrollChanged:
+ state.updateWithScrollChanged(scrollChanged)
+ default:
+ break
+ }
+ }
+ return currentState
+ }
+
+ func entities(from event: InspectableEvent, state: State?) -> [SelfDescribingJson]? {
+ guard let state = state as? ScreenSummaryState else { return nil }
+
+ return [
+ SelfDescribingJson(schema: kSPScreenSummarySchema, andData: state.data)
+ ]
+ }
+
+ func payloadValues(from event: InspectableEvent, state: State?) -> [String : Any]? {
+ return nil
+ }
+
+ func filter(event: InspectableEvent, state: State?) -> Bool? {
+ if event.schema == kSPScreenEndSchema {
+ return state != nil
+ }
+ // do not track list item view or scroll changed events
+ return false
+ }
+
+ func afterTrack(event: InspectableEvent) {
+ }
+}
diff --git a/Sources/Core/ScreenViewTracking/ScreenViewModifier.swift b/Sources/Core/ScreenViewTracking/ScreenViewModifier.swift
index d721a1b95..b9fe09350 100644
--- a/Sources/Core/ScreenViewTracking/ScreenViewModifier.swift
+++ b/Sources/Core/ScreenViewTracking/ScreenViewModifier.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/ScreenViewTracking/UIKitScreenViewTracking.swift b/Sources/Core/ScreenViewTracking/UIKitScreenViewTracking.swift
index 389cec01f..34ec8e806 100644
--- a/Sources/Core/ScreenViewTracking/UIKitScreenViewTracking.swift
+++ b/Sources/Core/ScreenViewTracking/UIKitScreenViewTracking.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -69,9 +69,15 @@ extension UIViewController {
@objc func sp_viewDidAppear(_ animated: Bool) {
sp_viewDidAppear(animated)
- let bundle = Bundle(for: self.classForCoder)
- if !bundle.bundlePath.hasPrefix(Bundle.main.bundlePath) {
- // Ignore view controllers that don't start with the main bundle path
+ let bundleURL = Bundle(for: self.classForCoder).bundleURL
+ let mainBundleURL = Bundle.main.bundleURL
+
+ // Resolve any symbolic links and standardize the file paths
+ let resolvedBundlePath = bundleURL.resolvingSymlinksInPath().path
+ let resolvedMainBundlePath = mainBundleURL.resolvingSymlinksInPath().path
+
+ // Ignore view controllers that don't start with the main bundle path
+ guard resolvedBundlePath.hasPrefix(resolvedMainBundlePath) else {
return
}
@@ -81,7 +87,7 @@ extension UIViewController {
// Construct userInfo
var userInfo: [AnyHashable : Any] = [:]
userInfo["viewControllerClassName"] = String(describing: self.classForCoder)
- userInfo["topViewControllerClassName"] = String(describing: top.self.classForCoder)
+ userInfo["topViewControllerClassName"] = String(describing: top.classForCoder)
// `name` is set to snowplowId class instance variable if it exists (hence no @"id" in userInfo)
userInfo["name"] = _SP_getName(self) ?? _SP_getName(top) ?? "Unknown"
diff --git a/Sources/Core/Session/Session.swift b/Sources/Core/Session/Session.swift
index 85a51863c..10daa9b65 100644
--- a/Sources/Core/Session/Session.swift
+++ b/Sources/Core/Session/Session.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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..702c953c0 100644
--- a/Sources/Core/Session/SessionControllerImpl.swift
+++ b/Sources/Core/Session/SessionControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/DeepLinkState.swift b/Sources/Core/StateMachine/DeepLinkState.swift
index e6540ffee..c787faf0c 100644
--- a/Sources/Core/StateMachine/DeepLinkState.swift
+++ b/Sources/Core/StateMachine/DeepLinkState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/StateMachine/DeepLinkStateMachine.swift b/Sources/Core/StateMachine/DeepLinkStateMachine.swift
index 9f551eb1f..0d956ebfc 100644
--- a/Sources/Core/StateMachine/DeepLinkStateMachine.swift
+++ b/Sources/Core/StateMachine/DeepLinkStateMachine.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -29,6 +29,10 @@ class DeepLinkStateMachine: StateMachineProtocol {
static var identifier: String { return "DeepLinkContext" }
var identifier: String { return DeepLinkStateMachine.identifier }
+ var subscribedEventSchemasForEventsBefore: [String] {
+ return []
+ }
+
var subscribedEventSchemasForTransitions: [String] {
return [DeepLinkReceived.schema, kSPScreenViewSchema]
}
@@ -49,6 +53,10 @@ class DeepLinkStateMachine: StateMachineProtocol {
return []
}
+ func eventsBefore(event: Event) -> [Event]? {
+ return nil
+ }
+
func transition(from event: Event, state: State?) -> State? {
if let dlEvent = event as? DeepLinkReceived {
return DeepLinkState(url: dlEvent.url, referrer: dlEvent.referrer)
diff --git a/Sources/Core/StateMachine/ImmersiveSpaceState.swift b/Sources/Core/StateMachine/ImmersiveSpaceState.swift
new file mode 100644
index 000000000..3c4a06e4a
--- /dev/null
+++ b/Sources/Core/StateMachine/ImmersiveSpaceState.swift
@@ -0,0 +1,30 @@
+// Copyright (c) 2013-present 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 ImmersiveSpaceState: State {
+ var dismissEventTracked = false
+
+ var id: String
+ var viewId: UUID?
+ var immersionStyle: ImmersionStyle?
+ var upperLimbVisibility: UpperLimbVisibility?
+
+ init(id: String, viewId: UUID? = nil, immersionStyle: ImmersionStyle? = nil, upperLimbVisibility: UpperLimbVisibility? = nil) {
+ self.id = id
+ self.viewId = viewId
+ self.immersionStyle = immersionStyle
+ self.upperLimbVisibility = upperLimbVisibility
+ }
+}
diff --git a/Sources/Core/StateMachine/ImmersiveSpaceStateMachine.swift b/Sources/Core/StateMachine/ImmersiveSpaceStateMachine.swift
new file mode 100644
index 000000000..f7a9555b7
--- /dev/null
+++ b/Sources/Core/StateMachine/ImmersiveSpaceStateMachine.swift
@@ -0,0 +1,107 @@
+// Copyright (c) 2013-present 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 ImmersiveSpaceStateMachine: StateMachineProtocol {
+
+ static var identifier: String { return "ImmersiveSpace" }
+ var identifier: String { return ImmersiveSpaceStateMachine.identifier }
+
+ var subscribedEventSchemasForEventsBefore: [String] {
+ return []
+ }
+
+ var subscribedEventSchemasForTransitions: [String] {
+ return [swiftuiOpenImmersiveSpaceSchema, swiftuiDismissImmersiveSpaceSchema]
+ }
+
+ var subscribedEventSchemasForEntitiesGeneration: [String] {
+ return ["*"]
+ }
+
+ var subscribedEventSchemasForPayloadUpdating: [String] {
+ return []
+ }
+
+ var subscribedEventSchemasForAfterTrackCallback: [String] {
+ return []
+ }
+
+ var subscribedEventSchemasForFiltering: [String] {
+ return []
+ }
+
+ func eventsBefore(event: Event) -> [Event]? {
+ return nil
+ }
+
+ func transition(from event: Event, state: State?) -> State? {
+ if let e = event as? OpenImmersiveSpaceEvent {
+ return ImmersiveSpaceState(
+ id: e.id,
+ viewId: e.viewId,
+ immersionStyle: e.immersionStyle,
+ upperLimbVisibility: e.upperLimbVisibility
+ )
+ } else {
+ if let s = state as? ImmersiveSpaceState {
+ if s.dismissEventTracked {
+ return nil
+ }
+ // state persists for the first Dismiss event after an Open
+ let currentState = ImmersiveSpaceState(
+ id: s.id,
+ viewId: s.viewId,
+ immersionStyle: s.immersionStyle,
+ upperLimbVisibility: s.upperLimbVisibility
+ )
+ currentState.dismissEventTracked = true
+ return currentState
+ }
+ }
+ return nil
+ }
+
+ func entities(from event: InspectableEvent, state: State?) -> [SelfDescribingJson]? {
+ // the open event already has the entity
+ if event.schema == swiftuiOpenImmersiveSpaceSchema {
+ return nil
+ }
+
+ if let s = state as? ImmersiveSpaceState {
+ if s.dismissEventTracked == true && event.schema != swiftuiDismissImmersiveSpaceSchema {
+ return nil
+ }
+ let entity = ImmersiveSpaceEntity(
+ id: s.id,
+ viewId: s.viewId,
+ immersionStyle: s.immersionStyle,
+ upperLimbVisibility: s.upperLimbVisibility
+ )
+ return [entity]
+ }
+ return nil
+ }
+
+ func payloadValues(from event: InspectableEvent, state: State?) -> [String : Any]? {
+ return nil
+ }
+
+ func afterTrack(event: InspectableEvent) {
+ }
+
+ func filter(event: InspectableEvent, state: State?) -> Bool? {
+ return nil
+ }
+}
diff --git a/Sources/Core/StateMachine/LifecycleState.swift b/Sources/Core/StateMachine/LifecycleState.swift
index 79f67d05c..87f342497 100644
--- a/Sources/Core/StateMachine/LifecycleState.swift
+++ b/Sources/Core/StateMachine/LifecycleState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/StateMachine/LifecycleStateMachine.swift b/Sources/Core/StateMachine/LifecycleStateMachine.swift
index 8bf143295..774041d0b 100644
--- a/Sources/Core/StateMachine/LifecycleStateMachine.swift
+++ b/Sources/Core/StateMachine/LifecycleStateMachine.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -17,10 +17,16 @@ class LifecycleStateMachine: StateMachineProtocol {
static var identifier: String { return "Lifecycle" }
var identifier: String { return LifecycleStateMachine.identifier }
+ var subscribedEventSchemasForEventsBefore: [String] = []
+
+ func eventsBefore(event: Event) -> [Event]? {
+ return nil
+ }
+
var subscribedEventSchemasForTransitions: [String] {
return [kSPBackgroundSchema, kSPForegroundSchema]
}
-
+
func transition(from event: Event, state currentState: State?) -> State? {
if let e = event as? Foreground {
return LifecycleState(asForegroundWithIndex: e.index)
diff --git a/Sources/Core/StateMachine/PluginStateMachine.swift b/Sources/Core/StateMachine/PluginStateMachine.swift
index 67149ad2f..3edb365ad 100644
--- a/Sources/Core/StateMachine/PluginStateMachine.swift
+++ b/Sources/Core/StateMachine/PluginStateMachine.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -35,6 +35,14 @@ class PluginStateMachine: StateMachineProtocol {
self.filterConfiguration = filterConfiguration
}
+ var subscribedEventSchemasForEventsBefore: [String] {
+ return []
+ }
+
+ func eventsBefore(event: Event) -> [Event]? {
+ return nil
+ }
+
var subscribedEventSchemasForTransitions: [String] {
return []
}
diff --git a/Sources/Core/StateMachine/State.swift b/Sources/Core/StateMachine/State.swift
index e9ffbce97..fb0f8f96d 100644
--- a/Sources/Core/StateMachine/State.swift
+++ b/Sources/Core/StateMachine/State.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/StateMachine/StateFuture.swift b/Sources/Core/StateMachine/StateFuture.swift
index c6d9f73ff..dce138afa 100644
--- a/Sources/Core/StateMachine/StateFuture.swift
+++ b/Sources/Core/StateMachine/StateFuture.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/StateMachineEvent.swift b/Sources/Core/StateMachine/StateMachineEvent.swift
index 0ce2eceab..35752be6a 100644
--- a/Sources/Core/StateMachine/StateMachineEvent.swift
+++ b/Sources/Core/StateMachine/StateMachineEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/StateMachine/StateMachineProtocol.swift b/Sources/Core/StateMachine/StateMachineProtocol.swift
index 7dbff9499..93982b564 100644
--- a/Sources/Core/StateMachine/StateMachineProtocol.swift
+++ b/Sources/Core/StateMachine/StateMachineProtocol.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -15,12 +15,16 @@ import Foundation
protocol StateMachineProtocol {
var identifier: String { get }
+ var subscribedEventSchemasForEventsBefore: [String] { get }
var subscribedEventSchemasForTransitions: [String] { get }
var subscribedEventSchemasForEntitiesGeneration: [String] { get }
var subscribedEventSchemasForPayloadUpdating: [String] { get }
var subscribedEventSchemasForAfterTrackCallback: [String] { get }
var subscribedEventSchemasForFiltering: [String] { get }
+ /// Only available for self-describing events (inheriting from SelfDescribingAbstract)
+ func eventsBefore(event: Event) -> [Event]?
+
/// Only available for self-describing events (inheriting from SelfDescribingAbstract)
func transition(from event: Event, state: State?) -> State?
diff --git a/Sources/Core/StateMachine/StateManager.swift b/Sources/Core/StateMachine/StateManager.swift
index 4e5e910b0..cd30414a0 100644
--- a/Sources/Core/StateMachine/StateManager.swift
+++ b/Sources/Core/StateMachine/StateManager.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -20,12 +20,10 @@ class StateManager {
private var eventSchemaToPayloadUpdater: [String : [StateMachineProtocol]] = [:]
private var eventSchemaToAfterTrackCallback: [String : [StateMachineProtocol]] = [:]
private var eventSchemaToFilter: [String : [StateMachineProtocol]] = [:]
+ private var eventSchemaToEventsBefore: [String : [StateMachineProtocol]] = [:]
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
@@ -53,12 +51,13 @@ class StateManager {
toSchemaRegistry: &eventSchemaToFilter,
schemas: stateMachine.subscribedEventSchemasForFiltering,
stateMachine: stateMachine)
+ add(
+ toSchemaRegistry: &eventSchemaToEventsBefore,
+ schemas: stateMachine.subscribedEventSchemasForEventsBefore,
+ stateMachine: stateMachine)
}
func removeStateMachine(_ stateMachineIdentifier: String) -> Bool {
- objc_sync_enter(self)
- defer { objc_sync_exit(self) }
-
guard let stateMachine = identifierToStateMachine[stateMachineIdentifier] else {
return false
}
@@ -84,13 +83,14 @@ class StateManager {
fromSchemaRegistry: &eventSchemaToFilter,
schemas: stateMachine.subscribedEventSchemasForFiltering,
stateMachine: stateMachine)
+ remove(
+ fromSchemaRegistry: &eventSchemaToEventsBefore,
+ schemas: stateMachine.subscribedEventSchemasForEventsBefore,
+ stateMachine: stateMachine)
return true
}
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 +121,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["*"] ?? [])
@@ -136,11 +133,24 @@ class StateManager {
}
return true
}
+
+ func eventsBefore(forProcessedEvent event: Event) -> [Event] {
+ var result: [Event] = []
+ guard let sdEvent = event as? SelfDescribingAbstract else { return result }
+
+ let schema = sdEvent.schema
+ var stateMachines = eventSchemaToEventsBefore[schema] ?? []
+ stateMachines.append(contentsOf: eventSchemaToEventsBefore["*"] ?? [])
+
+ for stateMachine in stateMachines {
+ if let eventsBefore = stateMachine.eventsBefore(event: event) {
+ result.append(contentsOf: eventsBefore)
+ }
+ }
+ return result
+ }
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 +166,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 +184,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..9ab222332 100644
--- a/Sources/Core/StateMachine/TrackerState.swift
+++ b/Sources/Core/StateMachine/TrackerState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/StateMachine/TrackerStateSnapshot.swift b/Sources/Core/StateMachine/TrackerStateSnapshot.swift
index d178aef08..7a7fb1742 100644
--- a/Sources/Core/StateMachine/TrackerStateSnapshot.swift
+++ b/Sources/Core/StateMachine/TrackerStateSnapshot.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Storage/Database.swift b/Sources/Core/Storage/Database.swift
new file mode 100644
index 000000000..ab711aef1
--- /dev/null
+++ b/Sources/Core/Storage/Database.swift
@@ -0,0 +1,197 @@
+// Copyright (c) 2013-present 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.
+
+#if os(iOS) || os(macOS) || os(visionOS)
+
+import Foundation
+import SQLite3
+
+class Database {
+ private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
+ private let dbPath: String
+
+ static func dbPath(namespace: String) -> String {
+ let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0]
+
+ // Create snowplow subdirectory if it doesn't exist
+ let snowplowDirPath = URL(fileURLWithPath: libraryPath).appendingPathComponent("snowplow").path
+ try? FileManager.default.createDirectory(atPath: snowplowDirPath, withIntermediateDirectories: true, attributes: nil)
+
+ // Create path for the database
+ let regex: NSRegularExpression? = try? NSRegularExpression(pattern: "[^a-zA-Z0-9_]+", options: [])
+
+ let sqliteSuffix = regex?.stringByReplacingMatches(in: namespace, options: [], range: NSRange(location: 0, length: namespace.count), withTemplate: "-")
+ let sqliteFilename = "snowplowEvents-\(sqliteSuffix ?? "").sqlite"
+ return URL(fileURLWithPath: snowplowDirPath).appendingPathComponent(sqliteFilename).path
+ }
+
+ init(namespace: String) {
+ dbPath = Database.dbPath(namespace: namespace)
+
+ createTable()
+ }
+
+ private func createTable() {
+ let sql = """
+ CREATE TABLE IF NOT EXISTS 'events'
+ (id INTEGER PRIMARY KEY, eventData BLOB, dateCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP)
+ """
+
+ _ = execute(sql: sql, name: "Create table")
+ }
+
+ func insertRow(_ dict: [String: Any]) {
+ guard let data = try? JSONSerialization.data(withJSONObject: dict) else {
+ logError(message: "Failed to serialize event to save in database")
+ return
+ }
+
+ let insertString = "INSERT INTO 'events' (eventData) VALUES (?)"
+ data.withUnsafeBytes { rawBuffer in
+ if let pointer = rawBuffer.baseAddress {
+ prepare(sql: insertString, name: "Insert row") { insertStatement, db in
+ sqlite3_bind_blob(insertStatement, 1, pointer, Int32(rawBuffer.count), SQLITE_TRANSIENT)
+
+ if sqlite3_step(insertStatement) == SQLITE_DONE {
+ logDebug(message: "Event stored in database")
+ } else {
+ logSqlError(message: "Failed to insert event to database", connection: db)
+ }
+ }
+ }
+ }
+ }
+
+ func deleteRows(ids: [Int64]? = nil) -> Bool {
+ var sql = "DELETE FROM 'events'"
+ if let ids = ids {
+ sql += " WHERE id IN \(idsSqlString(ids))"
+ }
+ return execute(sql: sql, name: "Delete rows")
+ }
+
+ func countRows() -> Int64? {
+ var count: Int64? = nil
+ let sql = "SELECT COUNT(*) AS count FROM 'events'"
+
+ prepare(sql: sql, name: "Count rows") { selectStatement, _ in
+ if sqlite3_step(selectStatement) == SQLITE_ROW {
+ count = sqlite3_column_int64(selectStatement, 0)
+ }
+ }
+ return count
+ }
+
+ private func idsSqlString(_ ids: [Int64] = []) -> String {
+ return "(" + ids.map { "\($0)" }.joined(separator: ",") + ")"
+ }
+
+ func readRows(numRows: Int) -> [(id: Int64, data: [String: Any])] {
+ var rows: [(id: Int64, data: [String: Any])] = []
+ let sql = "SELECT id, eventData FROM 'events' LIMIT \(numRows)"
+
+ var rowsRead: Int = 0
+ prepare(sql: sql, name: "Select rows") { selectStatement, db in
+ while sqlite3_step(selectStatement) == SQLITE_ROW {
+ if let blob = sqlite3_column_blob(selectStatement, 1) {
+ let blobLength = sqlite3_column_bytes(selectStatement, 1)
+ let data = Data(bytes: blob, count: Int(blobLength))
+ let id = sqlite3_column_int64(selectStatement, 0)
+
+ if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
+ rows.append((id: id, data: dict))
+
+ rowsRead += 1
+ }
+ } else {
+ logSqlError(message: "No data found for row in events", connection: db)
+ }
+ }
+ if rowsRead > 0 {
+ logDebug(message: "Read \(rowsRead) events from database")
+ }
+ }
+ return rows
+ }
+
+ func removeOldEvents(maxSize: Int64, maxAge: TimeInterval) {
+ let sql = """
+ DELETE FROM 'events'
+ WHERE id NOT IN (
+ SELECT id FROM events
+ WHERE dateCreated >= datetime('now','-\(maxAge) seconds')
+ ORDER BY dateCreated DESC, id DESC
+ LIMIT \(maxSize)
+ )
+ """
+
+ _ = execute(sql: sql, name: "Delete old events")
+ }
+
+ private func prepare(sql: String, name: String, closure: (OpaquePointer?, OpaquePointer?) -> ()) {
+ withConnection { db in
+ var statement: OpaquePointer?
+ if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
+ closure(statement, db)
+ } else {
+ logSqlError(message: "\(name) failed to prepare", connection: db)
+ }
+ sqlite3_finalize(statement)
+ }
+ }
+
+ private func execute(sql: String, name: String) -> Bool {
+ var success = false
+ prepare(sql: sql, name: name) { statement, db in
+ if sqlite3_step(statement) == SQLITE_DONE {
+ logDebug(message: "\(name) successful")
+ success = true
+ } else {
+ logSqlError(message: "\(name) failed", connection: db)
+ }
+ }
+ return success
+ }
+
+ private func logSqlError(message: String? = nil, connection: OpaquePointer? = nil) {
+ if let msg = message {
+ logError(message: msg)
+ }
+ if let db = connection {
+ let sqlError = String(cString: sqlite3_errmsg(db)!)
+ logError(message: sqlError)
+ }
+ }
+
+ private func withConnection(closure: (OpaquePointer) -> T) -> T? {
+ if let connection = open() {
+ defer { close(connection) }
+ return closure(connection)
+ }
+ return nil
+ }
+
+ private func open() -> OpaquePointer? {
+ var connection: OpaquePointer?
+ if sqlite3_open_v2(dbPath, &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) != SQLITE_OK {
+ logSqlError(message: "Failed to open database: \(dbPath)")
+ }
+ return connection
+ }
+
+ private func close(_ connection: OpaquePointer) {
+ sqlite3_close(connection)
+ }
+}
+
+#endif
diff --git a/Sources/Core/Storage/MemoryEventStore.swift b/Sources/Core/Storage/MemoryEventStore.swift
index 1e0f569b7..f885401e3 100644
--- a/Sources/Core/Storage/MemoryEventStore.swift
+++ b/Sources/Core/Storage/MemoryEventStore.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -15,17 +15,15 @@ import Foundation
class MemoryEventStore: NSObject, EventStore {
- var sendLimit: UInt
- var index: Int64
- var orderedSet: NSMutableOrderedSet
-
+ private var sendLimit: UInt
+ private var index: Int64
+ private var eventBuffer: [EmitterEvent] = []
convenience override init() {
self.init(limit: 250)
}
init(limit: UInt) {
- orderedSet = NSMutableOrderedSet()
sendLimit = limit
index = 0
}
@@ -33,67 +31,63 @@ 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)
+ eventBuffer.append(item)
index += 1
}
func count() -> UInt {
- objc_sync_enter(self)
- defer { objc_sync_exit(self) }
- return UInt(orderedSet.count)
+ InternalQueue.onQueuePrecondition()
+
+ return UInt(eventBuffer.count)
}
func emittableEvents(withQueryLimit queryLimit: UInt) -> [EmitterEvent] {
- objc_sync_enter(self)
- defer { objc_sync_exit(self) }
- let setCount = (orderedSet).count
- if setCount <= 0 {
- return []
- }
- let len = min(Int(queryLimit), setCount)
- _ = NSRange(location: 0, length: len)
- var count = 0
- let indexes = orderedSet.indexes { _, _, _ in
- count += 1
- return count <= queryLimit
- }
- let objects = orderedSet.objects(at: indexes)
- var result: [EmitterEvent] = []
- for object in objects {
- if let event = object as? EmitterEvent {
- result.append(event)
- }
- }
- return result
+ InternalQueue.onQueuePrecondition()
+
+ let limit = min(queryLimit, sendLimit)
+
+ return Array(eventBuffer.prefix(Int(limit)))
}
func removeAllEvents() -> Bool {
- objc_sync_enter(self)
- defer { objc_sync_exit(self) }
- orderedSet.removeAllObjects()
+ InternalQueue.onQueuePrecondition()
+
+ eventBuffer.removeAll()
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) }
- var itemsToRemove: [EmitterEvent] = []
- for item in orderedSet {
- guard let item = item as? EmitterEvent else {
- continue
- }
- if storeIds.contains(item.storeId) {
- itemsToRemove.append(item)
+ InternalQueue.onQueuePrecondition()
+
+ eventBuffer = eventBuffer.filter { !storeIds.contains($0.storeId) }
+ return true
+ }
+
+ func removeOldEvents(maxSize: Int64, maxAge: TimeInterval) {
+ InternalQueue.onQueuePrecondition()
+
+ let currentTimestamp = Date().timeIntervalSince1970
+
+ // remove old events by age
+ eventBuffer = eventBuffer.filter { emitterEvent in
+ if let timestampString = emitterEvent.payload[kSPTimestamp] as? String,
+ let timestamp = Double(timestampString) {
+ let timestampSecs = timestamp / 1000.0
+ return currentTimestamp - timestampSecs <= maxAge
}
+ return true
}
- orderedSet.removeObjects(in: itemsToRemove)
- return true
+
+ // remove old events by size limit
+ eventBuffer = Array(eventBuffer.suffix(Int(maxSize)))
}
}
diff --git a/Sources/Core/Storage/SQLiteEventStore.swift b/Sources/Core/Storage/SQLiteEventStore.swift
index 101badb09..e88b4a0b6 100644
--- a/Sources/Core/Storage/SQLiteEventStore.swift
+++ b/Sources/Core/Storage/SQLiteEventStore.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -11,273 +11,79 @@
// express or implied. See the Apache License Version 2.0 for the specific
// language governing permissions and limitations there under.
-#if os(iOS) || os(macOS)
+#if os(iOS) || os(macOS) || os(visionOS)
-import FMDB
import Foundation
-let _queryCreateTable = "CREATE TABLE IF NOT EXISTS 'events' (id INTEGER PRIMARY KEY, eventData BLOB, dateCreated TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"
-let _querySelectAll = "SELECT * FROM 'events'"
-let _querySelectCount = "SELECT Count(*) FROM 'events'"
-let _queryInsertEvent = "INSERT INTO 'events' (eventData) VALUES (?)"
-let _querySelectId = "SELECT * FROM 'events' WHERE id=?"
-let _queryDeleteId = "DELETE FROM 'events' WHERE id=?"
-let _queryDeleteIds = "DELETE FROM 'events' WHERE id IN (%@)"
-let _queryDeleteAll = "DELETE FROM 'events'"
-
class SQLiteEventStore: NSObject, EventStore {
- var namespace: String
- var sqliteFilename: String
- var dbPath: String
- var queue: FMDatabaseQueue?
- var sendLimit: Int
-
- /// IMPORTANT: This method is for internal use only. It's signature and behaviour might change in any
- /// future tracker release.
- ///
- /// Clears all the EventStores not associated at any of the namespaces passed as parameter.
- ///
- /// - Parameter allowedNamespaces: The namespace allowed. All the EventStores not associated at any of
- /// the allowedNamespaces will be cleared.
- /// - Returns: The list of namespaces that have been found with EventStores and have been cleared out.
- class func removeUnsentEventsExcept(forNamespaces allowedNamespaces: [String]?) -> [String]? {
- #if os(tvOS)
- let libraryPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).map(\.path)[0]
- #else
- let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0]
- #endif
- let snowplowDirPath = URL(fileURLWithPath: libraryPath).appendingPathComponent("snowplow").path
- var files: [String]? = nil
- do {
- files = try FileManager.default.contentsOfDirectory(atPath: snowplowDirPath)
- } catch {
- }
- var allowedFiles: [String]? = []
- for namespace in allowedNamespaces ?? [] {
- var regex: NSRegularExpression? = nil
- do {
- regex = try NSRegularExpression(pattern: "[^a-zA-Z0-9_]+", options: [])
- } catch {
- }
- let sqliteSuffix = regex?.stringByReplacingMatches(in: namespace, options: [], range: NSRange(location: 0, length: namespace.count), withTemplate: "-")
- let sqliteFilename = "snowplowEvents-\(sqliteSuffix ?? "").sqlite"
- allowedFiles?.append(sqliteFilename)
- }
- var removedFiles: [String]? = []
- for file in files ?? [] {
- if !(allowedFiles?.contains(file) ?? false) {
- let pathToRemove = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent(file).path
- try? FileManager.default.removeItem(atPath: pathToRemove)
- removedFiles?.append(file)
- }
- }
- return removedFiles
- }
-
- /// Basic initializer that creates a database event table (if one does not exist) and then closes the connection.
- convenience init(namespace: String?) {
- self.init(namespace: namespace, limit: 250)
- }
-
- init(namespace: String?, limit: Int) {
- self.namespace = namespace ?? ""
- sendLimit = limit
-
- #if os(tvOS)
- let libraryPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).map(\.path)[0]
- #else
- let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0]
- #endif
- // Create snowplow subdirectory if it doesn't exist
- let snowplowDirPath = URL(fileURLWithPath: libraryPath).appendingPathComponent("snowplow").path
- try? FileManager.default.createDirectory(atPath: snowplowDirPath, withIntermediateDirectories: true, attributes: nil)
-
- // Create path for the database
- var regex: NSRegularExpression? = nil
- do {
- regex = try NSRegularExpression(pattern: "[^a-zA-Z0-9_]+", options: [])
- } catch {
- }
- let sqliteSuffix = regex?.stringByReplacingMatches(in: self.namespace, options: [], range: NSRange(location: 0, length: namespace?.count ?? 0), withTemplate: "-")
- sqliteFilename = "snowplowEvents-\(sqliteSuffix ?? "").sqlite"
- dbPath = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent(sqliteFilename).path
+ private let database: Database
+ private var sendLimit: Int
+ init(namespace: String?, limit: Int = 250) {
+ let namespace = namespace ?? ""
+
// Migrate old database if it exists
+ let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0]
let oldDbPath = URL(fileURLWithPath: libraryPath).appendingPathComponent("snowplowEvents.sqlite").path
if FileManager.default.fileExists(atPath: oldDbPath) {
- try? FileManager.default.moveItem(atPath: oldDbPath, toPath: dbPath)
+ let newDbPath = Database.dbPath(namespace: namespace)
+ try? FileManager.default.moveItem(atPath: oldDbPath, toPath: newDbPath)
}
-
- // Create database
- queue = FMDatabaseQueue(path: dbPath)
- super.init()
- _ = createTable()
- }
-
- deinit {
- queue?.close()
+
+ database = Database(namespace: namespace)
+ sendLimit = limit
}
// MARK: SPEventStore implementation methods
- func addEvent(_ payload: Payload) {
- _ = insertDictionaryData(payload.dictionary)
+ func addEvent(_ data: Payload) {
+ InternalQueue.onQueuePrecondition()
+
+ self.database.insertRow(data.dictionary)
}
func removeEvent(withId storeId: Int64) -> Bool {
- var res = false
- queue?.inDatabase({ db in
- if db.open() {
- logDebug(message: String(format: "Removing %d from database now.", storeId))
- res = db.executeUpdate(_queryDeleteId, withArgumentsIn: [storeId])
- }
- })
- return res
+ InternalQueue.onQueuePrecondition()
+
+ return database.deleteRows(ids: [storeId])
}
func removeEvents(withIds storeIds: [Int64]) -> Bool {
- var res = false
- queue?.inDatabase({ db in
- if db.open() && storeIds.count != 0 {
- let ids = storeIds.map { String(describing: $0) }.joined(separator: ",")
- logDebug(message: String(format: "Removing [%@] from database now.", ids))
- let query = String(format: _queryDeleteIds, ids)
- res = db.executeUpdate(query, withArgumentsIn: [])
- }
- })
- return res
+ InternalQueue.onQueuePrecondition()
+
+ return database.deleteRows(ids: storeIds)
}
func removeAllEvents() -> Bool {
- var res = false
- queue?.inDatabase({ db in
- if db.open() {
- logDebug(message: "Removing all events from database now.")
- res = db.executeUpdate(_queryDeleteAll, withArgumentsIn: [])
- }
- })
- return res
+ InternalQueue.onQueuePrecondition()
+
+ return database.deleteRows()
}
func count() -> UInt {
- var num: UInt = 0
- queue?.inDatabase({ db in
- if db.open() {
- if let s = db.executeQuery(_querySelectCount, withArgumentsIn: []) {
- while s.next() {
- num = NSNumber(value: s.int(forColumnIndex: 0)).uintValue
- }
- s.close()
- }
- }
- })
- return num
+ InternalQueue.onQueuePrecondition()
+
+ if let count = database.countRows() {
+ return UInt(count)
+ }
+ return 0
}
func emittableEvents(withQueryLimit queryLimit: UInt) -> [EmitterEvent] {
- return getAllEventsLimited(min(queryLimit, UInt(sendLimit))) ?? []
- }
-
- // MARK: SPSQLiteEventStore methods
-
- func createTable() -> Bool {
- var res = false
- queue?.inDatabase({ db in
- if db.open() {
- res = db.executeStatements(_queryCreateTable)
- }
- })
- return res
- }
-
- /// Inserts events into the sqlite table for the app identified with it's bundleId (appId).
- /// - Parameter payload: A SnowplowPayload instance to be inserted into the database.
- /// - Returns: If the insert was successful, we return the rowId of the inserted entry, otherwise -1. We explicitly do this in the case of an error, sqlite would return the previous successful insert leading to incorrect data removals.
- func insertEvent(_ payload: Payload?) -> Int64 {
- return insertDictionaryData(payload?.dictionary)
- }
-
- func insertDictionaryData(_ dict: [AnyHashable : Any]?) -> Int64 {
- var res: Int64 = -1
- if dict == nil {
- return res
+ InternalQueue.onQueuePrecondition()
+
+ 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)
}
- queue?.inDatabase({ db in
- if db.open() {
- if let dict = dict,
- let data = try? JSONSerialization.data(withJSONObject: dict) {
- try? db.executeUpdate(_queryInsertEvent, values: [data])
- res = db.lastInsertRowId
- }
- }
- })
- return res
}
-
- /// Finds the row in the event table with the supplied ID.
- /// - Parameter id_: Unique ID of the row in the events table to be returned.
- /// - Returns: A dictionary containing data with keys: 'ID', 'eventData', and 'dateCreated'.
- func getEventWithId(_ id_: Int64) -> EmitterEvent? {
- var event: EmitterEvent? = nil
- queue?.inDatabase({ db in
- if db.open() {
- if let s = try? db.executeQuery(_querySelectId, values: [id_]) {
- while s.next() {
- if let data = s.data(forColumn: "eventData"),
- let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
- let payload = Payload(dictionary: dict)
- event = EmitterEvent(payload: payload, storeId: id_)
- }
- }
- s.close()
- }
- }
- })
- return event
- }
-
- /// Returns all the events in an array of dictionaries.
- /// - Returns: An array with each dictionary element containing key-value pairs of 'date', 'data', 'ID'.
- func getAllEvents() -> [EmitterEvent]? {
- return self.getAllEvents(withQuery: _querySelectAll)
- }
-
- /// Returns limited number the events that are NOT pending in an array of dictionaries.
- /// - Returns: An array with each dictionary element containing key-value pairs of 'date', 'data', 'ID'.
- func getAllEventsLimited(_ limit: UInt) -> [EmitterEvent]? {
- let query = "\(_querySelectAll) LIMIT \((NSNumber(value: limit)).stringValue)"
- return getAllEvents(withQuery: query)
- }
-
- func getAllEvents(withQuery query: String) -> [EmitterEvent]? {
- var res: [EmitterEvent] = []
- queue?.inDatabase({ db in
- if db.open() {
- if let s = try? db.executeQuery(query, values: []) {
- while s.next() {
- let index = s.longLongInt(forColumn: "ID")
- if let data = s.data(forColumn: "eventData"),
- let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
- let payload = Payload(dictionary: dict)
- let event = EmitterEvent(payload: payload, storeId: index)
- res.append(event)
- }
- }
- s.close()
- }
- }
- })
- return res
- }
-
- /// The row ID of the last insert made.
- /// - Returns: The row ID of the last insert made.
- func getLastInsertedRowId() -> Int64 {
- var res: Int64 = -1
- queue?.inDatabase({ db in
- res = db.lastInsertRowId
- })
- return res
+
+ func removeOldEvents(maxSize: Int64, maxAge: TimeInterval) {
+ InternalQueue.onQueuePrecondition()
+
+ return database.removeOldEvents(maxSize: maxSize, maxAge: maxAge)
}
}
diff --git a/Sources/Core/Subject/PlatformContext.swift b/Sources/Core/Subject/PlatformContext.swift
index db59ecd39..1c2c8044a 100644
--- a/Sources/Core/Subject/PlatformContext.swift
+++ b/Sources/Core/Subject/PlatformContext.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -12,7 +12,7 @@
// language governing permissions and limitations there under.
import Foundation
-#if os(iOS)
+#if os(iOS) || os(visionOS)
import UIKit
#endif
@@ -26,25 +26,31 @@ class PlatformContext {
private var lastUpdatedEphemeralNetworkDict: TimeInterval = 0.0
private var deviceInfoMonitor: DeviceInfoMonitor
+ /// Overrides for retrieving property values
+ var platformContextRetriever: PlatformContextRetriever
+
/// List of properties of the platform context to track
var platformContextProperties: [PlatformContextProperty]?
/// Initializes a newly allocated PlatformContext object with custom update frequency for mobile and network properties and a custom device info monitor
/// - Parameters:
/// - platformContextProperties: List of properties of the platform context to track
+ /// - platformContextRetriever: Overrides for the property retrieving behavior
/// - mobileDictUpdateFrequency: Minimal gap between subsequent updates of mobile platform information
/// - networkDictUpdateFrequency: Minimal gap between subsequent updates of network platform information
/// - deviceInfoMonitor: Device monitor for fetching platform information
/// - Returns: a PlatformContext object
init(platformContextProperties: [PlatformContextProperty]? = nil,
+ platformContextRetriever: PlatformContextRetriever? = nil,
mobileDictUpdateFrequency: TimeInterval = 1.0,
networkDictUpdateFrequency: TimeInterval = 10.0,
deviceInfoMonitor: DeviceInfoMonitor = DeviceInfoMonitor()) {
self.platformContextProperties = platformContextProperties
+ self.platformContextRetriever = platformContextRetriever ?? PlatformContextRetriever()
self.mobileDictUpdateFrequency = mobileDictUpdateFrequency
self.networkDictUpdateFrequency = networkDictUpdateFrequency
self.deviceInfoMonitor = deviceInfoMonitor
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
UIDevice.current.isBatteryMonitoringEnabled = true
#endif
setPlatformDict()
@@ -52,9 +58,8 @@ class PlatformContext {
/// Updates and returns payload dictionary with device context information.
/// - Parameter userAnonymisation: Whether to anonymise user identifiers (IDFA values)
- func fetchPlatformDict(userAnonymisation: Bool, advertisingIdentifierRetriever: (() -> UUID?)?) -> Payload {
- #if os(iOS)
- objc_sync_enter(self)
+ func fetchPlatformDict(userAnonymisation: Bool) -> Payload {
+ #if os(iOS) || os(visionOS)
let now = Date().timeIntervalSince1970
if now - lastUpdatedEphemeralMobileDict >= mobileDictUpdateFrequency {
setEphemeralMobileDict()
@@ -62,7 +67,6 @@ class PlatformContext {
if now - lastUpdatedEphemeralNetworkDict >= networkDictUpdateFrequency {
setEphemeralNetworkDict()
}
- objc_sync_exit(self)
#endif
if userAnonymisation {
// mask user identifiers
@@ -71,10 +75,8 @@ class PlatformContext {
copy[kSPMobileAppleIdfv] = nil
return copy
} else {
- if let retriever = advertisingIdentifierRetriever {
- if shouldTrack(.appleIdfa) && platformDict.dictionary[kSPMobileAppleIdfa] == nil {
- platformDict[kSPMobileAppleIdfa] = retriever()?.uuidString
- }
+ if shouldTrack(.appleIdfa) && platformDict.dictionary[kSPMobileAppleIdfa] == nil {
+ platformDict[kSPMobileAppleIdfa] = platformContextRetriever.appleIdfa?()?.uuidString
}
return platformDict
}
@@ -84,35 +86,63 @@ class PlatformContext {
func setPlatformDict() {
platformDict = Payload()
- platformDict[kSPPlatformOsType] = deviceInfoMonitor.osType
- platformDict[kSPPlatformOsVersion] = deviceInfoMonitor.osVersion
- platformDict[kSPPlatformDeviceManu] = deviceInfoMonitor.deviceVendor
- platformDict[kSPPlatformDeviceModel] = deviceInfoMonitor.deviceModel
+ platformDict[kSPPlatformOsType] = (
+ platformContextRetriever.osType == nil ?
+ deviceInfoMonitor.osType : platformContextRetriever.osType?()
+ )
+ platformDict[kSPPlatformOsVersion] = (
+ platformContextRetriever.osVersion == nil ?
+ deviceInfoMonitor.osVersion : platformContextRetriever.osVersion?()
+ )
+ platformDict[kSPPlatformDeviceManu] = (
+ platformContextRetriever.deviceVendor == nil ?
+ deviceInfoMonitor.deviceVendor : platformContextRetriever.deviceVendor?()
+ )
+ platformDict[kSPPlatformDeviceModel] = (
+ platformContextRetriever.deviceModel == nil ?
+ deviceInfoMonitor.deviceModel : platformContextRetriever.deviceModel?()
+ )
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
setMobileDict()
#endif
}
func setMobileDict() {
if shouldTrack(.resolution) {
- platformDict[kSPMobileResolution] = deviceInfoMonitor.resolution
+ platformDict[kSPMobileResolution] = (
+ platformContextRetriever.resolution == nil ?
+ deviceInfoMonitor.resolution : platformContextRetriever.resolution?()
+ )
}
if shouldTrack(.language) {
// the schema has a max-length 8 for language which iOS exceeds sometimes
- if let language = deviceInfoMonitor.language { platformDict[kSPMobileLanguage] = String(language.prefix(8)) }
+ let language = (
+ platformContextRetriever.language == nil ?
+ deviceInfoMonitor.language : platformContextRetriever.language?()
+ )
+ if let language = language { platformDict[kSPMobileLanguage] = String(language.prefix(8)) }
}
if shouldTrack(.scale) {
- platformDict[kSPMobileScale] = deviceInfoMonitor.scale
+ platformDict[kSPMobileScale] = (
+ platformContextRetriever.scale == nil ?
+ deviceInfoMonitor.scale : platformContextRetriever.scale?()
+ )
}
if shouldTrack(.carrier) {
- platformDict[kSPMobileCarrier] = deviceInfoMonitor.carrierName
+ platformDict[kSPMobileCarrier] = (
+ platformContextRetriever.carrier == nil ?
+ deviceInfoMonitor.carrierName : platformContextRetriever.carrier?()
+ )
}
if shouldTrack(.totalStorage) {
- platformDict[kSPMobileTotalStorage] = deviceInfoMonitor.totalStorage
+ platformDict[kSPMobileTotalStorage] = platformContextRetriever.totalStorage?()
}
if shouldTrack(.physicalMemory) {
- platformDict[kSPMobilePhysicalMemory] = deviceInfoMonitor.physicalMemory
+ platformDict[kSPMobilePhysicalMemory] = (
+ platformContextRetriever.physicalMemory == nil ?
+ deviceInfoMonitor.physicalMemory : platformContextRetriever.physicalMemory?()
+ )
}
setEphemeralMobileDict()
@@ -123,26 +153,44 @@ class PlatformContext {
lastUpdatedEphemeralMobileDict = Date().timeIntervalSince1970
if shouldTrack(.appleIdfv) && platformDict[kSPMobileAppleIdfv] == nil {
- platformDict[kSPMobileAppleIdfv] = deviceInfoMonitor.appleIdfv
+ platformDict[kSPMobileAppleIdfv] = (
+ platformContextRetriever.appleIdfv == nil ?
+ deviceInfoMonitor.appleIdfv : platformContextRetriever.appleIdfv?()
+ )
}
if shouldTrack(.batteryLevel) {
- platformDict[kSPMobileBatteryLevel] = deviceInfoMonitor.batteryLevel
+ platformDict[kSPMobileBatteryLevel] = (
+ platformContextRetriever.batteryLevel == nil ?
+ deviceInfoMonitor.batteryLevel : platformContextRetriever.batteryLevel?()
+ )
}
if shouldTrack(.batteryState) {
- platformDict[kSPMobileBatteryState] = deviceInfoMonitor.batteryState
+ platformDict[kSPMobileBatteryState] = (
+ platformContextRetriever.batteryState == nil ?
+ deviceInfoMonitor.batteryState : platformContextRetriever.batteryState?()
+ )
}
if shouldTrack(.lowPowerMode) {
- platformDict[kSPMobileLowPowerMode] = deviceInfoMonitor.isLowPowerModeEnabled
+ platformDict[kSPMobileLowPowerMode] = (
+ platformContextRetriever.lowPowerMode == nil ?
+ deviceInfoMonitor.isLowPowerModeEnabled : platformContextRetriever.lowPowerMode?()
+ )
}
if shouldTrack(.availableStorage) {
- platformDict[kSPMobileAvailableStorage] = deviceInfoMonitor.availableStorage
+ platformDict[kSPMobileAvailableStorage] = platformContextRetriever.availableStorage?()
}
if shouldTrack(.appAvailableMemory) {
- platformDict[kSPMobileAppAvailableMemory] = deviceInfoMonitor.appAvailableMemory
+ platformDict[kSPMobileAppAvailableMemory] = (
+ platformContextRetriever.appAvailableMemory == nil ?
+ deviceInfoMonitor.appAvailableMemory : platformContextRetriever.appAvailableMemory?()
+ )
}
if shouldTrack(.isPortrait) {
- platformDict[kSPMobileIsPortrait] = deviceInfoMonitor.isPortrait
+ platformDict[kSPMobileIsPortrait] = (
+ platformContextRetriever.isPortrait == nil ?
+ deviceInfoMonitor.isPortrait : platformContextRetriever.isPortrait?()
+ )
}
}
@@ -150,10 +198,16 @@ class PlatformContext {
lastUpdatedEphemeralNetworkDict = Date().timeIntervalSince1970
if shouldTrack(.networkTechnology) {
- platformDict[kSPMobileNetworkTech] = deviceInfoMonitor.networkTechnology
+ platformDict[kSPMobileNetworkTech] = (
+ platformContextRetriever.networkTechnology == nil ?
+ deviceInfoMonitor.networkTechnology : platformContextRetriever.networkTechnology?()
+ )
}
if shouldTrack(.networkType) {
- platformDict[kSPMobileNetworkType] = deviceInfoMonitor.networkType
+ platformDict[kSPMobileNetworkType] = (
+ platformContextRetriever.networkType == nil ?
+ deviceInfoMonitor.networkType : platformContextRetriever.networkType?()
+ )
}
}
diff --git a/Sources/Core/Subject/Subject.swift b/Sources/Core/Subject/Subject.swift
index 48dd09735..90588cce9 100644
--- a/Sources/Core/Subject/Subject.swift
+++ b/Sources/Core/Subject/Subject.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -17,19 +17,30 @@ import Foundation
/// This class is used to access and persist user information, it represents the current user being tracked.
class Subject : NSObject {
private var standardDict: [String : String] = [:]
- private var platformContextManager: PlatformContext
private var geoDict: [String : NSObject] = [:]
+
+ var geoLocationContext = false
+
+ // MARK: - Platform context
+ private var platformContextManager: PlatformContext
+
var platformContext = false
+
var platformContextProperties: [PlatformContextProperty]? {
- get {
- platformContextManager.platformContextProperties
- }
- set {
- platformContextManager.platformContextProperties = newValue
- }
+ get { return platformContextManager.platformContextProperties }
+ set { platformContextManager.platformContextProperties = newValue }
+ }
+
+ var platformContextRetriever: PlatformContextRetriever {
+ get { return platformContextManager.platformContextRetriever }
+ set { platformContextManager.platformContextRetriever = newValue }
+ }
+
+ var advertisingIdentifierRetriever: (() -> UUID?)? {
+ get { return platformContextManager.platformContextRetriever.appleIdfa }
+ set { platformContextManager.platformContextRetriever.appleIdfa = newValue }
}
- var geoLocationContext = false
// MARK: - Standard Dictionary
@@ -38,9 +49,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 +58,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 +68,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 +78,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 +88,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 +98,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 +108,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 +118,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 +128,7 @@ class Subject : NSObject {
var _screenResolution: SPSize?
var screenResolution: SPSize? {
- get {
- _screenResolution
- }
+ get { return _screenResolution }
set {
_screenResolution = newValue
if let size = newValue {
@@ -149,9 +142,7 @@ class Subject : NSObject {
var _screenViewPort: SPSize?
var screenViewPort: SPSize? {
- get {
- _screenViewPort
- }
+ get { return _screenViewPort }
set {
_screenViewPort = newValue
if let size = newValue {
@@ -160,7 +151,6 @@ class Subject : NSObject {
} else {
standardDict.removeValue(forKey: kSPViewPort)
}
-
}
}
@@ -170,92 +160,64 @@ 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,
platformContextProperties: [PlatformContextProperty]? = nil,
+ platformContextRetriever: PlatformContextRetriever? = nil,
geoLocationContext geoContext: Bool = false,
subjectConfiguration config: SubjectConfiguration? = nil) {
- self.platformContextManager = PlatformContext(platformContextProperties: platformContextProperties)
+ self.platformContextManager = PlatformContext(
+ platformContextProperties: platformContextProperties,
+ platformContextRetriever: platformContextRetriever
+ )
super.init()
- self.platformContextProperties = platformContextProperties
+ platformContextManager.platformContextProperties = platformContextProperties
self.platformContext = platformContext
- geoLocationContext = geoContext
+ self.geoLocationContext = geoContext
screenResolution = Utilities.resolution
screenViewPort = Utilities.viewPort
@@ -294,24 +256,22 @@ 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.
/// - Parameter userAnonymisation: Whether to anonymise user identifiers
/// - Returns: A SPPayload with all platform specific pairs.
- func platformDict(userAnonymisation: Bool, advertisingIdentifierRetriever: (() -> UUID?)?) -> Payload? {
+ func platformDict(userAnonymisation: Bool) -> Payload? {
if platformContext {
- return platformContextManager.fetchPlatformDict(
- userAnonymisation: userAnonymisation,
- advertisingIdentifierRetriever: advertisingIdentifierRetriever)
+ return platformContextManager.fetchPlatformDict(userAnonymisation: userAnonymisation)
} else {
return nil
}
@@ -331,4 +291,5 @@ class Subject : NSObject {
return nil
}
}
+
}
diff --git a/Sources/Core/Subject/SubjectControllerImpl.swift b/Sources/Core/Subject/SubjectControllerImpl.swift
index 4768ccd91..4cb8b8f16 100644
--- a/Sources/Core/Subject/SubjectControllerImpl.swift
+++ b/Sources/Core/Subject/SubjectControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Tracker/EcommerceControllerImpl.swift b/Sources/Core/Tracker/EcommerceControllerImpl.swift
index 16d4b2e77..a7348e238 100644
--- a/Sources/Core/Tracker/EcommerceControllerImpl.swift
+++ b/Sources/Core/Tracker/EcommerceControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Tracker/InstallTracker.swift b/Sources/Core/Tracker/InstallTracker.swift
index 1898a1619..c31fbb7aa 100644
--- a/Sources/Core/Tracker/InstallTracker.swift
+++ b/Sources/Core/Tracker/InstallTracker.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Tracker/PluginsControllerImpl.swift b/Sources/Core/Tracker/PluginsControllerImpl.swift
index 60c913354..6a89ab9cb 100644
--- a/Sources/Core/Tracker/PluginsControllerImpl.swift
+++ b/Sources/Core/Tracker/PluginsControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Tracker/ServiceProvider.swift b/Sources/Core/Tracker/ServiceProvider.swift
index 4fd828af1..b3ed43bc1 100644
--- a/Sources/Core/Tracker/ServiceProvider.swift
+++ b/Sources/Core/Tracker/ServiceProvider.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -14,7 +14,7 @@
import Foundation
class ServiceProvider: NSObject, ServiceProviderProtocol {
- private(set) var namespace: String
+ let namespace: String
var isTrackerInitialized: Bool { return _tracker != nil }
@@ -227,33 +227,46 @@ class ServiceProvider: NSObject, ServiceProviderProtocol {
return Subject(
platformContext: trackerConfiguration.platformContext,
platformContextProperties: trackerConfiguration.platformContextProperties,
+ platformContextRetriever: trackerConfiguration.platformContextRetriever,
geoLocationContext: trackerConfiguration.geoLocationContext,
subjectConfiguration: subjectConfiguration)
}
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
+ emitter.maxEventStoreSize = self.emitterConfiguration.maxEventStoreSize
+ emitter.maxEventStoreAge = self.emitterConfiguration.maxEventStoreAge
}
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,
+ timeout: self.networkConfiguration.timeout,
+ builder: builder
+ )
}
if emitterConfiguration.isPaused {
@@ -286,12 +299,13 @@ class ServiceProvider: NSObject, ServiceProviderProtocol {
tracker.applicationContext = trackerConfiguration.applicationContext
tracker.deepLinkContext = trackerConfiguration.deepLinkContext
tracker.screenContext = trackerConfiguration.screenContext
+ tracker.screenEngagementAutotracking = trackerConfiguration.screenEngagementAutotracking
tracker.autotrackScreenViews = trackerConfiguration.screenViewAutotracking
tracker.lifecycleEvents = trackerConfiguration.lifecycleAutotracking
tracker.installEvent = trackerConfiguration.installAutotracking
tracker.trackerDiagnostic = trackerConfiguration.diagnosticAutotracking
tracker.userAnonymisation = trackerConfiguration.userAnonymisation
- tracker.advertisingIdentifierRetriever = trackerConfiguration.advertisingIdentifierRetriever
+ tracker.immersiveSpaceContext = trackerConfiguration.immersiveSpaceContext
if gdprConfiguration.sourceConfig != nil {
tracker.gdprContext = GDPRContext(
basis: gdprConfiguration.basisForProcessing,
diff --git a/Sources/Core/Tracker/ServiceProviderProtocol.swift b/Sources/Core/Tracker/ServiceProviderProtocol.swift
index 6435d79a7..cfd07a51c 100644
--- a/Sources/Core/Tracker/ServiceProviderProtocol.swift
+++ b/Sources/Core/Tracker/ServiceProviderProtocol.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Core/Tracker/Tracker.swift b/Sources/Core/Tracker/Tracker.swift
index ee9dffa75..bf0ea3441 100644
--- a/Sources/Core/Tracker/Tracker.swift
+++ b/Sources/Core/Tracker/Tracker.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -40,10 +40,8 @@ func uncaughtExceptionHandler(_ exception: NSException) {
class Tracker: NSObject {
private var platformContextSchema: String = ""
private var dataCollection = true
-
private var builderFinished = false
-
/// The object used for sessionization, i.e. it characterizes user activity.
private(set) var session: Session?
/// Previous screen view state.
@@ -51,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
@@ -62,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?
@@ -89,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
@@ -163,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)
}
}
}
@@ -175,14 +132,12 @@ class Tracker: NSObject {
return _deepLinkContext
}
set(deepLinkContext) {
- objc_sync_enter(self)
- _deepLinkContext = deepLinkContext
+ self._deepLinkContext = deepLinkContext
if deepLinkContext {
- addOrReplace(stateMachine: DeepLinkStateMachine())
+ self.addOrReplace(stateMachine: DeepLinkStateMachine())
} else {
- _ = stateManager.removeStateMachine(DeepLinkStateMachine.identifier)
+ _ = self.stateManager.removeStateMachine(DeepLinkStateMachine.identifier)
}
- objc_sync_exit(self)
}
}
@@ -192,14 +147,25 @@ class Tracker: NSObject {
return _screenContext
}
set(screenContext) {
- objc_sync_enter(self)
- _screenContext = screenContext
+ self._screenContext = screenContext
if screenContext {
- addOrReplace(stateMachine: ScreenStateMachine())
+ self.addOrReplace(stateMachine: ScreenStateMachine())
} else {
- _ = stateManager.removeStateMachine(ScreenStateMachine.identifier)
+ _ = self.stateManager.removeStateMachine(ScreenStateMachine.identifier)
+ }
+ }
+ }
+
+ private var _screenEngagementAutotracking = false
+ var screenEngagementAutotracking: Bool {
+ get { return _screenEngagementAutotracking }
+ set {
+ self._screenEngagementAutotracking = newValue
+ if newValue {
+ self.addOrReplace(stateMachine: ScreenSummaryStateMachine())
+ } else {
+ _ = self.stateManager.removeStateMachine(ScreenSummaryStateMachine.identifier)
}
- objc_sync_exit(self)
}
}
@@ -241,14 +207,12 @@ class Tracker: NSObject {
return _lifecycleEvents
}
set(lifecycleEvents) {
- objc_sync_enter(self)
- _lifecycleEvents = lifecycleEvents
+ self._lifecycleEvents = lifecycleEvents
if lifecycleEvents {
- addOrReplace(stateMachine: LifecycleStateMachine())
+ self.addOrReplace(stateMachine: LifecycleStateMachine())
} else {
- _ = stateManager.removeStateMachine(LifecycleStateMachine.identifier)
+ _ = self.stateManager.removeStateMachine(LifecycleStateMachine.identifier)
}
- objc_sync_exit(self)
}
}
@@ -268,6 +232,21 @@ class Tracker: NSObject {
}
}
}
+
+ private var _immersiveSpaceContext = false
+ var immersiveSpaceContext: Bool {
+ get {
+ return _immersiveSpaceContext
+ }
+ set(immersiveSpaceContext) {
+ self._immersiveSpaceContext = immersiveSpaceContext
+ if immersiveSpaceContext {
+ self.addOrReplace(stateMachine: ImmersiveSpaceStateMachine())
+ } else {
+ _ = self.stateManager.removeStateMachine(ImmersiveSpaceStateMachine.identifier)
+ }
+ }
+ }
/// GDPR context
/// You can enable or disable the context by setting this property
@@ -282,21 +261,19 @@ class Tracker: NSObject {
var isTracking: Bool {
return dataCollection
}
-
- var advertisingIdentifierRetriever: (() -> UUID?)?
init(trackerNamespace: String,
appId: String?,
emitter: Emitter,
builder: ((Tracker) -> (Void))) {
- self._emitter = emitter
- self._appId = appId ?? ""
- self._trackerNamespace = trackerNamespace
+ self.emitter = emitter
+ self.appId = appId ?? ""
+ self.trackerNamespace = trackerNamespace
super.init()
builder(self)
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
platformContextSchema = kSPMobileContextSchema
#else
platformContextSchema = kSPDesktopContextSchema
@@ -307,14 +284,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()
@@ -345,7 +320,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()
@@ -356,7 +333,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)
}
}
}
@@ -399,13 +376,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)
+ }
}
}
@@ -416,9 +395,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)
+ }
}
}
@@ -427,10 +408,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)
+ }
}
}
@@ -438,31 +421,38 @@ 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
+ /// - Returns: The event ID
+ func track(_ event: Event, eventId: UUID = UUID()) -> UUID {
+ InternalQueue.onQueuePrecondition()
+
+ if dataCollection {
+ let events = withEventsBefore(event: event, eventId: eventId)
+ for (event, eventId) in events {
+ event.beginProcessing(withTracker: self)
+ self.processEvent(event, eventId)
+ event.endProcessing(withTracker: self)
+ }
}
- event.beginProcessing(withTracker: self)
- let eventId = processEvent(event)
- event.endProcessing(withTracker: self)
return eventId
}
// MARK: - Event Decoration
+
+ private func withEventsBefore(event: Event, eventId: UUID) -> [(event: Event, eventId: UUID)] {
+ let eventsBefore = stateManager.eventsBefore(forProcessedEvent: event)
+
+ return eventsBefore.map { (event: $0, eventId: UUID()) } + [(event: event, eventId: eventId)]
+ }
- func processEvent(_ event: Event) -> UUID? {
- objc_sync_enter(self)
+ func processEvent(_ event: Event, _ eventId: UUID) {
let stateSnapshot = stateManager.trackerState(forProcessedEvent: event)
- objc_sync_exit(self)
- let trackerEvent = TrackerEvent(event: event, state: stateSnapshot)
+ let trackerEvent = TrackerEvent(event: event, eventId: eventId, state: stateSnapshot)
if let payload = self.payload(with: trackerEvent) {
emitter.addPayload(toBuffer: payload)
stateManager.afterTrack(event: trackerEvent)
- return trackerEvent.eventId
+ } else {
+ logDebug(message: "Event not tracked due to filtering")
}
- logDebug(message: "Event not tracked due to filtering")
- return nil
}
func payload(with event: TrackerEvent) -> Payload? {
@@ -514,9 +504,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)
@@ -568,9 +556,7 @@ class Tracker: NSObject {
func addBasicContexts(event: TrackerEvent) {
if subject != nil {
- if let platformDict = subject?.platformDict(
- userAnonymisation: userAnonymisation,
- advertisingIdentifierRetriever: advertisingIdentifierRetriever)?.dictionary {
+ if let platformDict = subject?.platformDict(userAnonymisation: userAnonymisation)?.dictionary {
event.addContextEntity(SelfDescribingJson(schema: platformContextSchema, andDictionary: platformDict))
}
if let geoLocationDict = subject?.geoLocationDict {
diff --git a/Sources/Core/Tracker/TrackerControllerImpl.swift b/Sources/Core/Tracker/TrackerControllerImpl.swift
index afc0a5f90..20b3367a4 100644
--- a/Sources/Core/Tracker/TrackerControllerImpl.swift
+++ b/Sources/Core/Tracker/TrackerControllerImpl.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -69,10 +69,64 @@ 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)
}
+
+ func decorateLink(_ url: URL) -> URL? {
+ self.decorateLink(url, extendedParameters: CrossDeviceParameterConfiguration())
+ }
+
+ func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration) -> URL? {
+ var userId: String
+ switch self.session?.userId {
+ case .none:
+ logError(message: "\(url) could not be decorated as session.userId is nil")
+ return nil
+ case .some(let id):
+ userId = id
+ }
+
+ let sessionId = extendedParameters.sessionId ? self.session?.sessionId ?? "" : ""
+ if (extendedParameters.sessionId && sessionId.isEmpty) {
+ logDebug(message: "\(decorateLinkErrorTemplate("sessionId")) Ensure an event has been tracked to generate a session before calling this method.")
+ }
+
+ let sourceId = extendedParameters.sourceId ? self.appId : ""
+
+ let sourcePlatform = extendedParameters.sourcePlatform ? devicePlatformToString(self.devicePlatform) : ""
+
+ let subjectUserId = extendedParameters.subjectUserId ? self.subject?.userId ?? "" : ""
+ if (extendedParameters.subjectUserId && subjectUserId.isEmpty) {
+ logDebug(message: "\(decorateLinkErrorTemplate("subjectUserId")) Ensure SubjectConfiguration.userId has been set on your tracker.")
+ }
+
+ let reason = extendedParameters.reason ?? ""
+
+ let spParameters = [
+ userId,
+ String(Int(Date().timeIntervalSince1970 * 1000)),
+ sessionId,
+ subjectUserId.toBase64(),
+ sourceId.toBase64(),
+ sourcePlatform,
+ reason.toBase64()
+ ].joined(separator: ".").trimmingCharacters(in: ["."])
+
+ var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
+ let spQueryParam = URLQueryItem(name: "_sp", value: spParameters)
+
+ // Modification requires exclusive access, we must make a copy
+ let queryItems = components?.queryItems
+ components?.queryItems = (queryItems?.filter { $0.name != "_sp" } ?? []) + [spQueryParam]
+
+ return components?.url
+ }
// MARK: - Properties' setters and getters
@@ -157,6 +211,11 @@ class TrackerControllerImpl: Controller, TrackerController {
tracker.subject?.platformContextProperties = newValue
}
}
+
+ var platformContextRetriever: PlatformContextRetriever? {
+ get { return tracker.subject?.platformContextRetriever }
+ set { if let retriever = newValue { tracker.subject?.platformContextRetriever = retriever } }
+ }
var geoLocationContext: Bool {
get {
@@ -237,6 +296,14 @@ class TrackerControllerImpl: Controller, TrackerController {
tracker.autotrackScreenViews = newValue
}
}
+
+ var screenEngagementAutotracking: Bool {
+ get { return tracker.screenEngagementAutotracking }
+ set {
+ dirtyConfig.screenEngagementAutotracking = newValue
+ tracker.screenEngagementAutotracking = newValue
+ }
+ }
var trackerVersionSuffix: String? {
get {
@@ -269,14 +336,24 @@ class TrackerControllerImpl: Controller, TrackerController {
tracker.userAnonymisation = newValue
}
}
+
+ var immersiveSpaceContext: Bool {
+ get {
+ return tracker.immersiveSpaceContext
+ }
+ set {
+ dirtyConfig.immersiveSpaceContext = newValue
+ tracker.immersiveSpaceContext = newValue
+ }
+ }
var advertisingIdentifierRetriever: (() -> UUID?)? {
get {
- return tracker.advertisingIdentifierRetriever
+ return tracker.subject?.advertisingIdentifierRetriever
}
set {
dirtyConfig.advertisingIdentifierRetriever = newValue
- tracker.advertisingIdentifierRetriever = newValue
+ tracker.subject?.advertisingIdentifierRetriever = newValue
}
}
@@ -301,4 +378,8 @@ class TrackerControllerImpl: Controller, TrackerController {
private var dirtyConfig: TrackerConfiguration {
return serviceProvider.trackerConfiguration
}
+
+ private func decorateLinkErrorTemplate(_ extendedParameterName: String) -> String {
+ "\(extendedParameterName) has been requested in CrossDeviceParameterConfiguration, but it is not set."
+ }
}
diff --git a/Sources/Core/Tracker/TrackerDefaults.swift b/Sources/Core/Tracker/TrackerDefaults.swift
index 30b0f9a90..b5e79f7f4 100644
--- a/Sources/Core/Tracker/TrackerDefaults.swift
+++ b/Sources/Core/Tracker/TrackerDefaults.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -24,11 +24,13 @@ class TrackerDefaults {
private(set) static var screenContext = true
private(set) static var applicationContext = true
private(set) static var autotrackScreenViews = true
- private(set) static var lifecycleEvents = false
+ private(set) static var lifecycleEvents = true
private(set) static var exceptionEvents = true
private(set) static var installEvent = true
private(set) static var trackerDiagnostic = false
private(set) static var userAnonymisation = false
private(set) static var platformContext = true
private(set) static var geoLocationContext = false
+ private(set) static var screenEngagementAutotracking = true
+ private(set) static var immersiveSpaceContext = true
}
diff --git a/Sources/Core/Tracker/TrackerEvent.swift b/Sources/Core/Tracker/TrackerEvent.swift
index eee18f3f4..371708464 100644
--- a/Sources/Core/Tracker/TrackerEvent.swift
+++ b/Sources/Core/Tracker/TrackerEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -39,8 +39,8 @@ class TrackerEvent : InspectableEvent, StateMachineEvent {
private(set) var isService: Bool
- init(event: Event, state: TrackerStateSnapshot? = nil) {
- eventId = UUID()
+ init(event: Event, eventId: UUID = UUID(), state: TrackerStateSnapshot? = nil) {
+ self.eventId = eventId
timestamp = Int64(Date().timeIntervalSince1970 * 1000)
trueTimestamp = event.trueTimestamp
entities = event.entities
diff --git a/Sources/Core/Tracker/WebViewMessageHandler.swift b/Sources/Core/Tracker/WebViewMessageHandler.swift
index 04873c70b..93a0c79fd 100644
--- a/Sources/Core/Tracker/WebViewMessageHandler.swift
+++ b/Sources/Core/Tracker/WebViewMessageHandler.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -13,7 +13,7 @@
import Foundation
-#if os(iOS) || os(macOS)
+#if os(iOS) || os(macOS) || os(visionOS)
import WebKit
/// Handler for messages from the JavaScript library embedded in Web views.
diff --git a/Sources/Core/TrackerConstants.swift b/Sources/Core/TrackerConstants.swift
index 253749b51..dbc1811f9 100644
--- a/Sources/Core/TrackerConstants.swift
+++ b/Sources/Core/TrackerConstants.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -14,7 +14,7 @@
import Foundation
// --- Version
-let kSPRawVersion = "5.6.0"
+let kSPRawVersion = "6.0.0"
#if os(iOS)
let kSPVersion = "ios-\(kSPRawVersion)"
#elseif os(tvOS)
@@ -60,6 +60,7 @@ let kSPErrorSchema = "iglu:com.snowplowanalytics.snowplow/application_error/json
let kSPApplicationInstallSchema = "iglu:com.snowplowanalytics.mobile/application_install/jsonschema/1-0-0"
let kSPGdprContextSchema = "iglu:com.snowplowanalytics.snowplow/gdpr/jsonschema/1-0-0"
let kSPDiagnosticErrorSchema = "iglu:com.snowplowanalytics.snowplow/diagnostic_error/jsonschema/1-0-0"
+
let ecommerceActionSchema = "iglu:com.snowplowanalytics.snowplow.ecommerce/snowplow_ecommerce_action/jsonschema/1-0-2"
let ecommerceProductSchema = "iglu:com.snowplowanalytics.snowplow.ecommerce/product/jsonschema/1-0-0"
let ecommerceCartSchema = "iglu:com.snowplowanalytics.snowplow.ecommerce/cart/jsonschema/1-0-0"
@@ -70,6 +71,17 @@ let ecommercePromotionSchema = "iglu:com.snowplowanalytics.snowplow.ecommerce/pr
let ecommerceRefundSchema = "iglu:com.snowplowanalytics.snowplow.ecommerce/refund/jsonschema/1-0-0"
let ecommerceUserSchema = "iglu:com.snowplowanalytics.snowplow.ecommerce/user/jsonschema/1-0-0"
let ecommercePageSchema = "iglu:com.snowplowanalytics.snowplow.ecommerce/page/jsonschema/1-0-0"
+let kSPScreenEndSchema = "iglu:com.snowplowanalytics.mobile/screen_end/jsonschema/1-0-0"
+let kSPScreenSummarySchema = "iglu:com.snowplowanalytics.mobile/screen_summary/jsonschema/1-0-0"
+let kSPListItemViewSchema = "iglu:com.snowplowanalytics.mobile/list_item_view/jsonschema/1-0-0"
+let kSPScrollChangedSchema = "iglu:com.snowplowanalytics.mobile/scroll_changed/jsonschema/1-0-0"
+
+let swiftuiOpenWindowSchema = "iglu:com.apple.swiftui/open_window/jsonschema/1-0-0"
+let swiftuiDismissWindowSchema = "iglu:com.apple.swiftui/dismiss_window/jsonschema/1-0-0"
+let swiftuiOpenImmersiveSpaceSchema = "iglu:com.apple.swiftui/open_immersive_space/jsonschema/1-0-0"
+let swiftuiDismissImmersiveSpaceSchema = "iglu:com.apple.swiftui/dismiss_immersive_space/jsonschema/1-0-0"
+let swiftuiWindowGroupSchema = "iglu:com.apple.swiftui/window_group/jsonschema/1-0-0"
+let swiftuiImmersiveSpaceSchema = "iglu:com.apple.swiftui/immersive_space/jsonschema/1-0-0"
// --- Event Keys
let kSPEventPageView = "pv"
diff --git a/Sources/Core/Utils/DataPersistence.swift b/Sources/Core/Utils/DataPersistence.swift
index 9d2d3b887..e854ab1d6 100644
--- a/Sources/Core/Utils/DataPersistence.swift
+++ b/Sources/Core/Utils/DataPersistence.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/Core/Utils/DeviceInfoMonitor.swift b/Sources/Core/Utils/DeviceInfoMonitor.swift
index 4d03ddf17..0e3809189 100644
--- a/Sources/Core/Utils/DeviceInfoMonitor.swift
+++ b/Sources/Core/Utils/DeviceInfoMonitor.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -19,7 +19,7 @@ import WatchKit
#if os(iOS)
import CoreTelephony
#endif
-#if os(iOS) || os(tvOS)
+#if os(iOS) || os(tvOS) || os(visionOS)
import UIKit
#endif
@@ -28,7 +28,7 @@ class DeviceInfoMonitor {
/// Returns the generated identifier for vendors. More info can be found in UIDevice's identifierForVendor documentation.
/// - Returns: A string containing a formatted UUID for example E621E1F8-C36C-495A-93FC-0C247A3E6E5F.
var appleIdfv: String? {
- #if os(iOS) || os(tvOS)
+ #if os(iOS) || os(tvOS) || os(visionOS)
if let idfv = UIDevice.current.identifierForVendor?.uuidString {
return idfv
}
@@ -38,15 +38,15 @@ class DeviceInfoMonitor {
/// Returns the current device's vendor in the form of a string.
/// - Returns: A string with vendor, i.e. "Apple Inc."
- var deviceVendor: String? {
+ var deviceVendor: String {
return "Apple Inc."
}
/// Returns the current device's model in the form of a string.
/// - Returns: A string with device model.
- var deviceModel: String? {
+ var deviceModel: String {
let simulatorModel = (ProcessInfo.processInfo.environment)["SIMULATOR_MODEL_IDENTIFIER"]
- if simulatorModel != nil {
+ if let simulatorModel = simulatorModel {
return simulatorModel
}
@@ -59,8 +59,8 @@ class DeviceInfoMonitor {
/// This is to detect what the version of mobile OS of the current device.
/// - Returns: The current device's OS version type as a string.
- var osVersion: String? {
- #if os(iOS) || os(tvOS)
+ var osVersion: String {
+ #if os(iOS) || os(tvOS) || os(visionOS)
return UIDevice.current.systemVersion
#elseif os(watchOS)
return WKInterfaceDevice.current().systemVersion
@@ -78,13 +78,15 @@ class DeviceInfoMonitor {
#endif
}
- var osType: String? {
+ var osType: String {
#if os(iOS)
return "ios"
#elseif os(tvOS)
return "tvos"
#elseif os(watchOS)
return "watchos"
+ #elseif os(visionOS)
+ return "visionos"
#else
return "osx"
#endif
@@ -146,7 +148,7 @@ class DeviceInfoMonitor {
/// Returns the Network Type the device is connected to.
/// - Returns: A string containing the Network Type.
var networkType: String? {
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
let networkStatus = SNOWReachability.forInternetConnection()?.networkStatus
switch networkStatus {
case .offline:
@@ -165,7 +167,7 @@ class DeviceInfoMonitor {
/// Returns remaining battery level as an integer percentage of total battery capacity.
/// - Returns: Battery level.
var batteryLevel: Int? {
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
let batteryLevel = UIDevice.current.batteryLevel
if batteryLevel != Float(UIDevice.BatteryState.unknown.rawValue) && batteryLevel >= 0 {
return Int(batteryLevel * 100)
@@ -177,7 +179,7 @@ class DeviceInfoMonitor {
/// Returns battery state for the device.
/// - Returns: One of "charging", "full", "unplugged" or NULL
var batteryState: String? {
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
switch UIDevice.current.batteryState {
case .charging:
return "charging"
@@ -196,7 +198,7 @@ class DeviceInfoMonitor {
/// Returns whether low power mode is activated.
/// - Returns: Boolean indicating the state of low power mode.
var isLowPowerModeEnabled: Bool? {
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
return ProcessInfo.processInfo.isLowPowerModeEnabled
#else
return nil
@@ -221,36 +223,6 @@ class DeviceInfoMonitor {
// #endif
return nil
}
-
- /// Returns number of bytes of storage remaining. The information is requested from the home directory.
- /// - Returns: Bytes of storage remaining.
- var availableStorage: Int64? {
- #if os(iOS)
- let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)
- do {
- let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
- return values.volumeAvailableCapacityForImportantUsage
- } catch {
- logError(message: "Failed to read available storage size: \(error.localizedDescription)")
- }
- #endif
- return nil
- }
-
- /// Returns the total number of bytes of storage. The information is requested from the home directory.
- /// - Returns: Total size of storage in bytes.
- var totalStorage: Int? {
- #if os(iOS)
- let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)
- do {
- let values = try fileURL.resourceValues(forKeys: [.volumeTotalCapacityKey])
- return values.volumeTotalCapacity
- } catch {
- logError(message: "Failed to read available storage size: \(error.localizedDescription)")
- }
- #endif
- return nil
- }
/// Whether the device orientation is portrait (either upright or upside down)
var isPortrait: Bool? {
diff --git a/Sources/Core/Utils/SNOWReachability.swift b/Sources/Core/Utils/SNOWReachability.swift
index 84d4d9ff1..58ab49d1f 100644
--- a/Sources/Core/Utils/SNOWReachability.swift
+++ b/Sources/Core/Utils/SNOWReachability.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -11,7 +11,7 @@
// express or implied. See the Apache License Version 2.0 for the specific
// language governing permissions and limitations there under.
-#if os(iOS)
+#if os(iOS) || os(visionOS)
import Foundation
import SystemConfiguration
@@ -65,7 +65,7 @@ class SNOWReachability: NSObject {
return .offline
}
- #if os(iOS)
+ #if os(iOS) || os(visionOS)
let isWWAN = (flags.rawValue & SCNetworkReachabilityFlags.isWWAN.rawValue) == SCNetworkReachabilityFlags.isWWAN.rawValue
if isWWAN {
return .wwan
diff --git a/Sources/Core/Utils/Stringb64.swift b/Sources/Core/Utils/Stringb64.swift
new file mode 100644
index 000000000..bd22a2049
--- /dev/null
+++ b/Sources/Core/Utils/Stringb64.swift
@@ -0,0 +1,33 @@
+// Copyright (c) 2013-present 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
+
+extension String {
+ func toBase64(urlSafe: Bool = true) -> String {
+ var encoded = Data(self.utf8).base64EncodedString()
+ if urlSafe {
+ // We need URL safe with no padding. Since there is no built-in way to do this, we transform
+ // the encoded payload to make it URL safe by replacing chars that are different in the URL-safe
+ // alphabet. Namely, 62 is - instead of +, and 63 _ instead of /.
+ // See: https://tools.ietf.org/html/rfc4648#section-5
+ encoded = encoded
+ .replacingOccurrences(of: "/", with: "_")
+ .replacingOccurrences(of: "+", with: "-")
+
+ // There is also no padding since the length is implicitly known.
+ encoded = encoded.trimmingCharacters(in: CharacterSet(charactersIn: "="))
+ }
+ return encoded
+ }
+}
diff --git a/Sources/Core/Utils/Utilities.swift b/Sources/Core/Utils/Utilities.swift
index 9dc060bda..81617904a 100644
--- a/Sources/Core/Utils/Utilities.swift
+++ b/Sources/Core/Utils/Utilities.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -11,12 +11,10 @@
// express or implied. See the Apache License Version 2.0 for the specific
// language governing permissions and limitations there under.
-#if os(iOS)
+#if os(iOS) || os(tvOS) || os(visionOS)
import UIKit
#elseif os(macOS)
import AppKit
-#elseif os(tvOS)
-import UIKit
#elseif os(watchOS)
import WatchKit
#endif
@@ -38,8 +36,13 @@ class Utilities {
/// Returns the platform type of the device..
/// - Returns: A string of the platform type.
class var platform: DevicePlatform {
- #if os(iOS)
+ #if os(iOS) || os(visionOS) || os(watchOS)
return .mobile
+// TODO: use the headset platform by default in visionOS once Enrich 4 is commonly used
+// #elseif os(visionOS)
+// return .headset
+ #elseif os(tvOS)
+ return .connectedTV
#else
return .desktop
#endif
diff --git a/Sources/Snowplow/Configurations/ConfigurationBuilder.swift b/Sources/Snowplow/Configurations/ConfigurationBuilder.swift
index 9bc845410..17487da25 100644
--- a/Sources/Snowplow/Configurations/ConfigurationBuilder.swift
+++ b/Sources/Snowplow/Configurations/ConfigurationBuilder.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/ConfigurationBundle.swift b/Sources/Snowplow/Configurations/ConfigurationBundle.swift
index a792659b1..c9bdf0c46 100644
--- a/Sources/Snowplow/Configurations/ConfigurationBundle.swift
+++ b/Sources/Snowplow/Configurations/ConfigurationBundle.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/ConfigurationProtocol.swift b/Sources/Snowplow/Configurations/ConfigurationProtocol.swift
index 809d93471..85410beed 100644
--- a/Sources/Snowplow/Configurations/ConfigurationProtocol.swift
+++ b/Sources/Snowplow/Configurations/ConfigurationProtocol.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/ConfigurationState.swift b/Sources/Snowplow/Configurations/ConfigurationState.swift
index 23c86449b..bc3a9741a 100644
--- a/Sources/Snowplow/Configurations/ConfigurationState.swift
+++ b/Sources/Snowplow/Configurations/ConfigurationState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/EmitterConfiguration.swift b/Sources/Snowplow/Configurations/EmitterConfiguration.swift
index 2d04f1f5b..40e85500e 100644
--- a/Sources/Snowplow/Configurations/EmitterConfiguration.swift
+++ b/Sources/Snowplow/Configurations/EmitterConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -16,7 +16,7 @@ import Foundation
@objc(SPEmitterConfigurationProtocol)
public protocol EmitterConfigurationProtocol: AnyObject {
/// Sets whether the buffer should send events instantly or after the buffer
- /// has reached it's limit. By default, this is set to BufferOption Default.
+ /// has reached it's limit. By default, this is set to BufferOption single.
@objc
var bufferOption: BufferOption { get set }
/// Maximum number of events collected from the EventStore to be sent in a request.
@@ -45,6 +45,14 @@ public protocol EmitterConfigurationProtocol: AnyObject {
/// If disabled, events that failed to be sent will be dropped regardless of other configuration (such as the customRetryForStatusCodes).
@objc
var retryFailedRequests: Bool { get set }
+ /// Limit for the maximum number of unsent events to keep in the event store.
+ /// Defaults to 1000.
+ @objc
+ var maxEventStoreSize: Int64 { get set }
+ /// Limit for the maximum duration of how long events should be kept in the event store if they fail to be sent.
+ /// Defaults to 30 days.
+ @objc
+ var maxEventStoreAge: TimeInterval { get set }
}
/// It allows the tracker configuration from the emission perspective.
@@ -136,6 +144,24 @@ public class EmitterConfiguration: SerializableConfiguration, EmitterConfigurati
set { _retryFailedRequests = newValue }
}
+ private var _maxEventStoreSize: Int64?
+ /// Limit for the maximum number of unsent events to keep in the event store.
+ /// Defaults to 1000.
+ @objc
+ public var maxEventStoreSize: Int64 {
+ get { return _maxEventStoreSize ?? sourceConfig?.maxEventStoreSize ?? EmitterDefaults.maxEventStoreSize }
+ set { _maxEventStoreSize = newValue }
+ }
+
+ private var _maxEventStoreAge: TimeInterval?
+ /// Limit for the maximum duration of how long events should be kept in the event store if they fail to be sent.
+ /// Defaults to 30 days.
+ @objc
+ public var maxEventStoreAge: TimeInterval {
+ get { return _maxEventStoreAge ?? sourceConfig?.maxEventStoreAge ?? EmitterDefaults.maxEventStoreAge }
+ set { _maxEventStoreAge = newValue }
+ }
+
// MARK: - Internal
/// Fallback configuration to read from in case requested values are not present in this configuraiton.
@@ -258,6 +284,22 @@ public class EmitterConfiguration: SerializableConfiguration, EmitterConfigurati
return self
}
+ /// Limit for the maximum number of unsent events to keep in the event store.
+ /// Defaults to 1000.
+ @objc
+ public func maxEventStoreSize(_ maxEventStoreSize: Int64) -> Self {
+ self.maxEventStoreSize = maxEventStoreSize
+ return self
+ }
+
+ /// Limit for the maximum duration of how long events should be kept in the event store if they fail to be sent.
+ /// Defaults to 30 days.
+ @objc
+ public func maxEventStoreAge(_ maxEventStoreAge: TimeInterval) -> Self {
+ self.maxEventStoreAge = maxEventStoreAge
+ return self
+ }
+
// MARK: - NSCopying
@objc
@@ -273,6 +315,8 @@ public class EmitterConfiguration: SerializableConfiguration, EmitterConfigurati
copy.serverAnonymisation = serverAnonymisation
copy.eventStore = eventStore
copy.retryFailedRequests = retryFailedRequests
+ copy.maxEventStoreAge = maxEventStoreAge
+ copy.maxEventStoreSize = maxEventStoreSize
return copy
}
@@ -291,6 +335,8 @@ public class EmitterConfiguration: SerializableConfiguration, EmitterConfigurati
coder.encode(customRetryForStatusCodes, forKey: "customRetryForStatusCodes")
coder.encode(serverAnonymisation, forKey: "serverAnonymisation")
coder.encode(retryFailedRequests, forKey: "retryFailedRequests")
+ coder.encode(maxEventStoreAge, forKey: "maxEventStoreAge")
+ coder.encode(maxEventStoreSize, forKey: "maxEventStoreSize")
}
required init?(coder: NSCoder) {
@@ -309,5 +355,11 @@ public class EmitterConfiguration: SerializableConfiguration, EmitterConfigurati
if coder.containsValue(forKey: "retryFailedRequests") {
retryFailedRequests = coder.decodeBool(forKey: "retryFailedRequests")
}
+ if coder.containsValue(forKey: "maxEventStoreAge") {
+ maxEventStoreAge = coder.decodeDouble(forKey: "maxEventStoreAge")
+ }
+ if coder.containsValue(forKey: "maxEventStoreSize") {
+ maxEventStoreSize = coder.decodeInt64(forKey: "maxEventStoreSize")
+ }
}
}
diff --git a/Sources/Snowplow/Configurations/FocalMeterConfiguration.swift b/Sources/Snowplow/Configurations/FocalMeterConfiguration.swift
index 62df08806..61d423be3 100644
--- a/Sources/Snowplow/Configurations/FocalMeterConfiguration.swift
+++ b/Sources/Snowplow/Configurations/FocalMeterConfiguration.swift
@@ -2,7 +2,7 @@
// FocalMeterConfiguration.swift
// Snowplow
//
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/GDPRConfiguration.swift b/Sources/Snowplow/Configurations/GDPRConfiguration.swift
index c36f75fbd..aecd4b312 100644
--- a/Sources/Snowplow/Configurations/GDPRConfiguration.swift
+++ b/Sources/Snowplow/Configurations/GDPRConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/GlobalContextsConfiguration.swift b/Sources/Snowplow/Configurations/GlobalContextsConfiguration.swift
index 32e6c38b9..f4b9c9474 100644
--- a/Sources/Snowplow/Configurations/GlobalContextsConfiguration.swift
+++ b/Sources/Snowplow/Configurations/GlobalContextsConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/NetworkConfiguration.swift b/Sources/Snowplow/Configurations/NetworkConfiguration.swift
index 3cdd8191d..4eaf94c73 100644
--- a/Sources/Snowplow/Configurations/NetworkConfiguration.swift
+++ b/Sources/Snowplow/Configurations/NetworkConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -65,12 +65,18 @@ public class NetworkConfiguration: SerializableConfiguration, ConfigurationProto
set { _requestHeaders = newValue }
}
+ private var _timeout: TimeInterval?
+ /// The maximum timeout for emitting events to the collector.
+ /// Defaults to 30 seconds.
+ @objc var timeout: TimeInterval {
+ get { return _timeout ?? sourceConfig?.timeout ?? EmitterDefaults.emitTimeout }
+ set { _timeout = newValue }
+ }
+
// MARK: - Internal
/// Fallback configuration to read from in case requested values are not present in this configuraiton.
internal var sourceConfig: NetworkConfiguration?
-
- // TODO: add -> @property () NSInteger timeout;
internal override init() {
}
@@ -132,6 +138,14 @@ public class NetworkConfiguration: SerializableConfiguration, ConfigurationProto
self.requestHeaders = headers
return self
}
+
+ /// The maximum timeout for emitting events to the collector.
+ /// Defaults to 30 seconds.
+ @objc
+ public func timeout(_ timeout: TimeInterval) -> Self {
+ self.timeout = timeout
+ return self
+ }
// MARK: - NSCopying
@@ -144,6 +158,7 @@ public class NetworkConfiguration: SerializableConfiguration, ConfigurationProto
copy = NetworkConfiguration(endpoint: endpoint ?? "", method: method )
}
copy?.customPostPath = customPostPath
+ copy?.timeout = timeout
return copy!
}
@@ -159,6 +174,7 @@ public class NetworkConfiguration: SerializableConfiguration, ConfigurationProto
coder.encode(method.rawValue, forKey: "method")
coder.encode(customPostPath, forKey: "customPostPath")
coder.encode(requestHeaders, forKey: "requestHeaders")
+ coder.encode(timeout, forKey: "timeout")
}
required init?(coder: NSCoder) {
@@ -167,5 +183,6 @@ public class NetworkConfiguration: SerializableConfiguration, ConfigurationProto
_method = HttpMethodOptions(rawValue: coder.decodeInteger(forKey: "method"))
_customPostPath = coder.decodeObject(forKey: "customPostPath") as? String
_requestHeaders = coder.decodeObject(forKey: "requestHeaders") as? [String : String]
+ _timeout = coder.decodeObject(forKey: "timeout") as? TimeInterval
}
}
diff --git a/Sources/Snowplow/Configurations/PlatformContextProperty.swift b/Sources/Snowplow/Configurations/PlatformContextProperty.swift
index d1efa3d63..307128a3f 100644
--- a/Sources/Snowplow/Configurations/PlatformContextProperty.swift
+++ b/Sources/Snowplow/Configurations/PlatformContextProperty.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -13,39 +13,41 @@
import Foundation
-/// Optional properties tracked in the platform context entity
+/// Optional properties tracked in the platform context entity.
public enum PlatformContextProperty: Int {
- /// The carrier of the SIM inserted in the device
+ /// The carrier of the SIM inserted in the device.
case carrier
- /// Type of network the device is connected to
+ /// Type of network the device is connected to.
case networkType
- /// Radio access technology that the device is using
+ /// Radio access technology that the device is using.
case networkTechnology
- /// Advertising identifier on iOS
+ /// Advertising identifier on iOS.
case appleIdfa
- /// UUID identifier for vendors on iOS
+ /// UUID identifier for vendors on iOS.
case appleIdfv
- /// Total physical system memory in bytes
+ /// Total physical system memory in bytes.
case physicalMemory
- /// Amount of memory in bytes available to the current app
+ /// Amount of memory in bytes available to the current app.
/// The property is not tracked in the current version of the tracker due to the tracker not being able to access the API, see the issue here: https://github.com/snowplow/snowplow-ios-tracker/issues/772
case appAvailableMemory
- /// Remaining battery level as an integer percentage of total battery capacity
+ /// Remaining battery level as an integer percentage of total battery capacity.
case batteryLevel
- /// Battery state for the device
+ /// Battery state for the device.
case batteryState
- /// A Boolean indicating whether Low Power Mode is enabled
+ /// A Boolean indicating whether Low Power Mode is enabled.
case lowPowerMode
- /// Bytes of storage remaining
+ /// Bytes of storage remaining.
+ /// Note: This is not automatically assigned by the tracker as it may be considered as fingerprinting. You can assign it using the PlatformContextRetriever.
case availableStorage
/// Total size of storage in bytes
+ /// Note: This is not automatically assigned by the tracker as it may be considered as fingerprinting. You can assign it using the PlatformContextRetriever.
case totalStorage
- /// A Boolean indicating whether the device orientation is portrait (either upright or upside down)
+ /// A Boolean indicating whether the device orientation is portrait (either upright or upside down).
case isPortrait
- /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes
+ /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes.
case resolution
- /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS)
+ /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS).
case scale
- /// System language currently used on the device (ISO 639)
+ /// System language currently used on the device (ISO 639).
case language
}
diff --git a/Sources/Snowplow/Configurations/PluginConfiguration.swift b/Sources/Snowplow/Configurations/PluginConfiguration.swift
index e3e631c4a..fe8cd3b27 100644
--- a/Sources/Snowplow/Configurations/PluginConfiguration.swift
+++ b/Sources/Snowplow/Configurations/PluginConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/RemoteConfiguration.swift b/Sources/Snowplow/Configurations/RemoteConfiguration.swift
index 4f61ecd9b..e26aaf5d8 100644
--- a/Sources/Snowplow/Configurations/RemoteConfiguration.swift
+++ b/Sources/Snowplow/Configurations/RemoteConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/SerializableConfiguration.swift b/Sources/Snowplow/Configurations/SerializableConfiguration.swift
index b11e8a496..cc4299824 100644
--- a/Sources/Snowplow/Configurations/SerializableConfiguration.swift
+++ b/Sources/Snowplow/Configurations/SerializableConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/SessionConfiguration.swift b/Sources/Snowplow/Configurations/SessionConfiguration.swift
index 646983941..d2e1226ea 100644
--- a/Sources/Snowplow/Configurations/SessionConfiguration.swift
+++ b/Sources/Snowplow/Configurations/SessionConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/SubjectConfiguration.swift b/Sources/Snowplow/Configurations/SubjectConfiguration.swift
index b910f14d6..54e2f8953 100644
--- a/Sources/Snowplow/Configurations/SubjectConfiguration.swift
+++ b/Sources/Snowplow/Configurations/SubjectConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Configurations/TrackerConfiguration.swift b/Sources/Snowplow/Configurations/TrackerConfiguration.swift
index 5e940aacc..5fcf85001 100644
--- a/Sources/Snowplow/Configurations/TrackerConfiguration.swift
+++ b/Sources/Snowplow/Configurations/TrackerConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -31,43 +31,51 @@ public protocol TrackerConfigurationProtocol: AnyObject {
/// It sets the logger delegate that receive logs from the tracker.
@objc
var loggerDelegate: LoggerDelegate? { get set }
- /// Whether application context is sent with all the tracked events.
+ /// Whether the application context entity is sent with all the tracked events.
@objc
var applicationContext: Bool { get set }
- /// Whether mobile/platform context is sent with all the tracked events.
+ /// Whether the mobile/platform context entity is sent with all the tracked events.
@objc
var platformContext: Bool { get set }
- /// Whether geo-location context is sent with all the tracked events.
+ /// Whether the geo-location context entity is sent with all the tracked events.
@objc
var geoLocationContext: Bool { get set }
- /// Whether session context is sent with all the tracked events.
+ /// Whether the session context entity is sent with all the tracked events.
@objc
var sessionContext: Bool { get set }
- /// Whether deepLink context is sent with all the ScreenView events.
+ /// Whether the deepLink context entity is sent with all the ScreenView events.
@objc
var deepLinkContext: Bool { get set }
- /// Whether screen context is sent with all the tracked events.
+ /// Whether the screen context entity is sent with all the tracked events.
@objc
var screenContext: Bool { get set }
- /// Whether enable automatic tracking of ScreenView events.
+ /// Whether to enable automatic tracking of ScreenView events.
@objc
var screenViewAutotracking: Bool { get set }
- /// Whether enable automatic tracking of background and foreground transitions.
+ /// Whether to enable tracking of the screen end event and the screen summary context entity.
+ /// Make sure that you have lifecycle autotracking enabled for screen summary to have complete information.
+ @objc
+ var screenEngagementAutotracking: Bool { get set }
+ /// Whether to enable automatic tracking of background and foreground transitions.
+ /// Enabled by default.
@objc
var lifecycleAutotracking: Bool { get set }
- /// Whether enable automatic tracking of install event.
+ /// Whether to enable automatic tracking of install event.
@objc
var installAutotracking: Bool { get set }
- /// Whether enable crash reporting.
+ /// Whether to enable crash reporting.
@objc
var exceptionAutotracking: Bool { get set }
- /// Whether enable diagnostic reporting.
+ /// Whether to enable diagnostic reporting.
@objc
var diagnosticAutotracking: Bool { get set }
/// Whether to anonymise client-side user identifiers in session (userId, previousSessionId), subject (userId, networkUserId, domainUserId, ipAddress) and platform context entities (IDFA)
/// Setting this property on a running tracker instance starts a new session (if sessions are tracked).
@objc
var userAnonymisation: Bool { get set }
+ /// Whether the immersive space context entity should be sent with events tracked within an immersive space (visionOS).
+ @objc
+ var immersiveSpaceContext: Bool { get set }
/// Decorate the v_tracker field in the tracker protocol.
/// @note Do not use. Internal use only.
@objc
@@ -82,6 +90,10 @@ public protocol PlatformContextConfigurationProtocol {
/// List of properties of the platform context to track. If not passed and `platformContext` is enabled, all available properties will be tracked.
/// The required `osType`, `osVersion`, `deviceManufacturer`, and `deviceModel` properties will be tracked in the entity regardless of this setting.
var platformContextProperties: [PlatformContextProperty]? { get set }
+
+ /// Set of callbacks to be used to retrieve properties of the platform context.
+ /// Overrides the tracker implementation for setting the properties.
+ var platformContextRetriever: PlatformContextRetriever? { get set }
}
/// This class represents the configuration of the tracker and the core tracker properties.
@@ -130,7 +142,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _applicationContext: Bool?
- /// Whether application context is sent with all the tracked events.
+ /// Whether the application context entity is sent with all the tracked events.
@objc
public var applicationContext: Bool {
get { return _applicationContext ?? sourceConfig?.applicationContext ?? TrackerDefaults.applicationContext }
@@ -138,7 +150,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _platformContext: Bool?
- /// Whether mobile/platform context is sent with all the tracked events.
+ /// Whether the mobile/platform context entity is sent with all the tracked events.
@objc
public var platformContext: Bool {
get { return _platformContext ?? sourceConfig?.platformContext ?? TrackerDefaults.platformContext }
@@ -146,7 +158,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _geoLocationContext: Bool?
- /// Whether geo-location context is sent with all the tracked events.
+ /// Whether the geo-location context entity is sent with all the tracked events.
@objc
public var geoLocationContext: Bool {
get { return _geoLocationContext ?? sourceConfig?.geoLocationContext ?? TrackerDefaults.geoLocationContext }
@@ -154,7 +166,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _sessionContext: Bool?
- /// Whether session context is sent with all the tracked events.
+ /// Whether the session context entity is sent with all the tracked events.
@objc
public var sessionContext: Bool {
get { return _sessionContext ?? sourceConfig?.sessionContext ?? TrackerDefaults.sessionContext }
@@ -162,7 +174,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _deepLinkContext: Bool?
- /// Whether deepLink context is sent with all the ScreenView events.
+ /// Whether the deepLink context entity is sent with all the ScreenView events.
@objc
public var deepLinkContext: Bool {
get { return _deepLinkContext ?? sourceConfig?.deepLinkContext ?? TrackerDefaults.deepLinkContext }
@@ -170,7 +182,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _screenContext: Bool?
- /// Whether screen context is sent with all the tracked events.
+ /// Whether the screen context entity is sent with all the tracked events.
@objc
public var screenContext: Bool {
get { return _screenContext ?? sourceConfig?.screenContext ?? TrackerDefaults.screenContext }
@@ -178,15 +190,25 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _screenViewAutotracking: Bool?
- /// Whether enable automatic tracking of ScreenView events.
+ /// Whether to enable automatic tracking of ScreenView events.
@objc
public var screenViewAutotracking: Bool {
get { return _screenViewAutotracking ?? sourceConfig?.screenViewAutotracking ?? TrackerDefaults.autotrackScreenViews }
set { _screenViewAutotracking = newValue }
}
+ private var _screenEngagementAutotracking: Bool?
+ /// Whether to enable tracking of the screen end event and the screen summary context entity.
+ /// Make sure that you have lifecycle autotracking enabled for screen summary to have complete information.
+ @objc
+ public var screenEngagementAutotracking: Bool {
+ get { return _screenEngagementAutotracking ?? sourceConfig?.screenEngagementAutotracking ?? TrackerDefaults.screenEngagementAutotracking }
+ set { _screenEngagementAutotracking = newValue }
+ }
+
private var _lifecycleAutotracking: Bool?
- /// Whether enable automatic tracking of background and foreground transitions.
+ /// Whether to enable automatic tracking of background and foreground transitions.
+ /// Enabled by default.
@objc
public var lifecycleAutotracking: Bool {
get { return _lifecycleAutotracking ?? sourceConfig?.lifecycleAutotracking ?? TrackerDefaults.lifecycleEvents }
@@ -194,7 +216,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _installAutotracking: Bool?
- /// Whether enable automatic tracking of install event.
+ /// Whether to enable automatic tracking of install event.
@objc
public var installAutotracking: Bool {
get { return _installAutotracking ?? sourceConfig?.installAutotracking ?? TrackerDefaults.installEvent }
@@ -202,7 +224,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _exceptionAutotracking: Bool?
- /// Whether enable crash reporting.
+ /// Whether to enable crash reporting.
@objc
public var exceptionAutotracking: Bool {
get { return _exceptionAutotracking ?? sourceConfig?.exceptionAutotracking ?? TrackerDefaults.exceptionEvents }
@@ -210,7 +232,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
}
private var _diagnosticAutotracking: Bool?
- /// Whether enable diagnostic reporting.
+ /// Whether to enable diagnostic reporting.
@objc
public var diagnosticAutotracking: Bool {
get { return _diagnosticAutotracking ?? sourceConfig?.diagnosticAutotracking ?? TrackerDefaults.trackerDiagnostic }
@@ -226,6 +248,14 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
set { _userAnonymisation = newValue }
}
+ private var _immersiveSpaceContext: Bool?
+ /// Whether the immersive space context entity should be sent with events tracked within an immersive space (visionOS).
+ @objc
+ public var immersiveSpaceContext: Bool {
+ get { return _immersiveSpaceContext ?? sourceConfig?.immersiveSpaceContext ?? TrackerDefaults.immersiveSpaceContext }
+ set { _immersiveSpaceContext = newValue }
+ }
+
private var _trackerVersionSuffix: String?
/// Decorate the v_tracker field in the tracker protocol.
/// @note Do not use. Internal use only.
@@ -240,8 +270,14 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
/// It is called repeatedly (on each tracked event) until a UUID is returned.
@objc
public var advertisingIdentifierRetriever: (() -> UUID?)? {
- get { return _advertisingIdentifierRetriever ?? sourceConfig?.advertisingIdentifierRetriever }
- set { _advertisingIdentifierRetriever = newValue }
+ get { return platformContextRetriever?.appleIdfa }
+ set {
+ if let retriever = platformContextRetriever {
+ retriever.appleIdfa = newValue
+ } else {
+ platformContextRetriever = PlatformContextRetriever(appleIdfa: newValue)
+ }
+ }
}
private var _platformContextProperties: [PlatformContextProperty]?
@@ -252,6 +288,14 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
set { _platformContextProperties = newValue }
}
+ private var _platformContextRetriever: PlatformContextRetriever?
+ /// Set of callbacks to be used to retrieve properties of the platform context.
+ /// Overrides the tracker implementation for setting the properties.
+ public var platformContextRetriever: PlatformContextRetriever? {
+ get { return _platformContextRetriever ?? sourceConfig?.platformContextRetriever }
+ set { _platformContextRetriever = newValue }
+ }
+
// MARK: - Internal
/// Fallback configuration to read from in case requested values are not present in this configuraiton.
@@ -313,6 +357,9 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
if let screenViewAutotracking = dictionary["screenViewAutotracking"] as? Bool {
self.screenViewAutotracking = screenViewAutotracking
}
+ if let screenEngagementAutotracking = dictionary["screenEngagementAutotracking"] as? Bool {
+ self.screenEngagementAutotracking = screenEngagementAutotracking
+ }
if let lifecycleAutotracking = dictionary["lifecycleAutotracking"] as? Bool {
self.lifecycleAutotracking = lifecycleAutotracking
}
@@ -328,6 +375,9 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
if let userAnonymisation = dictionary["userAnonymisation"] as? Bool {
self.userAnonymisation = userAnonymisation
}
+ if let immersiveSpaceContext = dictionary["immersiveSpaceContext"] as? Bool {
+ self.immersiveSpaceContext = immersiveSpaceContext
+ }
}
// MARK: - Builders
@@ -367,91 +417,99 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
return self
}
- /// Whether application context is sent with all the tracked events.
+ /// Whether the application context entity is sent with all the tracked events.
@objc
public func applicationContext(_ applicationContext: Bool) -> Self {
self.applicationContext = applicationContext
return self
}
- /// Whether mobile/platform context is sent with all the tracked events.
+ /// Whether the mobile/platform context entity is sent with all the tracked events.
@objc
public func platformContext(_ platformContext: Bool) -> Self {
self.platformContext = platformContext
return self
}
- /// List of properties of the platform context to track. If not passed and `platformContext` is enabled, all available properties will be tracked.
+ /// List of properties of the platform context entity to track. If not passed and `platformContext` is enabled, all available properties will be tracked.
/// The required `osType`, `osVersion`, `deviceManufacturer`, and `deviceModel` properties will be tracked in the entity regardless of this setting.
public func platformContextProperties(_ platformContextProperties: [PlatformContextProperty]?) -> Self {
self.platformContextProperties = platformContextProperties
return self
}
- /// Whether geo-location context is sent with all the tracked events.
+ /// Whether the geo-location context entity is sent with all the tracked events.
@objc
public func geoLocationContext(_ geoLocationContext: Bool) -> Self {
self.geoLocationContext = geoLocationContext
return self
}
- /// Whether session context is sent with all the tracked events.
+ /// Whether the session context entity is sent with all the tracked events.
@objc
public func sessionContext(_ sessionContext: Bool) -> Self {
self.sessionContext = sessionContext
return self
}
- /// Whether deepLink context is sent with all the ScreenView events.
+ /// Whether the deepLink context entity is sent with all the ScreenView events.
@objc
public func deepLinkContext(_ deepLinkContext: Bool) -> Self {
self.deepLinkContext = deepLinkContext
return self
}
- /// Whether screen context is sent with all the tracked events.
+ /// Whether the screen context entity is sent with all the tracked events.
@objc
public func screenContext(_ screenContext: Bool) -> Self {
self.screenContext = screenContext
return self
}
- /// Whether enable automatic tracking of ScreenView events.
+ /// Whether to enable automatic tracking of ScreenView events.
@objc
public func screenViewAutotracking(_ screenViewAutotracking: Bool) -> Self {
self.screenViewAutotracking = screenViewAutotracking
return self
}
- /// Whether enable automatic tracking of background and foreground transitions.
+ /// Whether to enable tracking of the screen end event and the screen summary context entity.
+ @objc
+ public func screenEngagementAutotracking(_ screenEngagementAutotracking: Bool) -> Self {
+ self.screenEngagementAutotracking = screenEngagementAutotracking
+ return self
+ }
+
+ /// Whether to enable automatic tracking of background and foreground transitions.
+ /// Enabled by default.
@objc
public func lifecycleAutotracking(_ lifecycleAutotracking: Bool) -> Self {
self.lifecycleAutotracking = lifecycleAutotracking
return self
}
- /// Whether enable automatic tracking of install event.
+ /// Whether to enable automatic tracking of install event.
@objc
public func installAutotracking(_ installAutotracking: Bool) -> Self {
self.installAutotracking = installAutotracking
return self
}
- /// Whether enable crash reporting.
+ /// Whether to enable crash reporting.
@objc
public func exceptionAutotracking(_ exceptionAutotracking: Bool) -> Self {
self.exceptionAutotracking = exceptionAutotracking
return self
}
- /// Whether enable diagnostic reporting.
+ /// Whether to enable diagnostic reporting.
@objc
public func diagnosticAutotracking(_ diagnosticAutotracking: Bool) -> Self {
self.diagnosticAutotracking = diagnosticAutotracking
return self
}
- /// Whether to anonymise client-side user identifiers in session (userId, previousSessionId), subject (userId, networkUserId, domainUserId, ipAddress) and platform context entities (IDFA)
+ /// Whether to anonymise client-side user identifiers in session (userId, previousSessionId), subject (userId, networkUserId, domainUserId, ipAddress) and platform context entities (IDFA).
/// Setting this property on a running tracker instance starts a new session (if sessions are tracked).
@objc
public func userAnonymisation(_ userAnonymisation: Bool) -> Self {
@@ -459,6 +517,13 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
return self
}
+ /// Whether the immersive space context entity should be sent with events tracked within an immersive space (visionOS).
+ @objc
+ public func immersiveSpaceContext(_ immersiveSpaceContext: Bool) -> Self {
+ self.immersiveSpaceContext = immersiveSpaceContext
+ return self
+ }
+
/// Decorate the v_tracker field in the tracker protocol.
/// @note Do not use. Internal use only.
@objc
@@ -474,6 +539,13 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
self.advertisingIdentifierRetriever = retriever
return self
}
+
+ /// Set of callbacks to be used to retrieve properties of the platform context.
+ /// Overrides the tracker implementation for setting the properties.
+ public func platformContextRetriever(_ retriever: PlatformContextRetriever?) -> Self {
+ self.platformContextRetriever = retriever
+ return self
+ }
// MARK: - NSCopying
@@ -489,17 +561,19 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
copy.applicationContext = applicationContext
copy.platformContext = platformContext
copy.platformContextProperties = platformContextProperties
+ copy.platformContextRetriever = platformContextRetriever
copy.geoLocationContext = geoLocationContext
copy.deepLinkContext = deepLinkContext
copy.screenContext = screenContext
copy.screenViewAutotracking = screenViewAutotracking
+ copy.screenEngagementAutotracking = screenEngagementAutotracking
copy.lifecycleAutotracking = lifecycleAutotracking
copy.installAutotracking = installAutotracking
copy.exceptionAutotracking = exceptionAutotracking
copy.diagnosticAutotracking = diagnosticAutotracking
copy.trackerVersionSuffix = trackerVersionSuffix
copy.userAnonymisation = userAnonymisation
- copy.advertisingIdentifierRetriever = advertisingIdentifierRetriever
+ copy.immersiveSpaceContext = immersiveSpaceContext
return copy
}
@@ -522,12 +596,14 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
coder.encode(deepLinkContext, forKey: "deepLinkContext")
coder.encode(screenContext, forKey: "screenContext")
coder.encode(screenViewAutotracking, forKey: "screenViewAutotracking")
+ coder.encode(screenEngagementAutotracking, forKey: "screenEngagementAutotracking")
coder.encode(lifecycleAutotracking, forKey: "lifecycleAutotracking")
coder.encode(installAutotracking, forKey: "installAutotracking")
coder.encode(exceptionAutotracking, forKey: "exceptionAutotracking")
coder.encode(diagnosticAutotracking, forKey: "diagnosticAutotracking")
coder.encode(trackerVersionSuffix, forKey: "trackerVersionSuffix")
coder.encode(userAnonymisation, forKey: "userAnonymisation")
+ coder.encode(immersiveSpaceContext, forKey: "immersiveSpaceContext")
}
required init?(coder: NSCoder) {
@@ -552,6 +628,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
deepLinkContext = coder.decodeBool(forKey: "deepLinkContext")
screenContext = coder.decodeBool(forKey: "screenContext")
screenViewAutotracking = coder.decodeBool(forKey: "screenViewAutotracking")
+ screenEngagementAutotracking = coder.decodeBool(forKey: "screenEngagementAutotracking")
lifecycleAutotracking = coder.decodeBool(forKey: "lifecycleAutotracking")
installAutotracking = coder.decodeBool(forKey: "installAutotracking")
exceptionAutotracking = coder.decodeBool(forKey: "exceptionAutotracking")
@@ -560,5 +637,6 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati
self.trackerVersionSuffix = trackerVersionSuffix
}
userAnonymisation = coder.decodeBool(forKey: "userAnonymisation")
+ immersiveSpaceContext = coder.decodeBool(forKey: "immersiveSpaceContext")
}
}
diff --git a/Sources/Snowplow/Controllers/Controller.swift b/Sources/Snowplow/Controllers/Controller.swift
index 9340947c4..687314fb3 100644
--- a/Sources/Snowplow/Controllers/Controller.swift
+++ b/Sources/Snowplow/Controllers/Controller.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Controllers/EmitterController.swift b/Sources/Snowplow/Controllers/EmitterController.swift
index c0b016416..f230bef97 100644
--- a/Sources/Snowplow/Controllers/EmitterController.swift
+++ b/Sources/Snowplow/Controllers/EmitterController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -21,6 +21,9 @@ public protocol EmitterController: EmitterConfigurationProtocol {
/// Whether the emitter is currently sending events.
@objc
var isSending: Bool { get }
+ /// The EventStore being used by the emitter.
+ @objc
+ var eventStore: EventStore { get }
@objc
func flush()
/// Pause emitting events.
diff --git a/Sources/Snowplow/Controllers/GDPRController.swift b/Sources/Snowplow/Controllers/GDPRController.swift
index 0bc5c253d..b1b118115 100644
--- a/Sources/Snowplow/Controllers/GDPRController.swift
+++ b/Sources/Snowplow/Controllers/GDPRController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Controllers/GlobalContextsController.swift b/Sources/Snowplow/Controllers/GlobalContextsController.swift
index ad326c6c8..1898fe4f3 100644
--- a/Sources/Snowplow/Controllers/GlobalContextsController.swift
+++ b/Sources/Snowplow/Controllers/GlobalContextsController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Controllers/NetworkController.swift b/Sources/Snowplow/Controllers/NetworkController.swift
index a0511c49e..0894f53be 100644
--- a/Sources/Snowplow/Controllers/NetworkController.swift
+++ b/Sources/Snowplow/Controllers/NetworkController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Controllers/PluginsController.swift b/Sources/Snowplow/Controllers/PluginsController.swift
index deda80eab..288e94559 100644
--- a/Sources/Snowplow/Controllers/PluginsController.swift
+++ b/Sources/Snowplow/Controllers/PluginsController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Controllers/SessionController.swift b/Sources/Snowplow/Controllers/SessionController.swift
index a01a19790..35dcef822 100644
--- a/Sources/Snowplow/Controllers/SessionController.swift
+++ b/Sources/Snowplow/Controllers/SessionController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Controllers/SubjectController.swift b/Sources/Snowplow/Controllers/SubjectController.swift
index 7912aaaf7..fe793cbd6 100644
--- a/Sources/Snowplow/Controllers/SubjectController.swift
+++ b/Sources/Snowplow/Controllers/SubjectController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Controllers/TrackerController.swift b/Sources/Snowplow/Controllers/TrackerController.swift
index 3c5fcdf4c..9f929aa5b 100644
--- a/Sources/Snowplow/Controllers/TrackerController.swift
+++ b/Sources/Snowplow/Controllers/TrackerController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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.
@@ -75,4 +75,40 @@ public protocol TrackerController: TrackerConfigurationProtocol {
/// The tracker will start tracking again.
@objc
func resume()
+ /// Adds user and session information to a URL.
+ ///
+ /// For example, calling decorateLink on `appSchema://path/to/page` will return:
+ ///
+ /// `appSchema://path/to/page?_sp=domainUserId.timestamp.sessionId..sourceId`
+ ///
+ /// Filled by this method:
+ /// - `domainUserId`: Value of ``SessionController.userId``
+ /// - `timestamp`: ms precision epoch timestamp
+ /// - `sessionId`: Value of ``SessionController.sessionId``
+ /// - `sourceId`: Value of ``Tracker.appId``
+ ///
+ /// - Parameter uri The URI to add the query string to
+ ///
+ /// - Returns Optional URL
+ /// - nil if ``SnowplowTracker/SessionController/userId`` is null from `sessionContext(false)` being passed in ``TrackerConfiguration``
+ /// - otherwise, decorated URL
+ @objc
+ func decorateLink(_ url: URL) -> URL?
+ /// Adds user and session information to a URL.
+ ///
+ /// For example, calling decorateLink on `appSchema://path/to/page` with all extended parameters enabled will return:
+ ///
+ /// `appSchema://path/to/page?_sp=domainUserId.timestamp.sessionId.subjectUserId.sourceId.platform.reason`
+ ///
+ /// - Parameter url The URL to add the query string to
+ /// - Parameter extendedParameters Any optional parameters to include in the query string.
+ ///
+ /// - Returns Optional URL
+ /// - nil if:
+ ///
+ /// - ``SnowplowTracker/SessionController/userId`` is null from `sessionContext(false)` being passed in ``TrackerConfiguration``
+ /// - An enabled CrossDeviceParameter isn't set in the tracker
+ /// - otherwise, decorated URL
+ @objc
+ func decorateLink(_ url: URL, extendedParameters: CrossDeviceParameterConfiguration) -> URL?
}
diff --git a/Sources/Snowplow/Ecommerce/EcommerceController.swift b/Sources/Snowplow/Ecommerce/EcommerceController.swift
index df9707cf0..c5a84c2c8 100644
--- a/Sources/Snowplow/Ecommerce/EcommerceController.swift
+++ b/Sources/Snowplow/Ecommerce/EcommerceController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Entities/CartEntity.swift b/Sources/Snowplow/Ecommerce/Entities/CartEntity.swift
index 408fdb941..5e7018f70 100644
--- a/Sources/Snowplow/Ecommerce/Entities/CartEntity.swift
+++ b/Sources/Snowplow/Ecommerce/Entities/CartEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Entities/EcommerceScreenEntity.swift b/Sources/Snowplow/Ecommerce/Entities/EcommerceScreenEntity.swift
index 521c6e78a..15e696535 100644
--- a/Sources/Snowplow/Ecommerce/Entities/EcommerceScreenEntity.swift
+++ b/Sources/Snowplow/Ecommerce/Entities/EcommerceScreenEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Entities/EcommerceUserEntity.swift b/Sources/Snowplow/Ecommerce/Entities/EcommerceUserEntity.swift
index daaaf11a9..bf131a923 100644
--- a/Sources/Snowplow/Ecommerce/Entities/EcommerceUserEntity.swift
+++ b/Sources/Snowplow/Ecommerce/Entities/EcommerceUserEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Entities/ProductEntity.swift b/Sources/Snowplow/Ecommerce/Entities/ProductEntity.swift
index 016d61ac7..2300e38ff 100644
--- a/Sources/Snowplow/Ecommerce/Entities/ProductEntity.swift
+++ b/Sources/Snowplow/Ecommerce/Entities/ProductEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Entities/PromotionEntity.swift b/Sources/Snowplow/Ecommerce/Entities/PromotionEntity.swift
index d862e593e..e84cb9b86 100644
--- a/Sources/Snowplow/Ecommerce/Entities/PromotionEntity.swift
+++ b/Sources/Snowplow/Ecommerce/Entities/PromotionEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Entities/TransactionEntity.swift b/Sources/Snowplow/Ecommerce/Entities/TransactionEntity.swift
index e0f9ceedb..aebaa6cc6 100644
--- a/Sources/Snowplow/Ecommerce/Entities/TransactionEntity.swift
+++ b/Sources/Snowplow/Ecommerce/Entities/TransactionEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/AddToCartEvent.swift b/Sources/Snowplow/Ecommerce/Events/AddToCartEvent.swift
index a7430ed8c..356a18a09 100644
--- a/Sources/Snowplow/Ecommerce/Events/AddToCartEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/AddToCartEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/CheckoutStepEvent.swift b/Sources/Snowplow/Ecommerce/Events/CheckoutStepEvent.swift
index ee756b713..567c6e051 100644
--- a/Sources/Snowplow/Ecommerce/Events/CheckoutStepEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/CheckoutStepEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/ProductListClickEvent.swift b/Sources/Snowplow/Ecommerce/Events/ProductListClickEvent.swift
index 3e2a6e065..0a45d6fe1 100644
--- a/Sources/Snowplow/Ecommerce/Events/ProductListClickEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/ProductListClickEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/ProductListViewEvent.swift b/Sources/Snowplow/Ecommerce/Events/ProductListViewEvent.swift
index 1c8088af2..8d80be885 100644
--- a/Sources/Snowplow/Ecommerce/Events/ProductListViewEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/ProductListViewEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/ProductViewEvent.swift b/Sources/Snowplow/Ecommerce/Events/ProductViewEvent.swift
index c1c4f67ec..6b9a3d3ea 100644
--- a/Sources/Snowplow/Ecommerce/Events/ProductViewEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/ProductViewEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/PromotionClickEvent.swift b/Sources/Snowplow/Ecommerce/Events/PromotionClickEvent.swift
index f0114ebdb..4c3bf5741 100644
--- a/Sources/Snowplow/Ecommerce/Events/PromotionClickEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/PromotionClickEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/PromotionViewEvent.swift b/Sources/Snowplow/Ecommerce/Events/PromotionViewEvent.swift
index ac4071062..5d5f273a7 100644
--- a/Sources/Snowplow/Ecommerce/Events/PromotionViewEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/PromotionViewEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/RefundEvent.swift b/Sources/Snowplow/Ecommerce/Events/RefundEvent.swift
index 736742542..6cecc5670 100644
--- a/Sources/Snowplow/Ecommerce/Events/RefundEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/RefundEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/RemoveFromCartEvent.swift b/Sources/Snowplow/Ecommerce/Events/RemoveFromCartEvent.swift
index 9021cd3d0..9ba501c3b 100644
--- a/Sources/Snowplow/Ecommerce/Events/RemoveFromCartEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/RemoveFromCartEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/TransactionErrorEvent.swift b/Sources/Snowplow/Ecommerce/Events/TransactionErrorEvent.swift
index 513b73ddb..4ce2d210a 100644
--- a/Sources/Snowplow/Ecommerce/Events/TransactionErrorEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/TransactionErrorEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Ecommerce/Events/TransactionEvent.swift b/Sources/Snowplow/Ecommerce/Events/TransactionEvent.swift
index b330c6b00..b2f26c82d 100644
--- a/Sources/Snowplow/Ecommerce/Events/TransactionEvent.swift
+++ b/Sources/Snowplow/Ecommerce/Events/TransactionEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Emitter/BufferOption.swift b/Sources/Snowplow/Emitter/BufferOption.swift
index 36d12a51c..97b5c4616 100644
--- a/Sources/Snowplow/Emitter/BufferOption.swift
+++ b/Sources/Snowplow/Emitter/BufferOption.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -16,14 +16,16 @@ import Foundation
/// An enum for buffer options.
@objc(SPBufferOption)
public enum BufferOption : Int {
- /// Sends both GET and POST requests with only a single event. Can cause a spike in
- /// network traffic if used in correlation with a large amount of events.
+ /// Sends both GET and POST requests with only a single event.
+ /// This is the default setting.
+ /// Can cause a spike in network traffic if used in correlation with a large amount of events.
case single = 1
- /// Sends POST requests in groups of 10 events. This is the default amount of events too
- /// package into a POST. All GET requests will still emit one at a time.
- case defaultGroup = 10
- /// Sends POST requests in groups of 25 events. Useful for situations where many events
- /// need to be sent. All GET requests will still emit one at a time.
+ /// Sends POST requests in groups of 10 events.
+ /// All GET requests will still emit one at a time.
+ case smallGroup = 10
+ /// Sends POST requests in groups of 25 events.
+ /// Useful for situations where many events need to be sent.
+ /// All GET requests will still emit one at a time.
case largeGroup = 25
}
@@ -32,8 +34,12 @@ extension BufferOption {
switch value {
case "Single":
return .single
+ case "SmallGroup":
+ return .smallGroup
case "DefaultGroup":
- return .defaultGroup
+ return .smallGroup
+ case "LargeGroup":
+ return .largeGroup
case "HeavyGroup":
return .largeGroup
default:
diff --git a/Sources/Snowplow/Emitter/EmitterDefaults.swift b/Sources/Snowplow/Emitter/EmitterDefaults.swift
index c4d36d060..b9b0f485f 100644
--- a/Sources/Snowplow/Emitter/EmitterDefaults.swift
+++ b/Sources/Snowplow/Emitter/EmitterDefaults.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -16,11 +16,14 @@ import Foundation
public class EmitterDefaults {
public private(set) static var httpMethod: HttpMethodOptions = .post
public private(set) static var httpProtocol: ProtocolOptions = .https
- public private(set) static var emitRange = 150
+ public private(set) static var emitRange = BufferOption.largeGroup.rawValue
public private(set) static var emitThreadPoolSize = 15
public private(set) static var byteLimitGet = 40000
public private(set) static var byteLimitPost = 40000
public private(set) static var serverAnonymisation = false
- public private(set) static var bufferOption: BufferOption = .defaultGroup
+ public private(set) static var bufferOption: BufferOption = .single
public private(set) static var retryFailedRequests = true
+ public private(set) static var maxEventStoreSize: Int64 = 1000 // events
+ public private(set) static var maxEventStoreAge: TimeInterval = TimeInterval(60 * 60 * 24 * 30) // 30 days
+ public private(set) static var emitTimeout: TimeInterval = TimeInterval(30) // 30 seconds
}
diff --git a/Sources/Snowplow/Emitter/EmitterEvent.swift b/Sources/Snowplow/Emitter/EmitterEvent.swift
index 40c22c395..76f444a2c 100644
--- a/Sources/Snowplow/Emitter/EmitterEvent.swift
+++ b/Sources/Snowplow/Emitter/EmitterEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Emitter/EventStore.swift b/Sources/Snowplow/Emitter/EventStore.swift
index 745b12945..a036c4c40 100644
--- a/Sources/Snowplow/Emitter/EventStore.swift
+++ b/Sources/Snowplow/Emitter/EventStore.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -43,4 +43,10 @@ public protocol EventStore: NSObjectProtocol {
/// - Returns: EmitterEvent objects containing storeIds and event payloads.
@objc
func emittableEvents(withQueryLimit queryLimit: UInt) -> [EmitterEvent]
+ /// Remove events older than `maxAge` seconds and keep only the latest `maxSize` events.
+ /// - Parameters:
+ /// - maxSize: Limit for the maximum number of unsent events to keep
+ /// - maxAge: Limit for the maximum duration of how long events should be kept
+ @objc
+ func removeOldEvents(maxSize: Int64, maxAge: TimeInterval)
}
diff --git a/Sources/Snowplow/Entities/DeepLinkEntity.swift b/Sources/Snowplow/Entities/DeepLinkEntity.swift
index 2b0ea5788..c17efe1e7 100644
--- a/Sources/Snowplow/Entities/DeepLinkEntity.swift
+++ b/Sources/Snowplow/Entities/DeepLinkEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Entities/LifecycleEntity.swift b/Sources/Snowplow/Entities/LifecycleEntity.swift
index fc0c9fa6f..fc35407f3 100644
--- a/Sources/Snowplow/Entities/LifecycleEntity.swift
+++ b/Sources/Snowplow/Entities/LifecycleEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/Background.swift b/Sources/Snowplow/Events/Background.swift
index ba24d4d46..b8bd75140 100644
--- a/Sources/Snowplow/Events/Background.swift
+++ b/Sources/Snowplow/Events/Background.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/ConsentDocument.swift b/Sources/Snowplow/Events/ConsentDocument.swift
index 94c1d22ac..5ab4a5af9 100644
--- a/Sources/Snowplow/Events/ConsentDocument.swift
+++ b/Sources/Snowplow/Events/ConsentDocument.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/ConsentGranted.swift b/Sources/Snowplow/Events/ConsentGranted.swift
index 1250659f8..3465a09f8 100644
--- a/Sources/Snowplow/Events/ConsentGranted.swift
+++ b/Sources/Snowplow/Events/ConsentGranted.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/ConsentWithdrawn.swift b/Sources/Snowplow/Events/ConsentWithdrawn.swift
index b9fdf8376..7b7522353 100644
--- a/Sources/Snowplow/Events/ConsentWithdrawn.swift
+++ b/Sources/Snowplow/Events/ConsentWithdrawn.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/DeepLinkReceived.swift b/Sources/Snowplow/Events/DeepLinkReceived.swift
index 6fbf1cb1b..a30d3aad8 100644
--- a/Sources/Snowplow/Events/DeepLinkReceived.swift
+++ b/Sources/Snowplow/Events/DeepLinkReceived.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/Ecommerce.swift b/Sources/Snowplow/Events/Ecommerce.swift
index 8d142a94d..f1407647e 100644
--- a/Sources/Snowplow/Events/Ecommerce.swift
+++ b/Sources/Snowplow/Events/Ecommerce.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/EcommerceItem.swift b/Sources/Snowplow/Events/EcommerceItem.swift
index 751d2f7b7..3c0f31dad 100644
--- a/Sources/Snowplow/Events/EcommerceItem.swift
+++ b/Sources/Snowplow/Events/EcommerceItem.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/EventBase.swift b/Sources/Snowplow/Events/EventBase.swift
index 4b8f8ac77..447395555 100644
--- a/Sources/Snowplow/Events/EventBase.swift
+++ b/Sources/Snowplow/Events/EventBase.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -13,7 +13,7 @@
import Foundation
-/// This class has the basic functionality needed to represent all events
+/// This class has the basic functionality needed to represent all events.
@objc(SPEvent)
public class Event: NSObject {
/// The user event timestamp in milliseconds (epoch time).
@@ -82,19 +82,18 @@ public class Event: NSObject {
return self
}
- /// Replace the context entities attached to the event with a new list of entities.
+ /// Adds a list of context entities to the existing ones.
@objc
public func entities(_ entities: [SelfDescribingJson]) -> Self {
- self.entities = entities
+ self._entities.append(contentsOf: entities)
return self
}
- /// Replace the context entities attached to the event with a new list of entities.
+ /// Adds a list of context entities to the existing ones.
@objc
@available(*, deprecated, renamed: "entities")
public func contexts(_ entities: [SelfDescribingJson]) -> Self {
- self.entities = entities
- return self
+ return self.entities(entities)
}
}
diff --git a/Sources/Snowplow/Events/Foreground.swift b/Sources/Snowplow/Events/Foreground.swift
index f5bda7ce3..36e1c29d9 100644
--- a/Sources/Snowplow/Events/Foreground.swift
+++ b/Sources/Snowplow/Events/Foreground.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/ListItemView.swift b/Sources/Snowplow/Events/ListItemView.swift
new file mode 100644
index 000000000..1b54b256a
--- /dev/null
+++ b/Sources/Snowplow/Events/ListItemView.swift
@@ -0,0 +1,57 @@
+// Copyright (c) 2013-present 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
+
+/// Event tracking the view of an item in a list.
+/// If screen engagement tracking is enabled, the list item view events will be aggregated into a `screen_summary` entity.
+///
+/// Schema: `iglu:com.snowplowanalytics.mobile/list_item_view/jsonschema/1-0-0`
+@objc(SPListItemView)
+public class ListItemView: SelfDescribingAbstract {
+ /// Index of the item in the list
+ @objc
+ public var index: Int
+ /// Total number of items in the list
+ public var itemsCount: Int?
+
+ /// - Parameters:
+ /// - index: Index of the item in the list
+ @objc
+ public init(index: Int) {
+ self.index = index
+ }
+
+ /// - Parameters:
+ /// - index: Index of the item in the list
+ /// - totalItems: Total number of items in the list
+ @objc
+ public init(index: Int, totalItems: Int) {
+ self.index = index
+ self.itemsCount = totalItems
+ }
+
+ override var schema: String {
+ return kSPListItemViewSchema
+ }
+
+ override var payload: [String : Any] {
+ var data = [
+ "index": index
+ ]
+ if let itemsCount = itemsCount {
+ data["items_count"] = itemsCount
+ }
+ return data
+ }
+}
diff --git a/Sources/Snowplow/Events/MessageNotification.swift b/Sources/Snowplow/Events/MessageNotification.swift
index 411ddeaaa..79076931c 100644
--- a/Sources/Snowplow/Events/MessageNotification.swift
+++ b/Sources/Snowplow/Events/MessageNotification.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/MessageNotificationAttachment.swift b/Sources/Snowplow/Events/MessageNotificationAttachment.swift
index cc38413c6..cf077505a 100644
--- a/Sources/Snowplow/Events/MessageNotificationAttachment.swift
+++ b/Sources/Snowplow/Events/MessageNotificationAttachment.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/PageView.swift b/Sources/Snowplow/Events/PageView.swift
index 2db3292be..6950936f2 100644
--- a/Sources/Snowplow/Events/PageView.swift
+++ b/Sources/Snowplow/Events/PageView.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/PushNotification.swift b/Sources/Snowplow/Events/PushNotification.swift
index 16e97df73..365550306 100644
--- a/Sources/Snowplow/Events/PushNotification.swift
+++ b/Sources/Snowplow/Events/PushNotification.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/SNOWError.swift b/Sources/Snowplow/Events/SNOWError.swift
index 89e3cea61..97bfbb64f 100644
--- a/Sources/Snowplow/Events/SNOWError.swift
+++ b/Sources/Snowplow/Events/SNOWError.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/ScreenView.swift b/Sources/Snowplow/Events/ScreenView.swift
index ed4b16f3f..fe43a3282 100644
--- a/Sources/Snowplow/Events/ScreenView.swift
+++ b/Sources/Snowplow/Events/ScreenView.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/ScrollChanged.swift b/Sources/Snowplow/Events/ScrollChanged.swift
new file mode 100644
index 000000000..affdac771
--- /dev/null
+++ b/Sources/Snowplow/Events/ScrollChanged.swift
@@ -0,0 +1,114 @@
+// Copyright (c) 2013-present 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
+
+/// Event tracked when a scroll view's scroll position changes.
+/// If screen engagement tracking is enabled, the scroll changed events will be aggregated into a `screen_summary` entity.
+///
+/// Schema: `iglu:com.snowplowanalytics.mobile/scroll_changed/jsonschema/1-0-0`
+@objc(SPScrollChanged)
+public class ScrollChanged: SelfDescribingAbstract {
+ /// Vertical scroll offset in pixels
+ public var yOffset: Int?
+ /// Horizontal scroll offset in pixels
+ public var xOffset: Int?
+ /// The height of the scroll view in pixels
+ public var viewHeight: Int?
+ /// The width of the scroll view in pixels
+ public var viewWidth: Int?
+ /// The height of the content in the scroll view in pixels
+ public var contentHeight: Int?
+ /// The width of the content in the scroll view in pixels
+ public var contentWidth: Int?
+
+ /// - Parameters:
+ /// - xOffset: Horizontal scroll offset in pixels
+ /// - yOffset: Vertical scroll offset in pixels
+ /// - viewWidth: The width of the scroll view in pixels
+ /// - viewHeight: The height of the scroll view in pixels
+ /// - contentWidth: The width of the content in the scroll view in pixels
+ /// - contentHeight: The height of the content in the scroll view in pixels
+ public init(
+ xOffset: Int? = nil,
+ yOffset: Int? = nil,
+ viewWidth: Int? = nil,
+ viewHeight: Int? = nil,
+ contentWidth: Int? = nil,
+ contentHeight: Int? = nil
+ ) {
+ self.yOffset = yOffset
+ self.xOffset = xOffset
+ self.viewHeight = viewHeight
+ self.viewWidth = viewWidth
+ self.contentHeight = contentHeight
+ self.contentWidth = contentWidth
+ }
+
+ /// Vertical scroll offset in pixels
+ @objc
+ public func yOffset(_ yOffset: Int) -> Self {
+ self.yOffset = yOffset
+ return self
+ }
+
+ /// Horizontal scroll offset in pixels
+ @objc
+ public func xOffset(_ xOffset: Int) -> Self {
+ self.xOffset = xOffset
+ return self
+ }
+
+ /// The height of the scroll view in pixels
+ @objc
+ public func viewHeight(_ viewHeight: Int) -> Self {
+ self.viewHeight = viewHeight
+ return self
+ }
+
+ /// The width of the scroll view in pixels
+ @objc
+ public func viewWidth(_ viewWidth: Int) -> Self {
+ self.viewWidth = viewWidth
+ return self
+ }
+
+ /// The height of the scroll view content in pixels
+ @objc
+ public func contentHeight(_ contentHeight: Int) -> Self {
+ self.contentHeight = contentHeight
+ return self
+ }
+
+ /// The width of the scroll view content in pixels
+ @objc
+ public func contentWidth(_ contentWidth: Int) -> Self {
+ self.contentWidth = contentWidth
+ return self
+ }
+
+ override var schema: String {
+ return kSPScrollChangedSchema
+ }
+
+ override var payload: [String : Any] {
+ var data: [String: Any] = [:]
+ if let xOffset = xOffset { data["x_offset"] = xOffset }
+ if let yOffset = yOffset { data["y_offset"] = yOffset }
+ if let viewWidth = viewWidth { data["view_width"] = viewWidth }
+ if let viewHeight = viewHeight { data["view_height"] = viewHeight }
+ if let contentWidth = contentWidth { data["content_width"] = contentWidth }
+ if let contentHeight = contentHeight { data["content_height"] = contentHeight }
+ return data
+ }
+}
diff --git a/Sources/Snowplow/Events/SelfDescribing.swift b/Sources/Snowplow/Events/SelfDescribing.swift
index f3b762c2b..6aacac18a 100644
--- a/Sources/Snowplow/Events/SelfDescribing.swift
+++ b/Sources/Snowplow/Events/SelfDescribing.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -27,6 +27,19 @@ public class SelfDescribing: SelfDescribingAbstract {
self._payload = payload
}
+ /// Creates a self-describing event using data represented as an Encodable struct.
+ /// - Parameters:
+ /// - schema: A valid schema URI.
+ /// - data: Data represented using an Encodable struct.
+ /// - Returns: A SelfDescribing event.
+ public convenience init(schema: String, data: T) throws {
+ let data = try JSONEncoder().encode(data)
+ let jsonObject = try JSONSerialization.jsonObject(with: data)
+ let dict = jsonObject as! [String: Any]
+
+ self.init(schema: schema, payload: dict)
+ }
+
private var _schema: String
override var schema: String {
get { return _schema }
diff --git a/Sources/Snowplow/Events/Structured.swift b/Sources/Snowplow/Events/Structured.swift
index 448d6980f..aac0cc24d 100644
--- a/Sources/Snowplow/Events/Structured.swift
+++ b/Sources/Snowplow/Events/Structured.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/Timing.swift b/Sources/Snowplow/Events/Timing.swift
index c958f5945..ab5b5a2d5 100644
--- a/Sources/Snowplow/Events/Timing.swift
+++ b/Sources/Snowplow/Events/Timing.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Events/TrackerError.swift b/Sources/Snowplow/Events/TrackerError.swift
index 6c575d100..289c0b6f4 100644
--- a/Sources/Snowplow/Events/TrackerError.swift
+++ b/Sources/Snowplow/Events/TrackerError.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/GlobalContexts/ContextGenerator.swift b/Sources/Snowplow/GlobalContexts/ContextGenerator.swift
index 7bbb706ec..c50363bcd 100644
--- a/Sources/Snowplow/GlobalContexts/ContextGenerator.swift
+++ b/Sources/Snowplow/GlobalContexts/ContextGenerator.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/GlobalContexts/GlobalContext.swift b/Sources/Snowplow/GlobalContexts/GlobalContext.swift
index dcaaddea1..b1908cd67 100644
--- a/Sources/Snowplow/GlobalContexts/GlobalContext.swift
+++ b/Sources/Snowplow/GlobalContexts/GlobalContext.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/GlobalContexts/SchemaRuleset.swift b/Sources/Snowplow/GlobalContexts/SchemaRuleset.swift
index c5a8f398a..c4616ca2e 100644
--- a/Sources/Snowplow/GlobalContexts/SchemaRuleset.swift
+++ b/Sources/Snowplow/GlobalContexts/SchemaRuleset.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Configuration/MediaTrackingConfiguration.swift b/Sources/Snowplow/Media/Configuration/MediaTrackingConfiguration.swift
index a6710f6df..b23e2621f 100644
--- a/Sources/Snowplow/Media/Configuration/MediaTrackingConfiguration.swift
+++ b/Sources/Snowplow/Media/Configuration/MediaTrackingConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Controllers/MediaController.swift b/Sources/Snowplow/Media/Controllers/MediaController.swift
index 607b83a1e..e0f1eb300 100644
--- a/Sources/Snowplow/Media/Controllers/MediaController.swift
+++ b/Sources/Snowplow/Media/Controllers/MediaController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Controllers/MediaTracking.swift b/Sources/Snowplow/Media/Controllers/MediaTracking.swift
index 38975a222..c387dcd70 100644
--- a/Sources/Snowplow/Media/Controllers/MediaTracking.swift
+++ b/Sources/Snowplow/Media/Controllers/MediaTracking.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Entities/MediaAdBreakEntity.swift b/Sources/Snowplow/Media/Entities/MediaAdBreakEntity.swift
index 655cf40a3..578392b35 100644
--- a/Sources/Snowplow/Media/Entities/MediaAdBreakEntity.swift
+++ b/Sources/Snowplow/Media/Entities/MediaAdBreakEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Entities/MediaAdEntity.swift b/Sources/Snowplow/Media/Entities/MediaAdEntity.swift
index 00c774bea..04f811198 100644
--- a/Sources/Snowplow/Media/Entities/MediaAdEntity.swift
+++ b/Sources/Snowplow/Media/Entities/MediaAdEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Entities/MediaPlayerEntity.swift b/Sources/Snowplow/Media/Entities/MediaPlayerEntity.swift
index 1557d22b7..141572bea 100644
--- a/Sources/Snowplow/Media/Entities/MediaPlayerEntity.swift
+++ b/Sources/Snowplow/Media/Entities/MediaPlayerEntity.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdBreakEndEvent.swift b/Sources/Snowplow/Media/Events/MediaAdBreakEndEvent.swift
index b510a4f86..5894197e3 100644
--- a/Sources/Snowplow/Media/Events/MediaAdBreakEndEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdBreakEndEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdBreakStartEvent.swift b/Sources/Snowplow/Media/Events/MediaAdBreakStartEvent.swift
index abe8cad59..dcc529dfc 100644
--- a/Sources/Snowplow/Media/Events/MediaAdBreakStartEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdBreakStartEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdClickEvent.swift b/Sources/Snowplow/Media/Events/MediaAdClickEvent.swift
index 699eb6964..3d42c1922 100644
--- a/Sources/Snowplow/Media/Events/MediaAdClickEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdClickEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdCompleteEvent.swift b/Sources/Snowplow/Media/Events/MediaAdCompleteEvent.swift
index 3f5fc0dcd..2d4ff0c1c 100644
--- a/Sources/Snowplow/Media/Events/MediaAdCompleteEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdCompleteEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdFirstQuartileEvent.swift b/Sources/Snowplow/Media/Events/MediaAdFirstQuartileEvent.swift
index 1abbb5a7d..ad73acca5 100644
--- a/Sources/Snowplow/Media/Events/MediaAdFirstQuartileEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdFirstQuartileEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdMidpointEvent.swift b/Sources/Snowplow/Media/Events/MediaAdMidpointEvent.swift
index 8b35e664f..46c42f6f4 100644
--- a/Sources/Snowplow/Media/Events/MediaAdMidpointEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdMidpointEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdPauseEvent.swift b/Sources/Snowplow/Media/Events/MediaAdPauseEvent.swift
index 917477b1a..14385f7bd 100644
--- a/Sources/Snowplow/Media/Events/MediaAdPauseEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdPauseEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdResumeEvent.swift b/Sources/Snowplow/Media/Events/MediaAdResumeEvent.swift
index eda1d71e5..299bb63a8 100644
--- a/Sources/Snowplow/Media/Events/MediaAdResumeEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdResumeEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdSkipEvent.swift b/Sources/Snowplow/Media/Events/MediaAdSkipEvent.swift
index 7fe408255..8ce38418a 100644
--- a/Sources/Snowplow/Media/Events/MediaAdSkipEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdSkipEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdStartEvent.swift b/Sources/Snowplow/Media/Events/MediaAdStartEvent.swift
index c4f5fc0a0..7e33c06bf 100644
--- a/Sources/Snowplow/Media/Events/MediaAdStartEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdStartEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaAdThirdQuartileEvent.swift b/Sources/Snowplow/Media/Events/MediaAdThirdQuartileEvent.swift
index d357a72c9..061e65f4e 100644
--- a/Sources/Snowplow/Media/Events/MediaAdThirdQuartileEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaAdThirdQuartileEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaBufferEndEvent.swift b/Sources/Snowplow/Media/Events/MediaBufferEndEvent.swift
index e1fddfb13..001cca166 100644
--- a/Sources/Snowplow/Media/Events/MediaBufferEndEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaBufferEndEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaBufferStartEvent.swift b/Sources/Snowplow/Media/Events/MediaBufferStartEvent.swift
index 912d17040..62cc2f4c5 100644
--- a/Sources/Snowplow/Media/Events/MediaBufferStartEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaBufferStartEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaEndEvent.swift b/Sources/Snowplow/Media/Events/MediaEndEvent.swift
index 4d9fb48ea..d89fe1ed6 100644
--- a/Sources/Snowplow/Media/Events/MediaEndEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaEndEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaErrorEvent.swift b/Sources/Snowplow/Media/Events/MediaErrorEvent.swift
index 9cc052cb5..ae9428a38 100644
--- a/Sources/Snowplow/Media/Events/MediaErrorEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaErrorEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaFullscreenChangeEvent.swift b/Sources/Snowplow/Media/Events/MediaFullscreenChangeEvent.swift
index dc5540e0b..ed27d6724 100644
--- a/Sources/Snowplow/Media/Events/MediaFullscreenChangeEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaFullscreenChangeEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaPauseEvent.swift b/Sources/Snowplow/Media/Events/MediaPauseEvent.swift
index 307d116a4..fed926718 100644
--- a/Sources/Snowplow/Media/Events/MediaPauseEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaPauseEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaPictureInPictureChangeEvent.swift b/Sources/Snowplow/Media/Events/MediaPictureInPictureChangeEvent.swift
index e5a4882da..de509e26e 100644
--- a/Sources/Snowplow/Media/Events/MediaPictureInPictureChangeEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaPictureInPictureChangeEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaPlayEvent.swift b/Sources/Snowplow/Media/Events/MediaPlayEvent.swift
index 3b8d5a7b5..ea8741c2d 100644
--- a/Sources/Snowplow/Media/Events/MediaPlayEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaPlayEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaPlaybackRateChangeEvent.swift b/Sources/Snowplow/Media/Events/MediaPlaybackRateChangeEvent.swift
index b3844da99..86b8d2792 100644
--- a/Sources/Snowplow/Media/Events/MediaPlaybackRateChangeEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaPlaybackRateChangeEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaQualityChangeEvent.swift b/Sources/Snowplow/Media/Events/MediaQualityChangeEvent.swift
index f1ee1bf13..e512d0cc8 100644
--- a/Sources/Snowplow/Media/Events/MediaQualityChangeEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaQualityChangeEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaReadyEvent.swift b/Sources/Snowplow/Media/Events/MediaReadyEvent.swift
index 7a2f7608b..2b4e149ed 100644
--- a/Sources/Snowplow/Media/Events/MediaReadyEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaReadyEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaSeekEndEvent.swift b/Sources/Snowplow/Media/Events/MediaSeekEndEvent.swift
index e89c6ee4a..da8d72cce 100644
--- a/Sources/Snowplow/Media/Events/MediaSeekEndEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaSeekEndEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaSeekStartEvent.swift b/Sources/Snowplow/Media/Events/MediaSeekStartEvent.swift
index b47e699e9..465d7b372 100644
--- a/Sources/Snowplow/Media/Events/MediaSeekStartEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaSeekStartEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Media/Events/MediaVolumeChangeEvent.swift b/Sources/Snowplow/Media/Events/MediaVolumeChangeEvent.swift
index 57656f7d0..225ad8b5e 100644
--- a/Sources/Snowplow/Media/Events/MediaVolumeChangeEvent.swift
+++ b/Sources/Snowplow/Media/Events/MediaVolumeChangeEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Network/DefaultNetworkConnection.swift b/Sources/Snowplow/Network/DefaultNetworkConnection.swift
index 900604d9d..21652db4a 100644
--- a/Sources/Snowplow/Network/DefaultNetworkConnection.swift
+++ b/Sources/Snowplow/Network/DefaultNetworkConnection.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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
+ private var _emitThreadPoolSize = EmitterDefaults.emitThreadPoolSize
/// 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,121 +54,172 @@ public class DefaultNetworkConnection: NSObject, NetworkConnection {
}
}
}
+
/// Maximum event size for a GET request.
- public var byteLimitGet: Int = 40000
+ @objc
+ public var byteLimitGet: Int = EmitterDefaults.byteLimitGet
+
/// Maximum event size for a POST request.
@objc
- public var byteLimitPost = 40000
+ public var byteLimitPost = EmitterDefaults.byteLimitPost
+
+ 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
+
+ /// Custom timeout for the requests
+ private let timeout: TimeInterval
+
+ private var protocolClasses: [AnyClass]?
+ private var _urlSession: URLSession?
+ private var urlSession: URLSession {
+ if let urlSession = _urlSession { return urlSession }
+
+ let sessionConfig: URLSessionConfiguration = .default
+ sessionConfig.timeoutIntervalForRequest = TimeInterval(self.timeout)
+ sessionConfig.timeoutIntervalForResource = TimeInterval(self.timeout)
+ sessionConfig.protocolClasses = protocolClasses
+
+ let urlSession = URLSession(configuration: sessionConfig)
+ self._urlSession = urlSession
+ return urlSession
+ }
@objc
public init(urlString: String,
httpMethod: HttpMethodOptions = EmitterDefaults.httpMethod,
protocol: ProtocolOptions = EmitterDefaults.httpProtocol,
- customPostPath: String? = nil) {
+ customPostPath: String? = nil,
+ timeout: TimeInterval = EmitterDefaults.emitTimeout,
+ protocolClasses: [AnyClass]? = nil) {
self._urlString = urlString
+ self.timeout = timeout
super.init()
- self.httpMethod = httpMethod
- self.protocol = `protocol`
- self.customPostPath = customPostPath
+ self._httpMethod = httpMethod
+ self._protocol = `protocol`
+ self._customPostPath = customPostPath
+ self.protocolClasses = protocolClasses
setup()
}
// MARK: - Implement SPNetworkConnection protocol
+
+ @objc
+ public func sendRequests(_ requests: [Request]) -> [RequestResult] {
+ let urlRequests = requests.map { _httpMethod == .get ? buildGet($0) : buildPost($0) }
+
+ var results: [RequestResult] = []
+ // if there is only one request, make it directly
+ if requests.count == 1 {
+ if let request = requests.first, let urlRequest = urlRequests.first {
+ let result = DefaultNetworkConnection.makeRequest(
+ request: request,
+ urlRequest: urlRequest,
+ urlSession: urlSession
+ )
+
+ results.append(result)
+ }
+ }
+ // if there are more than 1 request, use the operation queue
+ else if requests.count > 1 {
+ for (request, urlRequest) in zip(requests, urlRequests) {
+ dataOperationQueue.addOperation({
+ let result = DefaultNetworkConnection.makeRequest(
+ request: request,
+ urlRequest: urlRequest,
+ urlSession: self.urlSession
+ )
+
+ objc_sync_enter(self)
+ results.append(result)
+ objc_sync_exit(self)
+ })
+ }
+ dataOperationQueue.waitUntilAllOperationsAreFinished()
+ }
+
+ return results
+ }
+
+ // MARK: - Private methods
+
+ private static func makeRequest(request: Request, urlRequest: URLRequest, urlSession: URLSession?) -> RequestResult {
+ //source: https://forums.developer.apple.com/thread/11519
+ var httpResponse: HTTPURLResponse? = nil
+ var connectionError: Error? = nil
+ var sem: DispatchSemaphore
+
+ sem = DispatchSemaphore(value: 0)
+
+ urlSession?.dataTask(with: urlRequest) { data, urlResponse, error in
+ connectionError = error
+ httpResponse = urlResponse as? HTTPURLResponse
+ sem.signal()
+ }.resume()
+
+ let _ = sem.wait(timeout: .distantFuture)
+ var statusCode: NSNumber?
+ if let httpResponse = httpResponse { statusCode = NSNumber(value: httpResponse.statusCode) }
+
+ let result = RequestResult(statusCode: statusCode, oversize: request.oversize, storeIds: request.emitterEventIds)
+ if !result.isSuccessful {
+ logError(message: "Connection error: " + (connectionError?.localizedDescription ?? "-"))
+ }
+
+ return result
+ }
private func setup() {
// Decode url to extract protocol
let url = URL(string: _urlString)
var endpoint = _urlString
if url?.scheme == "https" {
- `protocol` = .https
+ _protocol = .https
} else if url?.scheme == "http" {
- `protocol` = .http
+ _protocol = .http
} else {
- `protocol` = .https
+ _protocol = .https
endpoint = "https://\(_urlString)"
}
// Configure
- let urlPrefix = `protocol` == .http ? "http://" : "https://"
+ let urlPrefix = _protocol == .http ? "http://" : "https://"
var urlSuffix = _httpMethod == .get ? kSPEndpointGet : kSPEndpointPost
if _httpMethod == .post {
- if let customPostPath = customPostPath { urlSuffix = customPostPath }
+ 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)
+ _urlEndpoint = URL(string: endpoint)?.appendingPathComponent(urlSuffix)
// Log
- if urlEndpoint?.scheme != nil && urlEndpoint?.host != nil {
- logDebug(message: "Emitter URL created successfully '\(urlEndpoint?.absoluteString ?? "-")'")
+ if _urlEndpoint?.scheme != nil && _urlEndpoint?.host != nil {
+ logDebug(message: "Emitter URL created successfully '\(_urlEndpoint?.absoluteString ?? "-")'")
} else {
- logDebug(message: "Invalid emitter URL: '\(urlEndpoint?.absoluteString ?? "-")'")
+ 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] {
- var results: [RequestResult] = []
-
- for request in requests {
- let urlRequest = _httpMethod == .get
- ? buildGet(request)
- : buildPost(request)
-
- dataOperationQueue.addOperation({
- //source: https://forums.developer.apple.com/thread/11519
- var httpResponse: HTTPURLResponse? = nil
- var connectionError: Error? = nil
- var sem: DispatchSemaphore
-
- sem = DispatchSemaphore(value: 0)
-
- URLSession.shared.dataTask(with: urlRequest) { data, urlResponse, error in
- connectionError = error
- httpResponse = urlResponse as? HTTPURLResponse
- sem.signal()
- }.resume()
-
- let _ = sem.wait(timeout: .distantFuture)
- var statusCode: NSNumber?
- if let httpResponse = httpResponse { statusCode = NSNumber(value: httpResponse.statusCode) }
-
- let result = RequestResult(statusCode: statusCode, oversize: request.oversize, storeIds: request.emitterEventIds)
- if !result.isSuccessful {
- logError(message: "Connection error: " + (connectionError?.localizedDescription ?? "-"))
- }
-
- objc_sync_enter(self)
- results.append(result)
- objc_sync_exit(self)
- })
- }
- dataOperationQueue.waitUntilAllOperationsAreFinished()
- return results
- }
-
- // MARK: - Private methods
-
- func buildPost(_ request: Request) -> URLRequest {
+
+ private func buildPost(_ request: Request) -> URLRequest {
var requestData: Data? = nil
do {
requestData = try JSONSerialization.data(withJSONObject: request.payload?.dictionary ?? [:], options: [])
@@ -208,7 +241,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 +257,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/Network/HttpMethodOptions.swift b/Sources/Snowplow/Network/HttpMethodOptions.swift
index 1c9f9ba08..e44e63e5c 100644
--- a/Sources/Snowplow/Network/HttpMethodOptions.swift
+++ b/Sources/Snowplow/Network/HttpMethodOptions.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Network/NetworkConnection.swift b/Sources/Snowplow/Network/NetworkConnection.swift
index fa7e68ab9..ec6c94c8b 100644
--- a/Sources/Snowplow/Network/NetworkConnection.swift
+++ b/Sources/Snowplow/Network/NetworkConnection.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Network/ProtocolOptions.swift b/Sources/Snowplow/Network/ProtocolOptions.swift
index 4f884ddea..7c72c9c33 100644
--- a/Sources/Snowplow/Network/ProtocolOptions.swift
+++ b/Sources/Snowplow/Network/ProtocolOptions.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Network/Request.swift b/Sources/Snowplow/Network/Request.swift
index 21c77eee4..388fe8567 100644
--- a/Sources/Snowplow/Network/Request.swift
+++ b/Sources/Snowplow/Network/Request.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Network/RequestCallback.swift b/Sources/Snowplow/Network/RequestCallback.swift
index d02783da0..f030dff96 100644
--- a/Sources/Snowplow/Network/RequestCallback.swift
+++ b/Sources/Snowplow/Network/RequestCallback.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Network/RequestResult.swift b/Sources/Snowplow/Network/RequestResult.swift
index 9a93e26d1..7f24f1fae 100644
--- a/Sources/Snowplow/Network/RequestResult.swift
+++ b/Sources/Snowplow/Network/RequestResult.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Payload/Payload.swift b/Sources/Snowplow/Payload/Payload.swift
index f6aa86e49..7190a6a8f 100644
--- a/Sources/Snowplow/Payload/Payload.swift
+++ b/Sources/Snowplow/Payload/Payload.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/Payload/SelfDescribingJson.swift b/Sources/Snowplow/Payload/SelfDescribingJson.swift
index 64cc12aef..044b5f019 100644
--- a/Sources/Snowplow/Payload/SelfDescribingJson.swift
+++ b/Sources/Snowplow/Payload/SelfDescribingJson.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -82,6 +82,19 @@ public class SelfDescribingJson: NSObject {
public convenience init(schema: String, andSelfDescribingJson data: SelfDescribingJson) {
self.init(schema: schema, andData: data.dictionary)
}
+
+ /// Creates a self-describing JSON using data represented as an Encodable struct.
+ /// - Parameters:
+ /// - schema: A valid schema URI.
+ /// - data: Data represented using an Encodable struct.
+ /// - Returns: A SelfDescribingJson.
+ public convenience init(schema: String, andEncodable data: T) throws {
+ let data = try JSONEncoder().encode(data)
+ let jsonObject = try JSONSerialization.jsonObject(with: data)
+ let dict = jsonObject as! [String: Any]
+
+ self.init(schema: schema, andData: dict)
+ }
/// Sets the data field of the self-describing JSON.
/// - Parameter data: An SPPayload to be nested into the data.
diff --git a/Sources/Snowplow/Snowplow.swift b/Sources/Snowplow/Snowplow.swift
index f8fec0898..82d6d864f 100644
--- a/Sources/Snowplow/Snowplow.swift
+++ b/Sources/Snowplow/Snowplow.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -198,14 +198,16 @@ public class Snowplow: NSObject {
/// the tracker.
/// - 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
+ public class func createTracker(namespace: String, network networkConfiguration: NetworkConfiguration, configurations: [ConfigurationProtocol] = []) -> 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)
+ }
}
}
@@ -234,7 +236,7 @@ public class Snowplow: NSObject {
/// collector.
/// - configurationBuilder: Swift DSL builder for your configuration objects (e.g, `EmitterConfiguration`, `TrackerConfiguration`)
/// - Returns: The tracker instance created.
- public class func createTracker(namespace: String, network networkConfiguration: NetworkConfiguration, @ConfigurationBuilder _ configurationBuilder: () -> [ConfigurationProtocol]) -> TrackerController? {
+ public class func createTracker(namespace: String, network networkConfiguration: NetworkConfiguration, @ConfigurationBuilder _ configurationBuilder: () -> [ConfigurationProtocol]) -> TrackerController {
let configurations = configurationBuilder()
return createTracker(namespace: namespace,
network: networkConfiguration,
@@ -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,22 +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 {
- if let _ = createTracker(
- namespace: bundle.namespace,
- network: networkConfiguration,
- configurations: bundle.configurations) {
- namespaces?.append(bundle.namespace)
- }
+ _ = 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/Sources/Snowplow/Tracker/CrossDeviceParameterConfiguration.swift b/Sources/Snowplow/Tracker/CrossDeviceParameterConfiguration.swift
new file mode 100644
index 000000000..09658e2fd
--- /dev/null
+++ b/Sources/Snowplow/Tracker/CrossDeviceParameterConfiguration.swift
@@ -0,0 +1,42 @@
+// Copyright (c) 2013-present 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
+
+/// Configuration object for ``TrackerController/decorateLink``
+@objc public class CrossDeviceParameterConfiguration : NSObject {
+ /// Whether to include the value of ``SessionController.sessionId`` when decorating a link (enabled by default)
+ @objc var sessionId: Bool
+ /// Whether to include the value of ``Subject.userId`` when decorating a link
+ @objc var subjectUserId: Bool
+ /// Whether to include the value of ``Tracker.appId`` when decorating a link (enabled by default)
+ @objc var sourceId: Bool
+ /// Whether to include the value of ``Tracker.platform`` when decorating a link
+ @objc var sourcePlatform: Bool
+ /// Optional identifier/information for cross-navigation
+ @objc var reason: String?
+
+ @objc init(
+ sessionId: Bool = true,
+ subjectUserId: Bool = false,
+ sourceId: Bool = true,
+ sourcePlatform: Bool = false,
+ reason: String? = nil
+ ) {
+ self.sessionId = sessionId
+ self.subjectUserId = subjectUserId
+ self.sourceId = sourceId
+ self.sourcePlatform = sourcePlatform
+ self.reason = reason
+ }
+}
diff --git a/Sources/Snowplow/Tracker/DevicePlatform.swift b/Sources/Snowplow/Tracker/DevicePlatform.swift
index cbe7d6e2b..602c85d8e 100644
--- a/Sources/Snowplow/Tracker/DevicePlatform.swift
+++ b/Sources/Snowplow/Tracker/DevicePlatform.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -23,9 +23,10 @@ public enum DevicePlatform : Int {
case connectedTV
case gameConsole
case internetOfThings
+ case headset
}
-func devicePlatformToString(_ devicePlatform: DevicePlatform) -> String? {
+func devicePlatformToString(_ devicePlatform: DevicePlatform) -> String {
switch devicePlatform {
case .web:
return "web"
@@ -43,11 +44,13 @@ func devicePlatformToString(_ devicePlatform: DevicePlatform) -> String? {
return "cnsl"
case .internetOfThings:
return "iot"
+ case .headset:
+ return "headset"
}
}
func stringToDevicePlatform(_ devicePlatformString: String) -> DevicePlatform? {
- if let index = ["web", "mob", "pc", "srv", "app", "tv", "cnsl", "iot"].firstIndex(of: devicePlatformString) {
+ if let index = ["web", "mob", "pc", "srv", "app", "tv", "cnsl", "iot", "headset"].firstIndex(of: devicePlatformString) {
return DevicePlatform(rawValue: index)
}
return nil
diff --git a/Sources/Snowplow/Tracker/InspectableEvent.swift b/Sources/Snowplow/Tracker/InspectableEvent.swift
index e0ab6c1d2..726303312 100644
--- a/Sources/Snowplow/Tracker/InspectableEvent.swift
+++ b/Sources/Snowplow/Tracker/InspectableEvent.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Tracker/LogLevel.swift b/Sources/Snowplow/Tracker/LogLevel.swift
index 207c19b60..7fc6cf086 100644
--- a/Sources/Snowplow/Tracker/LogLevel.swift
+++ b/Sources/Snowplow/Tracker/LogLevel.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Tracker/LoggerDelegate.swift b/Sources/Snowplow/Tracker/LoggerDelegate.swift
index b8b937314..74179b02e 100644
--- a/Sources/Snowplow/Tracker/LoggerDelegate.swift
+++ b/Sources/Snowplow/Tracker/LoggerDelegate.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Tracker/PlaformContextRetriever.swift b/Sources/Snowplow/Tracker/PlaformContextRetriever.swift
new file mode 100644
index 000000000..6ae7d7ed6
--- /dev/null
+++ b/Sources/Snowplow/Tracker/PlaformContextRetriever.swift
@@ -0,0 +1,143 @@
+// Copyright (c) 2013-present 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
+
+/// Overrides for the values for properties of the platform context.
+public class PlatformContextRetriever {
+
+ /// Operating system type (e.g., ios, tvos, watchos, osx, android)
+ public var osType: (() -> String)? = nil
+
+ /// The current version of the operating system
+ public var osVersion: (() -> String)? = nil
+
+ /// The manufacturer of the product/hardware
+ public var deviceVendor: (() -> String)? = nil
+
+ /// The end-user-visible name for the end product
+ public var deviceModel: (() -> String)? = nil
+
+ /// The carrier of the SIM inserted in the device
+ public var carrier: (() -> String?)? = nil
+
+ /// Type of network the device is connected to
+ public var networkType: (() -> String?)? = nil
+
+ /// Radio access technology that the device is using
+ public var networkTechnology: (() -> String?)? = nil
+
+ /// Advertising identifier on iOS
+ public var appleIdfa: (() -> UUID?)? = nil
+
+ /// UUID identifier for vendors on iOS
+ public var appleIdfv: (() -> String?)? = nil
+
+ /// Bytes of storage remaining
+ public var availableStorage: (() -> Int64?)? = nil
+
+ /// Total size of storage in bytes
+ public var totalStorage: (() -> Int64?)? = nil
+
+ /// Total physical system memory in bytes
+ public var physicalMemory: (() -> UInt64?)? = nil
+
+ /// Amount of memory in bytes available to the current app
+ public var appAvailableMemory: (() -> Int?)? = nil
+
+ /// Remaining battery level as an integer percentage of total battery capacity
+ public var batteryLevel: (() -> Int?)? = nil
+
+ /// Battery state for the device
+ public var batteryState: (() -> String?)? = nil
+
+ /// A Boolean indicating whether Low Power Mode is enabled
+ public var lowPowerMode: (() -> Bool?)? = nil
+
+ /// A Boolean indicating whether the device orientation is portrait (either upright or upside down)
+ public var isPortrait: (() -> Bool?)? = nil
+
+ /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes
+ public var resolution: (() -> String?)? = nil
+
+ /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS)
+ public var scale: (() -> Double?)? = nil
+
+ /// System language currently used on the device (ISO 639)
+ public var language: (() -> String)? = nil
+
+ /// - Parameters:
+ /// - osType: Operating system type (e.g., ios, tvos, watchos, osx, android)
+ /// - osVersion: The current version of the operating system
+ /// - deviceVendor: The manufacturer of the product/hardware
+ /// - deviceModel: The end-user-visible name for the end product
+ /// - carrier: The carrier of the SIM inserted in the device
+ /// - networkType: Type of network the device is connected to
+ /// - networkTechnology: Radio access technology that the device is using
+ /// - appleIdfa: Advertising identifier on iOS
+ /// - appleIdfv: UUID identifier for vendors on iOS
+ /// - availableStorage: Bytes of storage remaining
+ /// - totalStorage: Total size of storage in bytes
+ /// - physicalMemory: Total physical system memory in bytes
+ /// - appAvailableMemory: Amount of memory in bytes available to the current app
+ /// - batteryLevel: Remaining battery level as an integer percentage of total battery capacity
+ /// - batteryState: Battery state for the device
+ /// - lowPowerMode: A Boolean indicating whether Low Power Mode is enabled
+ /// - isPortrait: A Boolean indicating whether the device orientation is portrait (either upright or upside down)
+ /// - resolution: Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes
+ /// - scale: Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS)
+ /// - language: System language currently used on the device (ISO 639)
+ public init(
+ osType: (() -> String)? = nil,
+ osVersion: (() -> String)? = nil,
+ deviceVendor: (() -> String)? = nil,
+ deviceModel: (() -> String)? = nil,
+ carrier: (() -> String?)? = nil,
+ networkType: (() -> String?)? = nil,
+ networkTechnology: (() -> String?)? = nil,
+ appleIdfa: (() -> UUID?)? = nil,
+ appleIdfv: (() -> String?)? = nil,
+ availableStorage: (() -> Int64?)? = nil,
+ totalStorage: (() -> Int64?)? = nil,
+ physicalMemory: (() -> UInt64?)? = nil,
+ appAvailableMemory: (() -> Int?)? = nil,
+ batteryLevel: (() -> Int?)? = nil,
+ batteryState: (() -> String?)? = nil,
+ lowPowerMode: (() -> Bool?)? = nil,
+ isPortrait: (() -> Bool?)? = nil,
+ resolution: (() -> String?)? = nil,
+ scale: (() -> Double?)? = nil,
+ language: (() -> String)? = nil
+ ) {
+ self.osType = osType
+ self.osVersion = osVersion
+ self.deviceVendor = deviceVendor
+ self.deviceModel = deviceModel
+ self.carrier = carrier
+ self.networkType = networkType
+ self.networkTechnology = networkTechnology
+ self.appleIdfa = appleIdfa
+ self.appleIdfv = appleIdfv
+ self.availableStorage = availableStorage
+ self.totalStorage = totalStorage
+ self.physicalMemory = physicalMemory
+ self.appAvailableMemory = appAvailableMemory
+ self.batteryLevel = batteryLevel
+ self.batteryState = batteryState
+ self.lowPowerMode = lowPowerMode
+ self.isPortrait = isPortrait
+ self.resolution = resolution
+ self.scale = scale
+ self.language = language
+ }
+}
diff --git a/Sources/Snowplow/Tracker/SessionState.swift b/Sources/Snowplow/Tracker/SessionState.swift
index cf8f0c90b..c8283a771 100644
--- a/Sources/Snowplow/Tracker/SessionState.swift
+++ b/Sources/Snowplow/Tracker/SessionState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Tracker/View.swift b/Sources/Snowplow/Tracker/View.swift
index 4773ffed0..3cc057133 100644
--- a/Sources/Snowplow/Tracker/View.swift
+++ b/Sources/Snowplow/Tracker/View.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -29,6 +29,16 @@ public extension View {
entities: entities,
trackerNamespace: trackerNamespace))
}
+
+ /// Sets up tracking of list item views that will be aggregated into the `screen_summary` entity if screen engagement tracking is enabled.
+ /// - Parameter index: Index of the item in the list
+ /// - Parameter itemsCount: Total number of items in the list
+ /// - Returns: View with the attached modifier to track list item views
+ func snowplowListItem(index: Int, itemsCount: Int?, trackerNamespace: String? = nil) -> some View {
+ return modifier(ListItemViewModifier(index: index,
+ itemsCount: itemsCount,
+ trackerNamespace: trackerNamespace))
+ }
}
#endif
diff --git a/Sources/Snowplow/Utils/GDPRProcessingBasis.swift b/Sources/Snowplow/Utils/GDPRProcessingBasis.swift
index 13d6f472f..23202ccf7 100644
--- a/Sources/Snowplow/Utils/GDPRProcessingBasis.swift
+++ b/Sources/Snowplow/Utils/GDPRProcessingBasis.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/Utils/SPSize.swift b/Sources/Snowplow/Utils/SPSize.swift
index 94c170de1..cd6c96617 100644
--- a/Sources/Snowplow/Utils/SPSize.swift
+++ b/Sources/Snowplow/Utils/SPSize.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Sources/Snowplow/VisionOS/Entities/ImmersiveSpaceEntity.swift b/Sources/Snowplow/VisionOS/Entities/ImmersiveSpaceEntity.swift
new file mode 100644
index 000000000..0d385f340
--- /dev/null
+++ b/Sources/Snowplow/VisionOS/Entities/ImmersiveSpaceEntity.swift
@@ -0,0 +1,113 @@
+// Copyright (c) 2013-present 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
+
+/// The style of a visionOS immersive space.
+public enum ImmersionStyle: Int {
+ /// Default immersion style.
+ case automatic
+ /// Displays unbounded content that obscures pass-through video.
+ case full
+ /// Displays unbounded content intermixed with other app content.
+ case mixed
+ /// Content displays with no clipping boundaries applied.
+ case progressive
+}
+
+extension ImmersionStyle {
+ var value: String {
+ switch self {
+ case .automatic:
+ return "automatic"
+ case .full:
+ return "full"
+ case .mixed:
+ return "mixed"
+ case .progressive:
+ return "progressive"
+ }
+ }
+}
+
+/// The visibility of the user's upper limbs in a visionOS immersive space.
+public enum UpperLimbVisibility: Int {
+ /// Limbs may be visible or hidden depending on the policies of the component accepting the visibility configuration.
+ case automatic
+ /// Limbs may be visible.
+ case visible
+ /// Limbs may be hidden.
+ case hidden
+}
+
+extension UpperLimbVisibility {
+ var value: String {
+ switch self {
+ case .automatic:
+ return "automatic"
+ case .visible:
+ return "visible"
+ case .hidden:
+ return "hidden"
+ }
+ }
+}
+
+/**
+ Properties for the visionOS immersive space entity.
+ Entity schema: `iglu:com.apple.swiftui/immersive_space/jsonschema/1-0-0`
+ */
+public class ImmersiveSpaceEntity: SelfDescribingJson {
+
+ /// The identifier of the immersive space to present.
+ public var id: String
+
+ /// UUID for the view of the immersive space.
+ public var viewId: UUID?
+
+ /// The style of an immersive space.
+ public var immersionStyle: ImmersionStyle?
+
+ /// Preferred visibility of the user's upper limbs, while an immersive space scene is presented.
+ public var upperLimbVisibility: UpperLimbVisibility?
+
+ override public var data: [String : Any] {
+ get {
+ var data: [String : Any] = [
+ "id": id
+ ]
+ if let viewId = viewId { data["view_id"] = viewId.uuidString }
+ if let immersionStyle = immersionStyle { data["immersion_style"] = immersionStyle.value }
+ if let upperLimbVisibility = upperLimbVisibility { data["upper_limb_visibility"] = upperLimbVisibility.value }
+ return data
+ }
+ set {}
+ }
+
+ /// - Parameter id: A localized string key to use for the window's title in system menus and in the window's title bar.
+ /// - Parameter viewId: UUID for the view of the immersive space. Generated by the tracker if not provided.
+ /// - Parameter immersionStyle: A specification for the appearance and interaction of a window.
+ /// - Parameter upperLimbVisibility: A specification for the appearance and interaction of a window.
+ public init(
+ id: String,
+ viewId: UUID? = nil,
+ immersionStyle: ImmersionStyle? = nil,
+ upperLimbVisibility: UpperLimbVisibility? = nil
+ ) {
+ self.id = id
+ self.viewId = viewId ?? UUID()
+ self.immersionStyle = immersionStyle
+ self.upperLimbVisibility = upperLimbVisibility
+ super.init(schema: swiftuiImmersiveSpaceSchema, andData: [:])
+ }
+}
diff --git a/Sources/Snowplow/VisionOS/Entities/WindowGroupEntity.swift b/Sources/Snowplow/VisionOS/Entities/WindowGroupEntity.swift
new file mode 100644
index 000000000..531e29eb8
--- /dev/null
+++ b/Sources/Snowplow/VisionOS/Entities/WindowGroupEntity.swift
@@ -0,0 +1,94 @@
+// Copyright (c) 2013-present 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
+
+/// A specification for the appearance and interaction of a window.
+public enum WindowStyle: Int {
+ /// Default window style.
+ case automatic
+ /// Hides both the window’s title and the backing of the titlebar area.
+ case hiddenTitleBar
+ /// Plain window style.
+ case plain
+ /// Displays the title bar section of the window.
+ case titleBar
+ /// Creates a 3D volumetric window.
+ case volumetric
+}
+
+extension WindowStyle {
+ var value: String {
+ switch self {
+ case .automatic:
+ return "automatic"
+ case .hiddenTitleBar:
+ return "hiddenTitleBar"
+ case .plain:
+ return "plain"
+ case .titleBar:
+ return "titleBar"
+ case .volumetric:
+ return "volumetric"
+ }
+ }
+}
+
+/**
+ Properties for the SwiftUI window group entity.
+ Entity schema: `iglu:com.apple.swiftui/window_group/jsonschema/1-0-0`
+ */
+public class WindowGroupEntity: SelfDescribingJson {
+
+ /// A string that uniquely identifies the window group. Identifiers must be unique among the window groups in your app.
+ public var id: String
+
+ /// UUID for the current window within the group.
+ public var windowId: UUID?
+
+ /// A localized string key to use for the window's title in system menus and in the window's title bar. Provide a title that describes the purpose of the window.
+ public var titleKey: String?
+
+ /// A specification for the appearance and interaction of a window.
+ public var windowStyle: WindowStyle?
+
+ override public var data: [String : Any] {
+ get {
+ var data: [String : Any] = [
+ "id": id
+ ]
+ if let windowId = windowId { data["window_id"] = windowId.uuidString }
+ if let titleKey = titleKey { data["title_key"] = titleKey }
+ if let windowStyle = windowStyle { data["window_style"] = windowStyle.value }
+ return data
+ }
+ set {}
+ }
+
+ /// - Parameter id: A string that uniquely identifies the window group.
+ /// - Parameter windowId: UUID for the current window within the group.
+ /// - Parameter titleKey: A localized string key to use for the window's title in system menus and in the window's title bar.
+ /// - Parameter windowStyle: A specification for the appearance and interaction of a window.
+ public init(
+ id: String,
+ windowId: UUID? = nil,
+ titleKey: String? = nil,
+ windowStyle: WindowStyle? = nil
+ ) {
+ self.id = id
+ self.windowId = windowId
+ self.titleKey = titleKey
+ self.windowStyle = windowStyle
+ super.init(schema: swiftuiWindowGroupSchema, andData: [:])
+ }
+}
diff --git a/Sources/Snowplow/VisionOS/Events/DismissImmersiveSpaceEvent.swift b/Sources/Snowplow/VisionOS/Events/DismissImmersiveSpaceEvent.swift
new file mode 100644
index 000000000..c9a4608a4
--- /dev/null
+++ b/Sources/Snowplow/VisionOS/Events/DismissImmersiveSpaceEvent.swift
@@ -0,0 +1,26 @@
+// Copyright (c) 2013-present 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
+
+/** Event for a visionOS immersive space being dismissed. */
+public class DismissImmersiveSpaceEvent: SelfDescribingAbstract {
+
+ override var schema: String {
+ return swiftuiDismissImmersiveSpaceSchema
+ }
+
+ override var payload: [String : Any] {
+ return [:]
+ }
+}
diff --git a/Sources/Snowplow/VisionOS/Events/DismissWindowEvent.swift b/Sources/Snowplow/VisionOS/Events/DismissWindowEvent.swift
new file mode 100644
index 000000000..d73f41456
--- /dev/null
+++ b/Sources/Snowplow/VisionOS/Events/DismissWindowEvent.swift
@@ -0,0 +1,68 @@
+// Copyright (c) 2013-present 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
+
+/** Event for a SwiftUI window group being dismissed. */
+public class DismissWindowEvent: SelfDescribingAbstract {
+
+ /// A string that uniquely identifies the window group. Identifiers must be unique among the window groups in your app.
+ public var id: String
+
+ /// UUID for the current window within the group.
+ public var windowId: UUID?
+
+ /// A localized string key to use for the window's title in system menus and in the window's title bar. Provide a title that describes the purpose of the window.
+ public var titleKey: String?
+
+ /// A specification for the appearance and interaction of a window.
+ public var windowStyle: WindowStyle?
+
+ override var schema: String {
+ return swiftuiDismissWindowSchema
+ }
+
+ override var payload: [String : Any] {
+ return [:]
+ }
+
+ override internal var entitiesForProcessing: [SelfDescribingJson]? {
+ get {
+ var entities = [SelfDescribingJson]()
+ let windowGroup = WindowGroupEntity(
+ id: self.id,
+ windowId: self.windowId,
+ titleKey: self.titleKey,
+ windowStyle: self.windowStyle
+ )
+ entities.append(windowGroup)
+ return entities
+ }
+ }
+
+ /// - Parameter id: A string that uniquely identifies the window group.
+ /// - Parameter windowId: UUID for the current window within the group.
+ /// - Parameter titleKey: A localized string key to use for the window's title in system menus and in the window's title bar.
+ /// - Parameter windowStyle: A specification for the appearance and interaction of a window.
+ public init(
+ id: String,
+ windowId: UUID? = nil,
+ titleKey: String? = nil,
+ windowStyle: WindowStyle? = nil
+ ) {
+ self.id = id
+ self.windowId = windowId
+ self.titleKey = titleKey
+ self.windowStyle = windowStyle
+ }
+}
diff --git a/Sources/Snowplow/VisionOS/Events/OpenImmersiveSpaceEvent.swift b/Sources/Snowplow/VisionOS/Events/OpenImmersiveSpaceEvent.swift
new file mode 100644
index 000000000..b6e168ba5
--- /dev/null
+++ b/Sources/Snowplow/VisionOS/Events/OpenImmersiveSpaceEvent.swift
@@ -0,0 +1,68 @@
+// Copyright (c) 2013-present 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
+
+/** Event for a visionOS immersive space being opened. */
+public class OpenImmersiveSpaceEvent: SelfDescribingAbstract {
+
+ /// The identifier of the immersive space to present.
+ public var id: String
+
+ /// UUID for the view of the immersive space.
+ public var viewId: UUID?
+
+ /// The style of an immersive space.
+ public var immersionStyle: ImmersionStyle?
+
+ /// Preferred visibility of the user's upper limbs, while an immersive space scene is presented.
+ public var upperLimbVisibility: UpperLimbVisibility?
+
+ override var schema: String {
+ return swiftuiOpenImmersiveSpaceSchema
+ }
+
+ override var payload: [String : Any] {
+ return [:]
+ }
+
+ override internal var entitiesForProcessing: [SelfDescribingJson]? {
+ get {
+ var entities = [SelfDescribingJson]()
+ let space = ImmersiveSpaceEntity(
+ id: self.id,
+ viewId: self.viewId,
+ immersionStyle: self.immersionStyle,
+ upperLimbVisibility: self.upperLimbVisibility
+ )
+ entities.append(space)
+ return entities
+ }
+ }
+
+ /// - Parameter id: A localized string key to use for the window's title in system menus and in the window's title bar.
+ /// - Parameter viewId: UUID for the view of the immersive space.
+ /// - Parameter immersionStyle: A specification for the appearance and interaction of a window.
+ /// - Parameter upperLimbVisibility: A specification for the appearance and interaction of a window.
+ public init(
+ id: String,
+ viewId: UUID? = nil,
+ immersionStyle: ImmersionStyle? = nil,
+ upperLimbVisibility: UpperLimbVisibility? = nil
+ ) {
+ self.id = id
+ self.viewId = viewId
+ self.immersionStyle = immersionStyle
+ self.upperLimbVisibility = upperLimbVisibility
+ }
+}
diff --git a/Sources/Snowplow/VisionOS/Events/OpenWindowEvent.swift b/Sources/Snowplow/VisionOS/Events/OpenWindowEvent.swift
new file mode 100644
index 000000000..d41f2e658
--- /dev/null
+++ b/Sources/Snowplow/VisionOS/Events/OpenWindowEvent.swift
@@ -0,0 +1,68 @@
+// Copyright (c) 2013-present 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
+
+/** Event for a SwiftUI window group being opened. */
+public class OpenWindowEvent: SelfDescribingAbstract {
+
+ /// A string that uniquely identifies the window group. Identifiers must be unique among the window groups in your app.
+ public var id: String
+
+ /// UUID for the current window within the group.
+ public var windowId: UUID?
+
+ /// A localized string key to use for the window's title in system menus and in the window's title bar. Provide a title that describes the purpose of the window.
+ public var titleKey: String?
+
+ /// A specification for the appearance and interaction of a window.
+ public var windowStyle: WindowStyle?
+
+ override var schema: String {
+ return swiftuiOpenWindowSchema
+ }
+
+ override var payload: [String : Any] {
+ return [:]
+ }
+
+ override internal var entitiesForProcessing: [SelfDescribingJson]? {
+ get {
+ var entities = [SelfDescribingJson]()
+ let windowGroup = WindowGroupEntity(
+ id: self.id,
+ windowId: self.windowId,
+ titleKey: self.titleKey,
+ windowStyle: self.windowStyle
+ )
+ entities.append(windowGroup)
+ return entities
+ }
+ }
+
+ /// - Parameter id: A string that uniquely identifies the window group.
+ /// - Parameter windowId: UUID for the current window within the group.
+ /// - Parameter titleKey: A localized string key to use for the window's title in system menus and in the window's title bar.
+ /// - Parameter windowStyle: A specification for the appearance and interaction of a window.
+ public init(
+ id: String,
+ windowId: UUID? = nil,
+ titleKey: String? = nil,
+ windowStyle: WindowStyle? = nil
+ ) {
+ self.id = id
+ self.windowId = windowId
+ self.titleKey = titleKey
+ self.windowStyle = windowStyle
+ }
+}
diff --git a/Tests/Configurations/TestConfigurationBuilder.swift b/Tests/Configurations/TestConfigurationBuilder.swift
index d3ba1eec4..c0662b0e4 100644
--- a/Tests/Configurations/TestConfigurationBuilder.swift
+++ b/Tests/Configurations/TestConfigurationBuilder.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Configurations/TestEmitterConfiguration.swift b/Tests/Configurations/TestEmitterConfiguration.swift
index 0abcd88c2..9cea943f2 100644
--- a/Tests/Configurations/TestEmitterConfiguration.swift
+++ b/Tests/Configurations/TestEmitterConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -80,6 +80,27 @@ class TestEmitterConfiguration: XCTestCase {
XCTAssertEqual(0, tracker.emitter?.dbCount)
}
+ func testAllowsAccessToTheEventStore() {
+ let networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 200)
+ let networkConfig = NetworkConfiguration(networkConnection: networkConnection)
+
+ let tracker = createTracker(networkConfig: networkConfig, emitterConfig: EmitterConfiguration())
+
+ tracker.emitter?.pause()
+ for i in 0..<10 {
+ _ = tracker.track(Structured(category: "cat", action: "act").value(NSNumber(value: i)))
+ }
+ Thread.sleep(forTimeInterval: 0.5)
+
+ XCTAssertEqual(10, tracker.emitter?.dbCount)
+ XCTAssertEqual(10, tracker.emitter?.eventStore.count())
+
+ XCTAssertTrue(tracker.emitter?.eventStore.removeAllEvents() ?? false)
+
+ XCTAssertEqual(0, tracker.emitter?.dbCount)
+ XCTAssertEqual(0, tracker.emitter?.eventStore.count())
+ }
+
private func createTracker(networkConfig: NetworkConfiguration, emitterConfig: EmitterConfiguration) -> TrackerController {
let trackerConfig = TrackerConfiguration()
trackerConfig.installAutotracking = false
@@ -88,7 +109,7 @@ class TestEmitterConfiguration: XCTestCase {
let namespace = "testEmitter" + String(describing: Int.random(in: 0..<100))
return Snowplow.createTracker(namespace: namespace,
network: networkConfig,
- configurations: [trackerConfig, emitterConfig])!
+ configurations: [trackerConfig, emitterConfig])
}
}
diff --git a/Tests/Configurations/TestFocalMeterConfiguration.swift b/Tests/Configurations/TestFocalMeterConfiguration.swift
index 3016d0498..463fdf918 100644
--- a/Tests/Configurations/TestFocalMeterConfiguration.swift
+++ b/Tests/Configurations/TestFocalMeterConfiguration.swift
@@ -2,7 +2,7 @@
// TestFocalMeterConfiguration.swift
// Snowplow-iOSTests
//
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -130,7 +130,7 @@ class TestFocalMeterConfiguration: XCTestCase {
configurations: [
trackerConfig,
focalMeterConfig ?? FocalMeterConfiguration(kantarEndpoint: endpoint)
- ])!
+ ])
}
#endif
diff --git a/Tests/Configurations/TestMultipleInstances.swift b/Tests/Configurations/TestMultipleInstances.swift
index 7cf3fa36b..fee386709 100644
--- a/Tests/Configurations/TestMultipleInstances.swift
+++ b/Tests/Configurations/TestMultipleInstances.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -25,18 +25,18 @@ class TestMultipleInstances: XCTestCase {
func testSingleInstanceIsReconfigurable() {
let t1 = Snowplow.createTracker(namespace: "t1", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake"))
- XCTAssertEqual(t1?.network?.endpoint, "https://snowplowanalytics.fake/com.snowplowanalytics.snowplow/tp2")
+ XCTAssertEqual(t1.network?.endpoint, "https://snowplowanalytics.fake/com.snowplowanalytics.snowplow/tp2")
let t2 = Snowplow.createTracker(namespace: "t1", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake2"))
- XCTAssertEqual(t2?.network?.endpoint, "https://snowplowanalytics.fake2/com.snowplowanalytics.snowplow/tp2")
+ 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() {
let t1 = Snowplow.createTracker(namespace: "t1", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake"))
- XCTAssertEqual(t1?.network?.endpoint, "https://snowplowanalytics.fake/com.snowplowanalytics.snowplow/tp2")
+ XCTAssertEqual(t1.network?.endpoint, "https://snowplowanalytics.fake/com.snowplowanalytics.snowplow/tp2")
let t2 = Snowplow.createTracker(namespace: "t2", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake2"))
- XCTAssertEqual(t2?.network?.endpoint, "https://snowplowanalytics.fake2/com.snowplowanalytics.snowplow/tp2")
+ XCTAssertEqual(t2.network?.endpoint, "https://snowplowanalytics.fake2/com.snowplowanalytics.snowplow/tp2")
XCTAssertFalse(t1 === t2)
let expectedNamespaces = Set(["t1", "t2"])
XCTAssertEqual(expectedNamespaces, Set(Snowplow.instancedTrackerNamespaces))
@@ -46,7 +46,7 @@ class TestMultipleInstances: XCTestCase {
let t1 = Snowplow.createTracker(namespace: "t1", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake"))
_ = Snowplow.createTracker(namespace: "t2", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake2"))
let td = Snowplow.defaultTracker()
- XCTAssertEqual(t1?.namespace, td?.namespace)
+ XCTAssertEqual(t1.namespace, td?.namespace)
}
func testUpdateDefaultTracker() {
@@ -54,7 +54,7 @@ class TestMultipleInstances: XCTestCase {
let t2 = Snowplow.createTracker(namespace: "t2", network: NetworkConfiguration(endpoint: "snowplowanalytics.fake2"))
_ = Snowplow.setAsDefault(tracker: t2)
let td = Snowplow.defaultTracker()
- XCTAssertEqual(t2?.namespace, td?.namespace)
+ XCTAssertEqual(t2.namespace, td?.namespace)
}
func testRemoveTracker() {
diff --git a/Tests/Configurations/TestRemoteConfiguration.swift b/Tests/Configurations/TestRemoteConfiguration.swift
index 982d2302e..c362e54db 100644
--- a/Tests/Configurations/TestRemoteConfiguration.swift
+++ b/Tests/Configurations/TestRemoteConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Configurations/TestTrackerConfiguration.swift b/Tests/Configurations/TestTrackerConfiguration.swift
index f43661be9..bcff59bc8 100644
--- a/Tests/Configurations/TestTrackerConfiguration.swift
+++ b/Tests/Configurations/TestTrackerConfiguration.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -94,8 +94,8 @@ class TestTrackerConfiguration: XCTestCase {
let tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig])
XCTAssertNotNil(tracker)
- XCTAssertNotNil(tracker?.emitter)
- let url = URL(string: tracker?.network?.endpoint ?? "")
+ XCTAssertNotNil(tracker.emitter)
+ let url = URL(string: tracker.network?.endpoint ?? "")
XCTAssertNotNil(url)
let host = url?.host
let scheme = url?.scheme
@@ -106,8 +106,8 @@ class TestTrackerConfiguration: XCTestCase {
XCTAssertEqual(networkConfig.endpoint, derivedEndpoint)
XCTAssertEqual(`protocol`, scheme)
- XCTAssertEqual(trackerConfig.appId, tracker?.appId)
- XCTAssertEqual("namespace", tracker?.namespace)
+ XCTAssertEqual(trackerConfig.appId, tracker.appId)
+ XCTAssertEqual("namespace", tracker.namespace)
}
func testSessionInitialization() {
@@ -120,13 +120,13 @@ class TestTrackerConfiguration: XCTestCase {
backgroundTimeoutInSeconds: expectedBackground)
let tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig, sessionConfig])
- let foreground = tracker?.session?.foregroundTimeoutInSeconds ?? 0
- let background = tracker?.session?.backgroundTimeoutInSeconds ?? 0
+ let foreground = tracker.session?.foregroundTimeoutInSeconds ?? 0
+ let background = tracker.session?.backgroundTimeoutInSeconds ?? 0
XCTAssertEqual(expectedForeground, foreground)
XCTAssertEqual(expectedBackground, background)
- let foregroundMeasure = (tracker?.session)?.foregroundTimeout
- let backgroundMeasure = (tracker?.session)?.backgroundTimeout
+ let foregroundMeasure = (tracker.session)?.foregroundTimeout
+ let backgroundMeasure = (tracker.session)?.backgroundTimeout
XCTAssertEqual(Measurement(value: Double(expectedForeground), unit: UnitDuration.seconds), foregroundMeasure)
XCTAssertEqual(Measurement(value: Double(expectedBackground), unit: UnitDuration.seconds), backgroundMeasure)
}
@@ -136,11 +136,11 @@ class TestTrackerConfiguration: XCTestCase {
let trackerConfig = TrackerConfiguration(appId: "appid")
trackerConfig.sessionContext = true
var tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig])
- XCTAssertNotNil(tracker?.session)
+ XCTAssertNotNil(tracker.session)
trackerConfig.sessionContext = false
tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig])
- XCTAssertNil(tracker?.session)
+ XCTAssertNil(tracker.session)
}
func testSessionConfigurationCallback() {
@@ -157,7 +157,7 @@ class TestTrackerConfiguration: XCTestCase {
expectation.fulfill()
}
- guard let tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig, sessionConfig]) else { return XCTFail() }
+ let tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig, sessionConfig])
_ = tracker.track(Timing(category: "cat", variable: "var", timing: 123))
@@ -185,9 +185,11 @@ class TestTrackerConfiguration: XCTestCase {
let tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig, sessionConfig])
- _ = tracker?.track(Timing(category: "cat", variable: "var", timing: 123))
- tracker?.session?.startNewSession()
- _ = tracker?.track(Timing(category: "cat", variable: "var", timing: 123))
+ _ = tracker.track(Timing(category: "cat", variable: "var", timing: 123))
+ Thread.sleep(forTimeInterval: 0.1)
+ tracker.session?.startNewSession()
+ _ = tracker.track(Timing(category: "cat", variable: "var", timing: 123))
+ Thread.sleep(forTimeInterval: 0.1)
wait(for: [expectation], timeout: 10)
}
@@ -207,7 +209,7 @@ class TestTrackerConfiguration: XCTestCase {
// Track fake event
let event = Structured(category: "category", action: "action")
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -232,7 +234,7 @@ class TestTrackerConfiguration: XCTestCase {
emitterConfiguration.threadPoolSize = 10
let gdprConfiguration = GDPRConfiguration(basis: .consent, documentId: "id", documentVersion: "ver", documentDescription: "desc")
let trackerController = Snowplow.createTracker(namespace: "namespace", network: networkConfiguration, configurations: [trackerConfiguration, gdprConfiguration, emitterConfiguration])
- let gdprController = trackerController?.gdpr
+ let gdprController = trackerController.gdpr
// Check gdpr settings
XCTAssertEqual(.consent, gdprController?.basisForProcessing)
@@ -246,7 +248,7 @@ class TestTrackerConfiguration: XCTestCase {
// Check gdpr context added
var event = Structured(category: "category", action: "action")
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -266,7 +268,7 @@ class TestTrackerConfiguration: XCTestCase {
// Check gdpr context not added
event = Structured(category: "category", action: "action")
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -292,14 +294,14 @@ class TestTrackerConfiguration: XCTestCase {
emitterConfiguration.eventStore = eventStore
emitterConfiguration.threadPoolSize = 10
let trackerController = Snowplow.createTracker(namespace: "namespace", network: networkConfiguration, configurations: [trackerConfiguration, emitterConfiguration])
- let gdprController = trackerController?.gdpr
+ let gdprController = trackerController.gdpr
// Check gdpr settings
XCTAssertFalse(gdprController?.isEnabled ?? false)
// Check gdpr context not added
var event = Structured(category: "category", action: "action")
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -318,7 +320,7 @@ class TestTrackerConfiguration: XCTestCase {
// Check gdpr context added
event = Structured(category: "category", action: "action")
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -347,7 +349,7 @@ class TestTrackerConfiguration: XCTestCase {
// Track an event and retrieve tracked context JSON from event store
let event = Structured(category: "category", action: "action")
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -376,7 +378,7 @@ class TestTrackerConfiguration: XCTestCase {
// Track fake event
let event = Structured(category: "category", action: "action")
- let eventId = trackerController?.track(event)
+ let eventId = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -387,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/Configurations/TestTrackerController.swift b/Tests/Configurations/TestTrackerController.swift
index 185789481..cca06983f 100644
--- a/Tests/Configurations/TestTrackerController.swift
+++ b/Tests/Configurations/TestTrackerController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -50,16 +50,19 @@ class TestTrackerController: XCTestCase {
tracker?.emitter?.pause()
_ = tracker?.track(Structured(category: "c", action: "a"))
+ Thread.sleep(forTimeInterval: 0.1)
let sessionIdBefore = tracker?.session?.sessionId
tracker?.userAnonymisation = true
_ = tracker?.track(Structured(category: "c", action: "a"))
+ Thread.sleep(forTimeInterval: 0.1)
let sessionIdAnonymous = tracker?.session?.sessionId
XCTAssertFalse((sessionIdBefore == sessionIdAnonymous))
tracker?.userAnonymisation = false
_ = tracker?.track(Structured(category: "c", action: "a"))
+ Thread.sleep(forTimeInterval: 0.1)
let sessionIdNotAnonymous = tracker?.session?.sessionId
XCTAssertFalse((sessionIdAnonymous == sessionIdNotAnonymous))
diff --git a/Tests/Ecommerce/TestEcommerceController.swift b/Tests/Ecommerce/TestEcommerceController.swift
index b1727aae1..8e8fdabe6 100644
--- a/Tests/Ecommerce/TestEcommerceController.swift
+++ b/Tests/Ecommerce/TestEcommerceController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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() {
@@ -101,18 +102,13 @@ class TestEcommerceController: XCTestCase {
let trackerConfig = TrackerConfiguration()
trackerConfig.installAutotracking = false
trackerConfig.lifecycleAutotracking = false
+ trackerConfig.screenEngagementAutotracking = 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/TestEcommerceEntities.swift b/Tests/Ecommerce/TestEcommerceEntities.swift
index 64fa0caf1..d7bb5a9d1 100644
--- a/Tests/Ecommerce/TestEcommerceEntities.swift
+++ b/Tests/Ecommerce/TestEcommerceEntities.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Ecommerce/TestEcommerceEvents.swift b/Tests/Ecommerce/TestEcommerceEvents.swift
index f04a9c016..ba3b39dc5 100644
--- a/Tests/Ecommerce/TestEcommerceEvents.swift
+++ b/Tests/Ecommerce/TestEcommerceEvents.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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..1b154df3b 100644
--- a/Tests/Global Contexts/TestGlobalContexts.swift
+++ b/Tests/Global Contexts/TestGlobalContexts.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/Global Contexts/TestSchemaRuleset.swift b/Tests/Global Contexts/TestSchemaRuleset.swift
index ac2839679..451e690b8 100644
--- a/Tests/Global Contexts/TestSchemaRuleset.swift
+++ b/Tests/Global Contexts/TestSchemaRuleset.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Legacy Tests/LegacyTestSubject.swift b/Tests/Legacy Tests/LegacyTestSubject.swift
index 8fe7366ae..f14ffc709 100644
--- a/Tests/Legacy Tests/LegacyTestSubject.swift
+++ b/Tests/Legacy Tests/LegacyTestSubject.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -33,7 +33,7 @@ class LegacyTestSubject: XCTestCase {
func testSubjectInitWithOptions() {
let subject = Subject(platformContext: true, geoLocationContext: false)
- XCTAssertNotNil(subject.platformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil))
+ XCTAssertNotNil(subject.platformDict(userAnonymisation: false))
XCTAssertNotNil(subject.standardDict(userAnonymisation: false))
}
diff --git a/Tests/Media/TestMediaAdTracking.swift b/Tests/Media/TestMediaAdTracking.swift
index 32aa6278e..7dfae30ac 100644
--- a/Tests/Media/TestMediaAdTracking.swift
+++ b/Tests/Media/TestMediaAdTracking.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Media/TestMediaController.swift b/Tests/Media/TestMediaController.swift
index 211312adf..b1c7ef3ab 100644
--- a/Tests/Media/TestMediaController.swift
+++ b/Tests/Media/TestMediaController.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/Media/TestMediaEventAndEntitySerialization.swift b/Tests/Media/TestMediaEventAndEntitySerialization.swift
index 249cb4d44..d07dba6fc 100644
--- a/Tests/Media/TestMediaEventAndEntitySerialization.swift
+++ b/Tests/Media/TestMediaEventAndEntitySerialization.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Media/TestMediaSessionTrackingStats.swift b/Tests/Media/TestMediaSessionTrackingStats.swift
index 99ffe9151..867b7d765 100644
--- a/Tests/Media/TestMediaSessionTrackingStats.swift
+++ b/Tests/Media/TestMediaSessionTrackingStats.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/ScreenViewTracking/TestListItemViewModifier.swift b/Tests/ScreenViewTracking/TestListItemViewModifier.swift
new file mode 100644
index 000000000..5785f74c8
--- /dev/null
+++ b/Tests/ScreenViewTracking/TestListItemViewModifier.swift
@@ -0,0 +1,68 @@
+// Copyright (c) 2013-present 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
+import XCTest
+@testable import SnowplowTracker
+
+#if canImport(SwiftUI)
+#if os(iOS) || os(tvOS) || os(macOS)
+
+class TestListItemViewModifier: XCTestCase {
+ func testTracksListItemViewEvent() {
+ if #available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, *) {
+ let expect = expectation(description: "Event received")
+ createTracker { event in
+ XCTAssertEqual(1, event.payload["index"] as? Int)
+ XCTAssertEqual(5, event.payload["items_count"] as? Int)
+ XCTAssertEqual(kSPListItemViewSchema, event.schema)
+ expect.fulfill()
+ }
+
+ let modifier = ListItemViewModifier(
+ index: 1,
+ itemsCount: 5,
+ trackerNamespace: "listItemViewTracker"
+ )
+ modifier.trackListItemView()
+
+ wait(for: [expect], timeout: 1)
+ }
+ }
+
+ private func createTracker(afterTrack: @escaping (InspectableEvent) -> ()) {
+ let networkConfig = NetworkConfiguration(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 200))
+
+ _ = Snowplow.createTracker(
+ namespace: "listItemViewTracker",
+ network: networkConfig,
+ configurations: [
+ EventSink(callback: afterTrack),
+ TrackerConfiguration()
+ .installAutotracking(false)
+ .lifecycleAutotracking(false)
+ .screenEngagementAutotracking(false)
+ ])
+ }
+}
+
+private struct ScreenViewExpected: Codable {
+ let name: String
+}
+
+private struct AnythingEntityExpected: Codable {
+ let works: Bool
+}
+
+#endif
+#endif
diff --git a/Tests/TestScreenState.swift b/Tests/ScreenViewTracking/TestScreenState.swift
similarity index 86%
rename from Tests/TestScreenState.swift
rename to Tests/ScreenViewTracking/TestScreenState.swift
index 30ceeb8bd..40af146f1 100644
--- a/Tests/TestScreenState.swift
+++ b/Tests/ScreenViewTracking/TestScreenState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/ScreenViewTracking/TestScreenSummaryStateMachine.swift b/Tests/ScreenViewTracking/TestScreenSummaryStateMachine.swift
new file mode 100644
index 000000000..48ea722b1
--- /dev/null
+++ b/Tests/ScreenViewTracking/TestScreenSummaryStateMachine.swift
@@ -0,0 +1,142 @@
+// Copyright (c) 2013-present 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 XCTest
+@testable import SnowplowTracker
+
+import Foundation
+class TestScreenSummaryStateMachine: XCTestCase {
+ var timeTraveler = TimeTraveler()
+
+ override func setUp() {
+ ScreenSummaryState.dateGenerator = timeTraveler.generateTimeInterval
+ super.setUp()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ }
+
+ func testTrackTransitionToBackgroundAndForeground() {
+ let expectBackground = expectation(description: "Background event")
+ let expectForeground = expectation(description: "Foreground event")
+
+ let eventSink = EventSink { event in
+ if event.schema == kSPBackgroundSchema {
+ let entity = event.entities.first { $0.schema == kSPScreenSummarySchema }
+ XCTAssertEqual((entity?.data as? [String: Any])?["foreground_sec"] as? Double, 10.0)
+ XCTAssertEqual((entity?.data as? [String: Any])?["background_sec"] as? Double, 0.0)
+ expectBackground.fulfill()
+ }
+
+ if event.schema == kSPForegroundSchema {
+ let entity = event.entities.first { $0.schema == kSPScreenSummarySchema }
+ XCTAssertEqual((entity?.data as? [String: Any])?["foreground_sec"] as? Double, 10.0)
+ XCTAssertEqual((entity?.data as? [String: Any])?["background_sec"] as? Double, 5.0)
+ expectForeground.fulfill()
+ }
+ }
+
+ let tracker = createTracker([eventSink])
+
+ _ = tracker.track(ScreenView(name: "Screen 1"))
+ InternalQueue.sync { timeTraveler.travel(by: 10) }
+ _ = tracker.track(Background(index: 1))
+ InternalQueue.sync { timeTraveler.travel(by: 5) }
+ _ = tracker.track(Foreground(index: 1))
+
+ wait(for: [expectBackground, expectForeground], timeout: 10)
+ }
+
+ func testTracksScreenEndEventWithScreenSummary() {
+ let expectScreenEnd = expectation(description: "Screen end event")
+
+ let eventSink = EventSink { event in
+ if event.schema == kSPScreenEndSchema {
+ let entity = event.entities.first { $0.schema == kSPScreenSummarySchema }
+ XCTAssertEqual((entity?.data as? [String: Any])?["foreground_sec"] as? Double, 10.0)
+ XCTAssertEqual((entity?.data as? [String: Any])?["background_sec"] as? Double, 0.0)
+ expectScreenEnd.fulfill()
+ }
+ }
+
+ let tracker = createTracker([eventSink])
+
+ _ = tracker.track(ScreenView(name: "Screen 1"))
+ InternalQueue.sync { timeTraveler.travel(by: 10) }
+ _ = tracker.track(ScreenView(name: "Screen 2"))
+
+ wait(for: [expectScreenEnd], timeout: 10)
+ }
+
+ func testUpdatesListMetrics() {
+ let expectScreenEnd = expectation(description: "Screen end event")
+
+ let eventSink = EventSink { event in
+ if event.schema == kSPScreenEndSchema {
+ let entity = event.entities.first { $0.schema == kSPScreenSummarySchema }
+ XCTAssertEqual((entity?.data as? [String: Any])?["last_item_index"] as? Int, 3)
+ XCTAssertEqual((entity?.data as? [String: Any])?["items_count"] as? Int, 10)
+ expectScreenEnd.fulfill()
+ }
+ }
+
+ let tracker = createTracker([eventSink])
+
+ _ = tracker.track(ScreenView(name: "Screen 1"))
+ _ = tracker.track(ListItemView(index: 1, totalItems: 10))
+ _ = tracker.track(ListItemView(index: 3, totalItems: 10))
+ _ = tracker.track(ListItemView(index: 2, totalItems: 10))
+ _ = tracker.track(ScreenView(name: "Screen 2"))
+
+ wait(for: [expectScreenEnd], timeout: 10)
+ }
+
+ func testUpdatesScrollMetrics() {
+ let expectScreenEnd = expectation(description: "Screen end event")
+
+ let eventSink = EventSink { event in
+ if event.schema == kSPScreenEndSchema {
+ let entity = event.entities.first { $0.schema == kSPScreenSummarySchema }
+ XCTAssertEqual((entity?.data as? [String: Any])?["min_y_offset"] as? Int, 10)
+ XCTAssertEqual((entity?.data as? [String: Any])?["min_x_offset"] as? Int, 15)
+ XCTAssertEqual((entity?.data as? [String: Any])?["max_y_offset"] as? Int, 50)
+ XCTAssertEqual((entity?.data as? [String: Any])?["max_x_offset"] as? Int, 30)
+ XCTAssertEqual((entity?.data as? [String: Any])?["content_height"] as? Int, 100)
+ XCTAssertEqual((entity?.data as? [String: Any])?["content_width"] as? Int, 200)
+ expectScreenEnd.fulfill()
+ }
+ }
+
+ let tracker = createTracker([eventSink])
+
+ _ = tracker.track(ScreenView(name: "Screen 1"))
+ _ = tracker.track(ScrollChanged(yOffset: 10, viewHeight: 20, contentHeight: 100))
+ _ = tracker.track(ScrollChanged(xOffset: 15, yOffset: 30, viewWidth: 15, viewHeight: 20, contentWidth: 200, contentHeight: 100))
+ _ = tracker.track(ScrollChanged(yOffset: 20, viewHeight: 20, contentHeight: 100))
+ _ = tracker.track(ScreenView(name: "Screen 2"))
+
+ wait(for: [expectScreenEnd], timeout: 10)
+ }
+
+ private func createTracker(_ configurations: [ConfigurationProtocol]) -> TrackerController {
+ let networkConfig = NetworkConfiguration(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 200))
+ let trackerConfig = TrackerConfiguration()
+ trackerConfig.installAutotracking = false
+ trackerConfig.lifecycleAutotracking = false
+ let namespace = "testScreenSummary" + String(describing: Int.random(in: 0..<100))
+ return Snowplow.createTracker(namespace: namespace,
+ network: networkConfig,
+ configurations: configurations + [trackerConfig])
+ }
+}
diff --git a/Tests/TestScreenViewModifier.swift b/Tests/ScreenViewTracking/TestScreenViewModifier.swift
similarity index 93%
rename from Tests/TestScreenViewModifier.swift
rename to Tests/ScreenViewTracking/TestScreenViewModifier.swift
index 82743a788..08f629c33 100644
--- a/Tests/TestScreenViewModifier.swift
+++ b/Tests/ScreenViewTracking/TestScreenViewModifier.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/Storage/TestDatabase.swift b/Tests/Storage/TestDatabase.swift
new file mode 100644
index 000000000..7add3ebbf
--- /dev/null
+++ b/Tests/Storage/TestDatabase.swift
@@ -0,0 +1,159 @@
+// Copyright (c) 2013-present 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.
+
+#if os(iOS) || os(macOS) || os(visionOS)
+
+import XCTest
+@testable import SnowplowTracker
+
+class TestDatabase: XCTestCase {
+
+ func testDatabasePathConsistentForNamespace() {
+ XCTAssertEqual(Database.dbPath(namespace: "ns1"), Database.dbPath(namespace: "ns1"))
+ }
+
+ func testDatabasePathDiffersByNamespace() {
+ XCTAssertNotEqual(Database.dbPath(namespace: "ns1"), Database.dbPath(namespace: "ns2"))
+ }
+
+ func testDatabasePathDoesntContainSpecialChars() {
+ XCTAssertFalse(Database.dbPath(namespace: "%*$@db").contains("%*$@"))
+ }
+
+ func testInsertsAndReadsRow() {
+ let database = createDatabase("db1")
+
+ database.insertRow(["test": true])
+ let rows = database.readRows(numRows: 100)
+
+ XCTAssertEqual(1, rows.count)
+ XCTAssertEqual(
+ try? JSONSerialization.data(withJSONObject: rows.first?.data ?? []),
+ try? JSONSerialization.data(withJSONObject: ["test": true])
+ )
+ }
+
+ func testCanWorkWithTwoOpenDatabases() {
+ let db1 = createDatabase("db1")
+ let db2 = createDatabase("db2")
+
+ db1.insertRow(["test": 1])
+ db2.insertRow(["test": 2])
+ let rows1 = db1.readRows(numRows: 100)
+ let rows2 = db2.readRows(numRows: 100)
+
+ XCTAssertEqual(1, rows1.count)
+ XCTAssertEqual(1, rows2.count)
+
+ XCTAssertNotEqual(
+ try? JSONSerialization.data(withJSONObject: rows1.first?.data ?? []),
+ try? JSONSerialization.data(withJSONObject: rows2.first?.data ?? [])
+ )
+ }
+
+ func testDeleteAllRows() {
+ let db = createDatabase("db")
+
+ db.insertRow(["test": 1])
+ db.insertRow(["test": 2])
+
+ XCTAssertEqual(db.readRows(numRows: 100).count, 2)
+
+ XCTAssertTrue(db.deleteRows())
+
+ XCTAssertEqual(db.readRows(numRows: 100).count, 0)
+ }
+
+ func testDeleteSpecificRows() {
+ let db = createDatabase("db")
+
+ db.insertRow(["test": 1])
+ db.insertRow(["test": 2])
+
+ let rows = db.readRows(numRows: 100)
+ XCTAssertEqual(rows.count, 2)
+
+ XCTAssertTrue(db.deleteRows(ids: [rows.first?.id ?? 0]))
+
+ let newRows = db.readRows(numRows: 100)
+ XCTAssertEqual(newRows.count, 1)
+ XCTAssertEqual(newRows.first?.id, rows.last?.id)
+ }
+
+ func testSelectRowsWithLimit() {
+ let db = createDatabase("db")
+
+ db.insertRow(["test": 1])
+ db.insertRow(["test": 2])
+
+ let rows = db.readRows(numRows: 1)
+ XCTAssertEqual(rows.count, 1)
+ }
+
+ func testCountRows() {
+ let db = createDatabase("db")
+
+ db.insertRow(["test": 1])
+ XCTAssertEqual(db.countRows(), 1)
+ db.insertRow(["test": 2])
+ XCTAssertEqual(db.countRows(), 2)
+ XCTAssertTrue(db.deleteRows())
+ XCTAssertEqual(db.countRows(), 0)
+ }
+
+ func testRemoveOldEventsByAge() {
+ let db = createDatabase("db")
+
+ for i in 1...5 {
+ db.insertRow(["test": i])
+ }
+
+ Thread.sleep(forTimeInterval: 2)
+
+ for i in 6...10 {
+ db.insertRow(["test": i])
+ }
+
+ db.removeOldEvents(maxSize: 10, maxAge: 1)
+
+ let rows = db.readRows(numRows: 10)
+ XCTAssertEqual(rows.count, 5)
+ XCTAssertEqual(
+ rows.map { $0.data["test"] as! Int }.min(),
+ 6
+ )
+ }
+
+ func testRemoveOldestEventsByMaxSize() {
+ let db = createDatabase("db")
+
+ for i in 1...5 {
+ db.insertRow(["test": i])
+ }
+ db.removeOldEvents(maxSize: 3, maxAge: 5)
+
+ let rows = db.readRows(numRows: 5)
+ XCTAssertEqual(rows.count, 3)
+ XCTAssertEqual(
+ rows.map { $0.data["test"] as! Int }.min(),
+ 3
+ )
+ }
+
+ private func createDatabase(_ namespace: String) -> Database {
+ DatabaseHelpers.clearPreviousDatabase(namespace)
+ return Database(namespace: namespace)
+ }
+}
+
+#endif
diff --git a/Tests/TestSQLiteEventStore.swift b/Tests/Storage/TestSQLiteEventStore.swift
similarity index 57%
rename from Tests/TestSQLiteEventStore.swift
rename to Tests/Storage/TestSQLiteEventStore.swift
index f152b7d3f..fcd114575 100644
--- a/Tests/TestSQLiteEventStore.swift
+++ b/Tests/Storage/TestSQLiteEventStore.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -11,24 +11,16 @@
// express or implied. See the Apache License Version 2.0 for the specific
// language governing permissions and limitations there under.
-#if os(iOS) || os(macOS)
+#if os(iOS) || os(macOS) || os(visionOS)
import XCTest
@testable import SnowplowTracker
class TestSQLiteEventStore: XCTestCase {
- override func setUp() {
- _ = SQLiteEventStore.removeUnsentEventsExcept(forNamespaces: [])
- }
-
- func testInit() {
- let eventStore = SQLiteEventStore(namespace: "aNamespace")
- XCTAssertNotNil(eventStore)
- }
func testInsertPayload() {
- let eventStore = SQLiteEventStore(namespace: "aNamespace")
- _ = eventStore.removeAllEvents()
+ let eventStore = createEventStore("aNamespace")
+ removeAllEvents(eventStore)
// Build an event
let payload = Payload()
@@ -38,20 +30,20 @@ class TestSQLiteEventStore: XCTestCase {
payload.addValueToPayload("MEEEE", forKey: "refr")
// Insert an event
- _ = eventStore.insertEvent(payload)
+ addEvent(payload, eventStore)
- XCTAssertEqual(eventStore.count(), 1)
- XCTAssertEqual(eventStore.getEventWithId(1)?.payload.dictionary as! [String : String],
+ XCTAssertEqual(count(eventStore), 1)
+ let emittableEvents = emittableEvents(withQueryLimit: 10, eventStore)
+ XCTAssertEqual(emittableEvents.first?.payload.dictionary as! [String : String],
payload.dictionary as! [String : String])
- XCTAssertEqual(eventStore.getLastInsertedRowId(), 1)
- _ = eventStore.removeEvent(withId: 1)
+ removeEvent(withId: emittableEvents.first?.storeId ?? 0, eventStore)
- XCTAssertEqual(eventStore.count(), 0)
+ XCTAssertEqual(count(eventStore), 0)
}
func testInsertManyPayloads() {
- let eventStore = SQLiteEventStore(namespace: "aNamespace")
- _ = eventStore.removeAllEvents()
+ let eventStore = createEventStore("aNamespace")
+ removeAllEvents(eventStore)
// Build an event
let payload = Payload()
@@ -61,43 +53,29 @@ class TestSQLiteEventStore: XCTestCase {
payload.addValueToPayload("MEEEE", forKey: "refr")
for _ in 0..<250 {
- _ = eventStore.insertEvent(payload)
+ addEvent(payload, eventStore)
}
-
- XCTAssertEqual(eventStore.count(), 250)
- XCTAssertEqual(eventStore.getAllEventsLimited(600)?.count, 250)
- XCTAssertEqual(eventStore.getAllEventsLimited(150)?.count, 150)
- XCTAssertEqual(eventStore.getAllEvents()?.count, 250)
-
- _ = eventStore.removeAllEvents()
- XCTAssertEqual(eventStore.count(), 0)
+
+ XCTAssertEqual(count(eventStore), 250)
+ XCTAssertEqual(emittableEvents(withQueryLimit: 600, eventStore).count, 250)
+ XCTAssertEqual(emittableEvents(withQueryLimit: 150, eventStore).count, 150)
+
+ removeAllEvents(eventStore)
+ XCTAssertEqual(count(eventStore), 0)
}
func testSQLiteEventStoreCreateSQLiteFile() {
- _ = SQLiteEventStore(namespace: "aNamespace")
+ let eventStore = createEventStore("aNamespace")
+
let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0]
let snowplowDirPath = URL(fileURLWithPath: libraryPath).appendingPathComponent("snowplow").path
let dbPath = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent("snowplowEvents-aNamespace.sqlite").path
XCTAssertTrue(FileManager.default.fileExists(atPath: dbPath))
}
- func testSQLiteEventStoreRemoveFiles() {
- _ = SQLiteEventStore(namespace: "aNamespace1")
- _ = SQLiteEventStore(namespace: "aNamespace2")
- _ = SQLiteEventStore(namespace: "aNamespace3")
- let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0]
- let snowplowDirPath = URL(fileURLWithPath: libraryPath).appendingPathComponent("snowplow").path
- _ = SQLiteEventStore.removeUnsentEventsExcept(forNamespaces: ["aNamespace2"])
- var dbPath = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent("snowplowEvents-aNamespace1.sqlite").path
- XCTAssertFalse(FileManager.default.fileExists(atPath: dbPath))
- dbPath = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent("snowplowEvents-aNamespace2.sqlite").path
- XCTAssertTrue(FileManager.default.fileExists(atPath: dbPath))
- dbPath = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent("snowplowEvents-aNamespace3.sqlite").path
- XCTAssertFalse(FileManager.default.fileExists(atPath: dbPath))
- }
-
func testSQLiteEventStoreInvalidNamespaceConversion() {
- _ = SQLiteEventStore(namespace: "namespace*.^?1ò2@")
+ let eventStore = createEventStore("namespace*.^?1ò2@")
+
let libraryPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).map(\.path)[0]
let snowplowDirPath = URL(fileURLWithPath: libraryPath).appendingPathComponent("snowplow").path
let dbPath = URL(fileURLWithPath: snowplowDirPath).appendingPathComponent("snowplowEvents-namespace-1-2-.sqlite").path
@@ -105,11 +83,11 @@ class TestSQLiteEventStore: XCTestCase {
}
func testMigrationFromLegacyToNamespacedEventStore() {
- var eventStore = SQLiteEventStore(namespace: "aNamespace")
- eventStore.addEvent(Payload(dictionary: [
+ var eventStore = self.createEventStore("aNamespace")
+ 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]
@@ -123,28 +101,53 @@ class TestSQLiteEventStore: XCTestCase {
XCTAssertFalse(FileManager.default.fileExists(atPath: newDbPath))
// Migrate database when SQLiteEventStore is launched the first time
- eventStore = SQLiteEventStore(namespace: "aNewNamespace")
+ eventStore = createEventStore("aNewNamespace")
+ 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))
- XCTAssertEqual(1, eventStore.count())
- for event in eventStore.getAllEvents() ?? [] {
+ for event in emittableEvents(withQueryLimit: 100, eventStore) {
XCTAssertEqual("value", event.payload.dictionary["key"] as? String)
}
}
func testMultipleAccessToSameSQLiteFile() {
- let eventStore1 = SQLiteEventStore(namespace: "aNamespace")
- eventStore1.addEvent(Payload(dictionary: [
+ let eventStore1 = createEventStore("aNamespace")
+ 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) }
}
}
diff --git a/Tests/TestDataPersistence.swift b/Tests/TestDataPersistence.swift
index 81e088f77..5ccd0b2e3 100644
--- a/Tests/TestDataPersistence.swift
+++ b/Tests/TestDataPersistence.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Legacy Tests/LegacyTestEmitter.swift b/Tests/TestEmitter.swift
similarity index 62%
rename from Tests/Legacy Tests/LegacyTestEmitter.swift
rename to Tests/TestEmitter.swift
index 255372efc..501af50bc 100644
--- a/Tests/Legacy Tests/LegacyTestEmitter.swift
+++ b/Tests/TestEmitter.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -14,29 +14,9 @@
import XCTest
@testable import SnowplowTracker
-//class BrokenNetworkConnection: NetworkConnection {
-// func sendRequests(_ requests: [Request]) -> [RequestResult] {
-// NSException.raise("BrokenNetworkConnection", format: "Fake exception on network connection.")
-// return nil
-// }
-//
-// var urlEndpoint: URL? {
-// NSException.raise("BrokenNetworkConnection", format: "Fake exception on network connection.")
-// return nil
-// }
-//
-// var httpMethod: HttpMethodOptions {
-// NSException.raise("BrokenNetworkConnection", format: "Fake exception on network connection.")
-// return .get
-// }
-//}
-
-//#pragma clang diagnostic push
-//#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-
let TEST_SERVER_EMITTER = "www.notarealurl.com"
-class LegacyTestEmitter: XCTestCase {
+class TestEmitter: XCTestCase {
override func setUp() {
super.setUp()
Logger.logLevel = .verbose
@@ -49,13 +29,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 +54,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,61 +89,45 @@ 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
-// func testEmitEventWithBrokenNetworkConnectionDoesntFreezeStatus() {
-// let networkConnection = SPBrokenNetworkConnection()
-// let emitter = self.emitter(with: networkConnection, bufferOption: SPBufferOptionSingle)
-// emitter?.addPayload(toBuffer: generatePayloads(1)?.first)
-//
-// Thread.sleep(forTimeInterval: 1)
-//
-// XCTAssertFalse(emitter?.getSendingStatus())
-//
-// emitter?.flush()
-// }
-
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)
- }
+ Thread.sleep(forTimeInterval: 1)
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)
- }
+ Thread.sleep(forTimeInterval: 1)
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 +135,13 @@ 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)
- }
+ Thread.sleep(forTimeInterval: 1)
- XCTAssertEqual(0, emitter.dbCount)
+ XCTAssertEqual(0, dbCount(emitter))
+ XCTAssertEqual(2, networkConnection.previousResults.count)
var totEvents = 0
for results in networkConnection.previousResults {
for result in results {
@@ -183,7 +151,7 @@ class LegacyTestEmitter: XCTestCase {
}
XCTAssertEqual(2, totEvents)
- emitter.flush()
+ flush(emitter)
}
func testEmitTwoGetEventsWithNoSuccess() {
@@ -191,64 +159,58 @@ 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)
- }
+ 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)
- }
+ Thread.sleep(forTimeInterval: 1)
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 networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 500)
- let emitter = self.emitter(with: networkConnection, bufferOption: .defaultGroup)
-
let payloads = generatePayloads(15)
+
+ let networkConnection = MockNetworkConnection(requestOption: .post, statusCode: 500)
+ let emitter = self.emitter(with: networkConnection, bufferOption: .smallGroup)
+
for i in 0..<14 {
- emitter.addPayload(toBuffer: payloads[i])
+ addPayload(payloads[i], emitter)
}
- for _ in 0..<10 {
- Thread.sleep(forTimeInterval: 1)
- }
+ // wait longer than the stop sending timeout
+ Thread.sleep(forTimeInterval: 6)
- 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)
- }
+ 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 +419,22 @@ 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/TestEvents.swift b/Tests/TestEvents.swift
index c8511f2d8..6fe0e0805 100644
--- a/Tests/TestEvents.swift
+++ b/Tests/TestEvents.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -25,6 +25,27 @@ class TestEvents: XCTestCase {
XCTAssertEqual(event.trueTimestamp, testDate)
}
+ func testEntities() {
+ let event = ScreenView(name: "screen")
+ let entity1 = SelfDescribingJson(schema: "schema1", andData: [String:NSObject]())
+ let entity2 = SelfDescribingJson(schema: "schema2", andData: [String:NSObject]())
+ let entity3 = SelfDescribingJson(schema: "schema3", andData: [String:NSObject]())
+
+ event.entities.append(entity1)
+ XCTAssertEqual(1, event.entities.count)
+
+ _ = event.entities([entity2])
+ XCTAssertEqual(2, event.entities.count)
+
+ _ = event.contexts([entity3])
+ XCTAssertEqual(3, event.entities.count)
+
+ XCTAssertEqual(3, event.contexts.count)
+ XCTAssertTrue(event.entities.contains(entity1))
+ XCTAssertTrue(event.entities.contains(entity2))
+ XCTAssertTrue(event.entities.contains(entity3))
+ }
+
func testApplicationInstall() {
// Prepare ApplicationInstall event
let installEvent = SelfDescribingJson(schema: kSPApplicationInstallSchema, andDictionary: [String:NSObject]())
@@ -44,7 +65,7 @@ class TestEvents: XCTestCase {
let trackerController = Snowplow.createTracker(namespace: "namespace", network: networkConfiguration, configurations: [trackerConfiguration, emitterConfiguration])
// Track event
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -76,7 +97,7 @@ class TestEvents: XCTestCase {
let trackerController = Snowplow.createTracker(namespace: "namespace", network: networkConfiguration, configurations: [trackerConfiguration, emitterConfiguration])
// Track event
- _ = trackerController?.track(event)
+ _ = trackerController.track(event)
for _ in 0..<1 {
Thread.sleep(forTimeInterval: 1)
}
@@ -112,8 +133,8 @@ class TestEvents: XCTestCase {
let trackerController = Snowplow.createTracker(namespace: "namespace", network: networkConfiguration, configurations: [trackerConfiguration, emitterConfiguration])
// Track event
- _ = trackerController?.track(deepLink)
- let screenViewId = trackerController?.track(screenView)
+ _ = trackerController.track(deepLink)
+ let screenViewId = trackerController.track(screenView)
for _ in 0..<2 {
Thread.sleep(forTimeInterval: 1)
}
@@ -123,7 +144,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
}
}
@@ -152,7 +173,7 @@ class TestEvents: XCTestCase {
XCTAssertEqual("action", event.payload["se_ac"] as? String)
}
- func testUnstructured() {
+ func testSelfDescribing() {
var data: [String : Any] = [:]
data["level"] = 23
data["score"] = 56473
@@ -164,6 +185,22 @@ class TestEvents: XCTestCase {
XCTAssertEqual(23, event.payload["level"] as? Int)
}
+ func testSelfDescribingWithEncodableData() {
+ struct Data: Encodable {
+ var level: Int
+ var score: Int
+ }
+
+ let data = Data(level: 23, score: 56473)
+ let event = try? SelfDescribing(
+ schema: "iglu:com.acme_company/demo_ios_event/jsonschema/1-0-0",
+ data: data
+ )
+ XCTAssertNotNil(event)
+ XCTAssertEqual("iglu:com.acme_company/demo_ios_event/jsonschema/1-0-0", event?.schema)
+ XCTAssertEqual(23, event?.payload["level"] as? Int)
+ }
+
func testConsentWithdrawn() {
let event = ConsentWithdrawn()
event.name = "name"
diff --git a/Tests/TestLifecycleState.swift b/Tests/TestLifecycleState.swift
index 024e7e3c0..2b5a10d53 100644
--- a/Tests/TestLifecycleState.swift
+++ b/Tests/TestLifecycleState.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/TestLinkDecorator.swift b/Tests/TestLinkDecorator.swift
new file mode 100644
index 000000000..fab79aadb
--- /dev/null
+++ b/Tests/TestLinkDecorator.swift
@@ -0,0 +1,169 @@
+// Copyright (c) 2013-present 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 XCTest
+@testable import SnowplowTracker
+
+class TestLinkDecorator: XCTestCase {
+ let epoch = "\\d{13}"
+
+ let replacements = [".", "/", "?"]
+ func matches(for regex: String, in text: String) {
+ var regex = "^\(regex)$"
+
+ do {
+ for replacement in replacements {
+ regex = regex.replacingOccurrences(of: replacement, with: "\\" + replacement)
+ }
+ let pattern = try NSRegularExpression(pattern: regex)
+ let nsString = text as NSString
+ let results = pattern.matches(in: text, range: NSRange(location: 0, length: nsString.length))
+ let fullMatch = results.map { nsString.substring(with: $0.range)}
+ if (fullMatch.count == 0) {
+ XCTFail("URL does not match pattern:\n\(text)\n\(regex)")
+ }
+ XCTAssertEqual(fullMatch.count, 1)
+ } catch let error {
+ print("invalid regex: \(error.localizedDescription)")
+ }
+ }
+
+ func testParameterConfiguration() {
+ let tracker = getTracker()
+ let _ = tracker.track(ScreenView(name: "test"))
+
+ let link = URL(string: "https://example.com")!
+ let userId = tracker.session!.userId!
+ let sessionId = tracker.session!.sessionId!
+ let subjectUserId = tracker.subject!.userId!.toBase64()
+ let appId = tracker.appId.toBase64()
+ let platform = devicePlatformToString(tracker.devicePlatform)
+ let reason = "reason"
+ let reasonb64 = reason.toBase64()
+
+ // All false
+ matches(
+ for: "https://example.com?_sp=\(userId).\(epoch)",
+ in: tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(sessionId: false, sourceId: false))!.absoluteString
+ )
+
+ // Default
+ matches(
+ for: "https://example.com?_sp=\(userId).\(epoch).\(sessionId)..\(appId)",
+ in: tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration())!.absoluteString
+ )
+
+ matches(
+ for: "https://example.com?_sp=\(userId).\(epoch).\(sessionId).\(subjectUserId).\(appId)",
+ in: tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(subjectUserId: true))!.absoluteString
+ )
+
+ matches(
+ for: "https://example.com?_sp=\(userId).\(epoch).\(sessionId).\(subjectUserId).\(appId).\(platform)",
+ in: tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(subjectUserId: true, sourcePlatform: true))!.absoluteString
+ )
+
+ matches(
+ for: "https://example.com?_sp=\(userId).\(epoch).\(sessionId).\(subjectUserId).\(appId).\(platform).\(reasonb64)",
+ in: tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(subjectUserId: true, sourcePlatform: true, reason: reason))!.absoluteString
+ )
+
+ matches(
+ for: "https://example.com?_sp=\(userId).\(epoch).....\(reasonb64)",
+ in: tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(sessionId: false, sourceId: false, reason: reason))!.absoluteString
+ )
+ }
+
+ func testWithExistingSpQueryParameter() {
+ let tracker = getTracker()
+ let link = URL(string: "https://example.com?_sp=test")!
+
+ let result = tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(sessionId: false, sourceId: false))!
+
+ matches(for: "https://example.com?_sp=\(tracker.session!.userId!).\(epoch)", in: result.absoluteString)
+ }
+
+ func testWithOtherQueryParameters() {
+ let tracker = getTracker()
+ let link = URL(string: "https://example.com?a=a&b=b")!
+ let userId = tracker.session!.userId!
+
+ let result = tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(sessionId: false, sourceId: false))!
+
+ matches(for: "https://example.com?a=a&b=b&_sp=\(userId).\(epoch)", in: result.absoluteString)
+ }
+
+ func testExistingSpQueryParameterInMiddleOfOtherQueryParameters() {
+ let tracker = getTracker()
+ let link = URL(string: "https://example.com?a=a&_sp=test&b=b")!
+
+ let result = tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(sessionId: false, sourceId: false))!
+
+ matches(for: "https://example.com?a=a&b=b&_sp=\(tracker.session!.userId!).\(epoch)", in: result.absoluteString)
+ }
+
+ func testMissingFields() {
+ let tracker = getTrackerNoSubjectUserId()
+ let link = URL(string: "https://example.com")!
+
+ let result = tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(sessionId: true, subjectUserId: true))!
+
+ // Resulting _sp param will have nothing for:
+ // - sessionId, as an event has not been tracked
+ // - subjectUserId, as it has not been set
+ matches(
+ for: "https://example.com?_sp=\(tracker.session!.userId!).\(epoch)...\(tracker.appId.toBase64())",
+ in: result.absoluteString
+ )
+ }
+
+ func testMissingSessionUserId() {
+ let tracker = getTrackerNoSessionUserId()
+ let link = URL(string: "https://example.com")!
+
+ let result = tracker.decorateLink(link, extendedParameters: CrossDeviceParameterConfiguration(sessionId: true, subjectUserId: true))
+
+ XCTAssertNil(result)
+ }
+
+ var (emitterConfig, networkConfig, trackerConfig) = (
+ EmitterConfiguration().eventStore(MockEventStore()).bufferOption(.single),
+ NetworkConfiguration(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 200)),
+ TrackerConfiguration().installAutotracking(false).screenViewAutotracking(false).lifecycleAutotracking(false).sessionContext(true)
+ )
+
+ func getTracker() -> TrackerController {
+ let subjectConfig = SubjectConfiguration().userId("userId")
+
+ let namespace = "testEmitter" + String(describing: Int.random(in: 0..<100))
+ return Snowplow.createTracker(namespace: namespace,
+ network: networkConfig,
+ configurations: [trackerConfig, emitterConfig, subjectConfig])
+ }
+
+ private func getTrackerNoSubjectUserId() -> TrackerController {
+ let namespace = "testEmitter" + String(describing: Int.random(in: 0..<100))
+ return Snowplow.createTracker(namespace: namespace,
+ network: networkConfig,
+ configurations: [trackerConfig, emitterConfig])
+ }
+
+ private func getTrackerNoSessionUserId() -> TrackerController {
+ trackerConfig.sessionContext = false
+
+ let namespace = "testEmitter" + String(describing: Int.random(in: 0..<100))
+ return Snowplow.createTracker(namespace: namespace,
+ network: networkConfig,
+ configurations: [trackerConfig, emitterConfig])
+ }
+}
diff --git a/Tests/TestLogger.swift b/Tests/TestLogger.swift
index 94095ca32..df2ecd09a 100644
--- a/Tests/TestLogger.swift
+++ b/Tests/TestLogger.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/TestMemoryEventStore.swift b/Tests/TestMemoryEventStore.swift
index cd6868649..8fdaccd77 100644
--- a/Tests/TestMemoryEventStore.swift
+++ b/Tests/TestMemoryEventStore.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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,85 @@ 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)
+ }
+
+ func testRemoveOldEventsByAge() {
+ let eventStore = MemoryEventStore()
+
+ for (i, timeDiff) in [5.0, 4.0, 3.0, 2.0, 1.0].enumerated() {
+ let payload = Payload()
+ payload.addValueToPayload(getTimestamp(Date().timeIntervalSince1970 - timeDiff), forKey: "dtm")
+ payload.addValueToPayload(String(i + 1), forKey: "eid")
+ addEvent(payload, eventStore)
+ }
+
+ XCTAssertEqual(count(eventStore), 5)
+
+ removeOldEvents(maxSize: 5, maxAge: 3, eventStore)
+ XCTAssertEqual(count(eventStore), 2)
+
+ let events = emittableEvents(withQueryLimit: 5, eventStore)
+ XCTAssertEqual(
+ events.map { $0.payload["eid"] as! String }.sorted(),
+ ["4", "5"]
+ )
+ }
+
+ func testRemoveOldestEventsByMaxSize() {
+ let eventStore = MemoryEventStore()
+
+ for i in 0..<5 {
+ let payload = Payload()
+ payload.addValueToPayload(String(i + 1), forKey: "eid")
+ addEvent(payload, eventStore)
+ }
+
+ XCTAssertEqual(count(eventStore), 5)
+
+ removeOldEvents(maxSize: 2, maxAge: 5, eventStore)
+ XCTAssertEqual(count(eventStore), 2)
+
+ let events = emittableEvents(withQueryLimit: 5, eventStore)
+ XCTAssertEqual(
+ events.map { $0.payload["eid"] as! String }.sorted(),
+ ["4", "5"]
+ )
+ }
+
+ 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) }
+ }
+
+ private func removeOldEvents(maxSize: Int64, maxAge: TimeInterval, _ eventStore: EventStore) {
+ InternalQueue.sync { eventStore.removeOldEvents(maxSize: maxSize, maxAge: maxAge) }
+ }
+
+ private func getTimestamp(_ timeInterval: TimeInterval) -> String {
+ return String(format: "%lld", Int64(timeInterval * 1000))
}
}
diff --git a/Tests/TestNetworkConnection.swift b/Tests/TestNetworkConnection.swift
index 2ba85ba44..c153f96c9 100644
--- a/Tests/TestNetworkConnection.swift
+++ b/Tests/TestNetworkConnection.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -28,7 +28,7 @@ class TestNetworkConnection: XCTestCase {
let endpoint = "https://\(TEST_URL_ENDPOINT)/i"
Mock(url: URL(string: endpoint)!, ignoreQuery: true, dataType: .json, statusCode: 200, data: [.get: Data()]).register()
- let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .get)
+ let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .get, protocolClasses: [MockingURLProtocol.self])
let payload = Payload()
payload.addValueToPayload("value", forKey: "key")
@@ -45,7 +45,7 @@ class TestNetworkConnection: XCTestCase {
let endpoint = "https://\(TEST_URL_ENDPOINT)/i"
Mock(url: URL(string: endpoint)!, ignoreQuery: true, dataType: .json, statusCode: 404, data: [.get: Data()]).register()
- let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .get)
+ let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .get, protocolClasses: [MockingURLProtocol.self])
let payload = Payload()
payload.addValueToPayload("value", forKey: "key")
@@ -62,7 +62,7 @@ class TestNetworkConnection: XCTestCase {
let endpoint = "https://\(TEST_URL_ENDPOINT)/com.snowplowanalytics.snowplow/tp2"
Mock(url: URL(string: endpoint)!, ignoreQuery: true, dataType: .json, statusCode: 200, data: [.post: Data()]).register()
- let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post)
+ let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post, protocolClasses: [MockingURLProtocol.self])
let payload = Payload()
payload.addValueToPayload("value", forKey: "key")
@@ -79,7 +79,7 @@ class TestNetworkConnection: XCTestCase {
let endpoint = "https://\(TEST_URL_ENDPOINT)/com.snowplowanalytics.snowplow/tp2"
Mock(url: URL(string: endpoint)!, ignoreQuery: true, dataType: .json, statusCode: 404, data: [.post: Data()]).register()
- let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post)
+ let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post, protocolClasses: [MockingURLProtocol.self])
let payload = Payload()
payload.addValueToPayload("value", forKey: "key")
@@ -124,7 +124,7 @@ class TestNetworkConnection: XCTestCase {
}
mock.register()
- let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post)
+ let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post, protocolClasses: [MockingURLProtocol.self])
connection.serverAnonymisation = false
let payload = Payload()
@@ -149,7 +149,7 @@ class TestNetworkConnection: XCTestCase {
}
mock.register()
- let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post)
+ let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .post, protocolClasses: [MockingURLProtocol.self])
connection.serverAnonymisation = true
let payload = Payload()
@@ -174,7 +174,7 @@ class TestNetworkConnection: XCTestCase {
}
mock.register()
- let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .get)
+ let connection = DefaultNetworkConnection(urlString: TEST_URL_ENDPOINT, httpMethod: .get, protocolClasses: [MockingURLProtocol.self])
connection.serverAnonymisation = true
let payload = Payload()
diff --git a/Tests/TestPayload.swift b/Tests/TestPayload.swift
index d29ae7ea2..91573cbc9 100644
--- a/Tests/TestPayload.swift
+++ b/Tests/TestPayload.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/TestPlatformContext.swift b/Tests/TestPlatformContext.swift
index 22d57ec12..91f856a92 100644
--- a/Tests/TestPlatformContext.swift
+++ b/Tests/TestPlatformContext.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -17,23 +17,24 @@ import XCTest
class TestPlatformContext: XCTestCase {
func testContainsPlatformInfo() {
let context = PlatformContext(deviceInfoMonitor: MockDeviceInfoMonitor())
- let platformDict = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil).dictionary
+ let platformDict = context.fetchPlatformDict(userAnonymisation: false).dictionary
XCTAssertNotNil(platformDict)
XCTAssertNotNil(platformDict)
}
func testContainsMobileInfo() {
let context = PlatformContext(deviceInfoMonitor: MockDeviceInfoMonitor())
- let platformDict = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil).dictionary
+ let platformDict = context.fetchPlatformDict(userAnonymisation: false).dictionary
XCTAssertNotNil(platformDict)
XCTAssertNotNil(platformDict)
}
func testAddsAllMockedInfo() {
let deviceInfoMonitor = MockDeviceInfoMonitor()
- let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
let idfa = UUID()
- let platformDict = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: { idfa })
+ let retriever = PlatformContextRetriever(appleIdfa: { idfa })
+ let context = PlatformContext(platformContextRetriever: retriever, mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
+ let platformDict = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(idfa.uuidString, platformDict[kSPMobileAppleIdfa] as? String)
XCTAssertEqual("Apple Inc.", platformDict[kSPPlatformDeviceManu] as? String)
XCTAssertEqual("deviceModel", platformDict[kSPPlatformDeviceModel] as? String)
@@ -59,10 +60,10 @@ class TestPlatformContext: XCTestCase {
let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(2, deviceInfoMonitor.accessCount("batteryLevel"))
XCTAssertEqual(2, deviceInfoMonitor.accessCount("appAvailableMemory"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(3, deviceInfoMonitor.accessCount("batteryLevel"))
XCTAssertEqual(3, deviceInfoMonitor.accessCount("appAvailableMemory"))
}
@@ -72,10 +73,10 @@ class TestPlatformContext: XCTestCase {
let context = PlatformContext(mobileDictUpdateFrequency: 1000, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory"))
}
@@ -85,10 +86,10 @@ class TestPlatformContext: XCTestCase {
let context = PlatformContext(mobileDictUpdateFrequency: 1, networkDictUpdateFrequency: 0, deviceInfoMonitor: deviceInfoMonitor)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(2, deviceInfoMonitor.accessCount("networkTechnology"))
XCTAssertEqual(2, deviceInfoMonitor.accessCount("networkType"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(3, deviceInfoMonitor.accessCount("networkTechnology"))
XCTAssertEqual(3, deviceInfoMonitor.accessCount("networkType"))
}
@@ -98,10 +99,10 @@ class TestPlatformContext: XCTestCase {
let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1000, deviceInfoMonitor: deviceInfoMonitor)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology"))
XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType"))
}
@@ -110,20 +111,20 @@ class TestPlatformContext: XCTestCase {
let deviceInfoMonitor = MockDeviceInfoMonitor()
let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 0, deviceInfoMonitor: deviceInfoMonitor)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("physicalMemory"))
- XCTAssertEqual(1, deviceInfoMonitor.accessCount("totalStorage"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ XCTAssertEqual(1, deviceInfoMonitor.accessCount("carrierName"))
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("physicalMemory"))
- XCTAssertEqual(1, deviceInfoMonitor.accessCount("totalStorage"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ XCTAssertEqual(1, deviceInfoMonitor.accessCount("carrierName"))
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("physicalMemory"))
- XCTAssertEqual(1, deviceInfoMonitor.accessCount("totalStorage"))
+ XCTAssertEqual(1, deviceInfoMonitor.accessCount("carrierName"))
}
func testDoesntUpdateIdfvIfNotNil() {
let deviceInfoMonitor = MockDeviceInfoMonitor()
let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("appleIdfv"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("appleIdfv"))
}
@@ -132,53 +133,61 @@ class TestPlatformContext: XCTestCase {
deviceInfoMonitor.customAppleIdfv = nil
let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
XCTAssertEqual(1, deviceInfoMonitor.accessCount("appleIdfv"))
- _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ _ = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(2, deviceInfoMonitor.accessCount("appleIdfv"))
}
func testUpdatesIdfaIfNil() {
let deviceInfoMonitor = MockDeviceInfoMonitor()
- let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
-
- let platformDict1 = context.fetchPlatformDict(
- userAnonymisation: false,
- advertisingIdentifierRetriever: { nil }
+ var idfa: UUID? = nil
+ let retriever = PlatformContextRetriever(appleIdfa: { idfa })
+ let context = PlatformContext(
+ platformContextRetriever: retriever,
+ mobileDictUpdateFrequency: 0,
+ networkDictUpdateFrequency: 1,
+ deviceInfoMonitor: deviceInfoMonitor
)
+
+ let platformDict1 = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertNil(platformDict1[kSPMobileAppleIdfa])
- let idfa = UUID()
- let platformDict2 = context.fetchPlatformDict(
- userAnonymisation: false,
- advertisingIdentifierRetriever: { idfa }
- )
- XCTAssertEqual(idfa.uuidString, platformDict2[kSPMobileAppleIdfa] as? String)
+ idfa = UUID()
+ let platformDict2 = context.fetchPlatformDict(userAnonymisation: false)
+ XCTAssertEqual(idfa?.uuidString, platformDict2[kSPMobileAppleIdfa] as? String)
}
func testDoesntUpdateIdfaIfAlreadyRetrieved() {
let deviceInfoMonitor = MockDeviceInfoMonitor()
- let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
let idfa1 = UUID()
- let platformDict1 = context.fetchPlatformDict(
- userAnonymisation: false,
- advertisingIdentifierRetriever: { idfa1 }
+ var idfa = idfa1
+
+ let retriever = PlatformContextRetriever(appleIdfa: { idfa })
+ let context = PlatformContext(
+ platformContextRetriever: retriever,
+ mobileDictUpdateFrequency: 0,
+ networkDictUpdateFrequency: 1,
+ deviceInfoMonitor: deviceInfoMonitor
)
+
+ let platformDict1 = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(idfa1.uuidString, platformDict1[kSPMobileAppleIdfa] as? String)
- let platformDict2 = context.fetchPlatformDict(
- userAnonymisation: false,
- advertisingIdentifierRetriever: { UUID() }
- )
+ idfa = UUID()
+ let platformDict2 = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertEqual(idfa1.uuidString, platformDict2[kSPMobileAppleIdfa] as? String)
}
func testAnonymisesUserIdentifiers() {
let deviceInfoMonitor = MockDeviceInfoMonitor()
- let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
- let platformDict = context.fetchPlatformDict(
- userAnonymisation: true,
- advertisingIdentifierRetriever: { UUID() }
+ let retriever = PlatformContextRetriever(appleIdfa: { UUID() })
+ let context = PlatformContext(
+ platformContextRetriever: retriever,
+ mobileDictUpdateFrequency: 0,
+ networkDictUpdateFrequency: 1,
+ deviceInfoMonitor: deviceInfoMonitor
)
+ let platformDict = context.fetchPlatformDict(userAnonymisation: true)
XCTAssertNil(platformDict[kSPMobileAppleIdfa])
XCTAssertNil(platformDict[kSPMobileAppleIdfv])
}
@@ -187,25 +196,21 @@ class TestPlatformContext: XCTestCase {
let deviceInfoMonitor = MockDeviceInfoMonitor()
deviceInfoMonitor.language = "1234567890"
let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor)
- let platformDict = context.fetchPlatformDict(
- userAnonymisation: true,
- advertisingIdentifierRetriever: { UUID() }
- )
+ let platformDict = context.fetchPlatformDict(userAnonymisation: true)
XCTAssertEqual("12345678", platformDict[kSPMobileLanguage] as? String)
}
#endif
func testOnlyAddsRequestedProperties() {
let deviceInfoMonitor = MockDeviceInfoMonitor()
+ let retriever = PlatformContextRetriever(appleIdfa: { UUID() })
let context = PlatformContext(
- platformContextProperties: [.appAvailableMemory, .availableStorage, .language],
+ platformContextProperties: [.appAvailableMemory, .language],
+ platformContextRetriever: retriever,
mobileDictUpdateFrequency: 0,
networkDictUpdateFrequency: 1,
deviceInfoMonitor: deviceInfoMonitor)
- let platformDict = context.fetchPlatformDict(
- userAnonymisation: false,
- advertisingIdentifierRetriever: { UUID() }
- )
+ let platformDict = context.fetchPlatformDict(userAnonymisation: false)
XCTAssertNotNil(platformDict[kSPPlatformDeviceManu])
#if os(iOS)
@@ -213,10 +218,64 @@ class TestPlatformContext: XCTestCase {
XCTAssertNil(platformDict[kSPMobileScale])
XCTAssertNil(platformDict[kSPMobileResolution])
XCTAssertNotNil(platformDict[kSPMobileAppAvailableMemory])
- XCTAssertNotNil(platformDict[kSPMobileAvailableStorage])
XCTAssertNil(platformDict[kSPMobilePhysicalMemory])
XCTAssertNil(platformDict[kSPMobileIsPortrait])
XCTAssertNil(platformDict[kSPMobileAppleIdfa])
+#endif
+ }
+
+ func testPlatformContextRetrieverOverridesProperties() {
+ let deviceInfoMonitor = MockDeviceInfoMonitor()
+ let idfa = UUID()
+ let retriever = PlatformContextRetriever(
+ osType: { "r1" },
+ osVersion: { "r2" },
+ deviceVendor: { "r3" },
+ deviceModel: { "r4" },
+ carrier: { "r5" },
+ networkType: { "r6" },
+ networkTechnology: { "r7" },
+ appleIdfa: { idfa },
+ appleIdfv: { "r9" },
+ availableStorage: { 100 },
+ totalStorage: { 101 },
+ physicalMemory: { 102 },
+ appAvailableMemory: { 103 },
+ batteryLevel: { 104 },
+ batteryState: { "r10" },
+ lowPowerMode: { true },
+ isPortrait: { false },
+ resolution: { "r11" },
+ scale: { 105 },
+ language: { "r12" }
+ )
+ let context = PlatformContext(
+ platformContextRetriever: retriever,
+ deviceInfoMonitor: deviceInfoMonitor)
+ let platformDict = context.fetchPlatformDict(userAnonymisation: false)
+
+ XCTAssertEqual(platformDict[kSPPlatformOsType] as? String, "r1")
+ XCTAssertEqual(platformDict[kSPPlatformOsVersion] as? String, "r2")
+ XCTAssertEqual(platformDict[kSPPlatformDeviceManu] as? String, "r3")
+ XCTAssertEqual(platformDict[kSPPlatformDeviceModel] as? String, "r4")
+
+#if os(iOS) || os(visionOS)
+ XCTAssertEqual(platformDict[kSPMobileCarrier] as? String, "r5")
+ XCTAssertEqual(platformDict[kSPMobileNetworkType] as? String, "r6")
+ XCTAssertEqual(platformDict[kSPMobileNetworkTech] as? String, "r7")
+ XCTAssertEqual(platformDict[kSPMobileAppleIdfa] as? String, idfa.uuidString)
+ XCTAssertEqual(platformDict[kSPMobileAppleIdfv] as? String, "r9")
+ XCTAssertEqual(platformDict[kSPMobileAvailableStorage] as? Int64, 100)
+ XCTAssertEqual(platformDict[kSPMobileTotalStorage] as? Int64, 101)
+ XCTAssertEqual(platformDict[kSPMobilePhysicalMemory] as? UInt64, 102)
+ XCTAssertEqual(platformDict[kSPMobileAppAvailableMemory] as? Int, 103)
+ XCTAssertEqual(platformDict[kSPMobileBatteryLevel] as? Int, 104)
+ XCTAssertEqual(platformDict[kSPMobileBatteryState] as? String, "r10")
+ XCTAssertEqual(platformDict[kSPMobileLowPowerMode] as? Bool, true)
+ XCTAssertEqual(platformDict[kSPMobileIsPortrait] as? Bool, false)
+ XCTAssertEqual(platformDict[kSPMobileResolution] as? String, "r11")
+ XCTAssertEqual(platformDict[kSPMobileScale] as? Double, 105)
+ XCTAssertEqual(platformDict[kSPMobileLanguage] as? String, "r12")
#endif
}
}
diff --git a/Tests/TestPlugins.swift b/Tests/TestPlugins.swift
index b69b2658b..a554ec8d2 100644
--- a/Tests/TestPlugins.swift
+++ b/Tests/TestPlugins.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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: [:]))
@@ -205,7 +202,7 @@ class TestPlugins: XCTestCase {
let namespace = "testPlugins" + String(describing: Int.random(in: 0..<100))
return Snowplow.createTracker(namespace: namespace,
network: networkConfig,
- configurations: configurations + [trackerConfig])!
+ configurations: configurations + [trackerConfig])
}
private func waitForEventsToBeTracked() {
diff --git a/Tests/TestRequest.swift b/Tests/TestRequest.swift
index 0d3e9c225..23ee0eccd 100644
--- a/Tests/TestRequest.swift
+++ b/Tests/TestRequest.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/TestRequestResult.swift b/Tests/TestRequestResult.swift
index 0d41e29d7..184ac7904 100644
--- a/Tests/TestRequestResult.swift
+++ b/Tests/TestRequestResult.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/TestSelfDescribingJson.swift b/Tests/TestSelfDescribingJson.swift
index 178084861..15e4febf3 100644
--- a/Tests/TestSelfDescribingJson.swift
+++ b/Tests/TestSelfDescribingJson.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -78,7 +78,40 @@ class TestSelfDescribingJson: XCTestCase {
XCTAssertEqual(NSDictionary(dictionary: expected),
NSDictionary(dictionary: sdj.dictionary))
}
-
+
+ func testInitWithEncodable() {
+ struct EncodableUserData: Encodable {
+ var firstName: String
+ var lastName: String
+ var nickname: String?
+ var age: Decimal
+ var children: [EncodableUserData]?
+ }
+
+ let user = EncodableUserData(
+ firstName: "John",
+ lastName: "Doe",
+ age: 32.5,
+ children: [
+ EncodableUserData(firstName: "Emily", lastName: "Doe", age: 1.2)
+ ]
+ )
+
+ let json = try? SelfDescribingJson(schema: "iglu:acme.com/user/jsonschema/1-0-0", andEncodable: user)
+ XCTAssertNotNil(json)
+ XCTAssertEqual(json?.data["firstName"] as? String, "John")
+ XCTAssertEqual(json?.data["lastName"] as? String, "Doe")
+ XCTAssertFalse(json?.data.keys.contains("nickname") ?? false)
+ XCTAssertNotNil(json?.data["children"])
+ XCTAssertEqual((json?.data["children"] as? Array)?.count, 1)
+ XCTAssertEqual(json?.data["age"] as? Double, 32.5)
+ let children = json?.data["children"] as? Array>
+ XCTAssertEqual(children?.count, 1)
+ XCTAssertEqual(children?[0]["firstName"] as? String, "Emily")
+ XCTAssertEqual(children?[0]["lastName"] as? String, "Doe")
+ XCTAssertEqual(children?[0]["age"] as? Double, 1.2)
+ }
+
func testUpdateSchema() {
let expected: [String : Any] = [
"schema": "iglu:acme.com/test_event_2/jsonschema/1-0-0",
@@ -161,4 +194,3 @@ class TestSelfDescribingJson: XCTestCase {
NSDictionary(dictionary: sdj.dictionary))
}
}
-
diff --git a/Tests/TestServiceProvider.swift b/Tests/TestServiceProvider.swift
index 26617a807..d80f56fd3 100644
--- a/Tests/TestServiceProvider.swift
+++ b/Tests/TestServiceProvider.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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 7a89cfcd7..e3b2afe76 100644
--- a/Tests/TestSession.swift
+++ b/Tests/TestSession.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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,7 +126,7 @@ class TestSession: XCTestCase {
func testBackgroundEventsOnSameSession() {
cleanFile(withNamespace: "t1")
- let emitter = Emitter(urlEndpoint: "") { emitter 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
@@ -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,7 +269,7 @@ class TestSession: XCTestCase {
func testBackgroundTimeBiggerThanBackgroundTimeoutCausesNewSession() {
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 = true
tracker.sessionContext = true
@@ -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,7 +301,7 @@ class TestSession: XCTestCase {
func testBackgroundTimeSmallerThanBackgroundTimeoutDoesntCauseNewSession() {
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 = true
tracker.sessionContext = true
@@ -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,48 +347,49 @@ class TestSession: XCTestCase {
cleanFile(withNamespace: "tracker1")
cleanFile(withNamespace: "tracker2")
- let emitter = Emitter(urlEndpoint: "") { emitter in}
- let tracker1 = Tracker(trackerNamespace: "tracker1", appId: nil, emitter: emitter) { 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) { 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) { 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)
@@ -397,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 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)
@@ -434,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
@@ -467,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..c359a6828 100644
--- a/Tests/TestStateManager.swift
+++ b/Tests/TestStateManager.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -35,6 +35,14 @@ class MockStateMachine: StateMachineProtocol {
self.identifier = identifier
}
+ var subscribedEventSchemasForEventsBefore: [String] {
+ return []
+ }
+
+ func eventsBefore(event: SnowplowTracker.Event) -> [SnowplowTracker.Event]? {
+ return nil
+ }
+
var subscribedEventSchemasForTransitions: [String] {
return ["inc", "dec"]
}
@@ -244,22 +252,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/TestSubject.swift b/Tests/TestSubject.swift
index 36e163f14..e11d6302d 100644
--- a/Tests/TestSubject.swift
+++ b/Tests/TestSubject.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -17,14 +17,14 @@ import XCTest
class TestSubject: XCTestCase {
func testReturnsPlatformContextIfEnabled() {
let subject = Subject(platformContext: true, geoLocationContext: false)
- let platformDict = subject.platformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ let platformDict = subject.platformDict(userAnonymisation: false)
XCTAssertNotNil(platformDict)
XCTAssertNotNil(platformDict?.dictionary[kSPPlatformOsType])
}
func testDoesntReturnPlatformContextIfDisabled() {
let subject = Subject(platformContext: false, geoLocationContext: false)
- let platformDict = subject.platformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)
+ let platformDict = subject.platformDict(userAnonymisation: false)
XCTAssertNil(platformDict)
}
diff --git a/Tests/TestUtils.swift b/Tests/TestUtils.swift
index 601d75a02..cd87b6c69 100644
--- a/Tests/TestUtils.swift
+++ b/Tests/TestUtils.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -34,16 +34,20 @@ class TestUtils: XCTestCase {
}
func testGetPlatform() {
-#if os(iOS)
+#if os(iOS) || os(visionOS) || os(watchOS)
XCTAssertEqual(Utilities.platform, .mobile)
+#elseif os(tvOS)
+ XCTAssertEqual(Utilities.platform, .connectedTV)
#else
XCTAssertEqual(Utilities.platform, .desktop)
#endif
}
func testGetResolution() {
+ #if !os(visionOS)
let actualResolution = Utilities.resolution
XCTAssertTrue(actualResolution != nil)
+ #endif
}
func testGetEventId() {
diff --git a/Tests/TestWebViewMessageHandler.swift b/Tests/TestWebViewMessageHandler.swift
index a71aa39d6..a2ebe5e76 100644
--- a/Tests/TestWebViewMessageHandler.swift
+++ b/Tests/TestWebViewMessageHandler.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -11,7 +11,7 @@
// express or implied. See the Apache License Version 2.0 for the specific
// language governing permissions and limitations there under.
-#if os(iOS) || os(macOS)
+#if os(iOS) || os(macOS) || os(visionOS)
import XCTest
@testable import SnowplowTracker
diff --git a/Tests/Legacy Tests/LegacyTestTracker.swift b/Tests/Tracker/TestTracker.swift
similarity index 75%
rename from Tests/Legacy Tests/LegacyTestTracker.swift
rename to Tests/Tracker/TestTracker.swift
index 695db9db5..9153997f2 100644
--- a/Tests/Legacy Tests/LegacyTestTracker.swift
+++ b/Tests/Tracker/TestTracker.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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/Tracker/TestTrackerEvent.swift b/Tests/Tracker/TestTrackerEvent.swift
new file mode 100644
index 000000000..c24bc76d4
--- /dev/null
+++ b/Tests/Tracker/TestTrackerEvent.swift
@@ -0,0 +1,31 @@
+// Copyright (c) 2013-present 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 XCTest
+@testable import SnowplowTracker
+
+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)
+ }
+
+}
diff --git a/Tests/Utils/DatabaseHelpers.swift b/Tests/Utils/DatabaseHelpers.swift
new file mode 100644
index 000000000..bc9cb066f
--- /dev/null
+++ b/Tests/Utils/DatabaseHelpers.swift
@@ -0,0 +1,28 @@
+// Copyright (c) 2013-present 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.
+
+#if os(iOS) || os(macOS) || os(visionOS)
+
+import Foundation
+@testable import SnowplowTracker
+
+class DatabaseHelpers {
+ static func clearPreviousDatabase(_ namespace: String) {
+ let path = Database.dbPath(namespace: namespace)
+ if FileManager.default.fileExists(atPath: path) {
+ try? FileManager.default.removeItem(atPath: path)
+ }
+ }
+}
+
+#endif
diff --git a/Tests/Utils/EventSink.swift b/Tests/Utils/EventSink.swift
new file mode 100644
index 000000000..b5195657c
--- /dev/null
+++ b/Tests/Utils/EventSink.swift
@@ -0,0 +1,41 @@
+// Copyright (c) 2013-present 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/MockDeviceInfoMonitor.swift b/Tests/Utils/MockDeviceInfoMonitor.swift
index c3a56706c..fdf0177eb 100644
--- a/Tests/Utils/MockDeviceInfoMonitor.swift
+++ b/Tests/Utils/MockDeviceInfoMonitor.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -23,22 +23,22 @@ class MockDeviceInfoMonitor: DeviceInfoMonitor {
return customAppleIdfv
}
- override var deviceVendor: String? {
+ override var deviceVendor: String {
increaseMethodAccessCount("deviceVendor")
return "Apple Inc."
}
- override var deviceModel: String? {
+ override var deviceModel: String {
increaseMethodAccessCount("deviceModel")
return "deviceModel"
}
- override var osVersion: String? {
+ override var osVersion: String {
increaseMethodAccessCount("osVersion")
return "13.0.0"
}
- override var osType: String? {
+ override var osType: String {
increaseMethodAccessCount("osType")
return "ios"
}
@@ -86,16 +86,6 @@ class MockDeviceInfoMonitor: DeviceInfoMonitor {
increaseMethodAccessCount("appAvailableMemory")
return 1000
}
-
- override var availableStorage: Int64? {
- increaseMethodAccessCount("availableStorage")
- return 9000
- }
-
- override var totalStorage: Int? {
- increaseMethodAccessCount("totalStorage")
- return 900000
- }
override var isPortrait: Bool? {
return true
diff --git a/Tests/Utils/MockEventStore.swift b/Tests/Utils/MockEventStore.swift
index 9c5053e60..cfd20b762 100644
--- a/Tests/Utils/MockEventStore.swift
+++ b/Tests/Utils/MockEventStore.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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 {
@@ -79,4 +69,8 @@ class MockEventStore: NSObject, EventStore {
logVerbose(message: "emittableEventsWithQueryLimit: \(eventIds)")
return events
}
+
+ func removeOldEvents(maxSize: Int64, maxAge: TimeInterval) {
+ // Not implemented in the mock event store.
+ }
}
diff --git a/Tests/Utils/MockLoggerDelegate.swift b/Tests/Utils/MockLoggerDelegate.swift
index 5e7463dc1..9398c1849 100644
--- a/Tests/Utils/MockLoggerDelegate.swift
+++ b/Tests/Utils/MockLoggerDelegate.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
diff --git a/Tests/Utils/MockNetworkConnection.swift b/Tests/Utils/MockNetworkConnection.swift
index 220cd5608..78df9c03f 100644
--- a/Tests/Utils/MockNetworkConnection.swift
+++ b/Tests/Utils/MockNetworkConnection.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -48,4 +48,9 @@ class MockNetworkConnection: NSObject, NetworkConnection {
previousResults.append(requestResults)
return requestResults
}
+
+ func clear() {
+ previousRequests = []
+ previousResults = []
+ }
}
diff --git a/Tests/Utils/MockTimer.swift b/Tests/Utils/MockTimer.swift
index 47da12c45..037893b00 100644
--- a/Tests/Utils/MockTimer.swift
+++ b/Tests/Utils/MockTimer.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -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
diff --git a/Tests/Utils/MockWKScriptMessage.swift b/Tests/Utils/MockWKScriptMessage.swift
index 9ba06d742..cb34b160d 100644
--- a/Tests/Utils/MockWKScriptMessage.swift
+++ b/Tests/Utils/MockWKScriptMessage.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -11,7 +11,7 @@
// express or implied. See the Apache License Version 2.0 for the specific
// language governing permissions and limitations there under.
-#if os(iOS) || os(macOS)
+#if os(iOS) || os(macOS) || os(visionOS)
import WebKit
class MockWKScriptMessage: WKScriptMessage {
diff --git a/Tests/Utils/TimeTraveler.swift b/Tests/Utils/TimeTraveler.swift
index 30fcac2bf..1589007b0 100644
--- a/Tests/Utils/TimeTraveler.swift
+++ b/Tests/Utils/TimeTraveler.swift
@@ -1,4 +1,4 @@
-// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved.
+// Copyright (c) 2013-present 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
@@ -23,4 +23,8 @@ class TimeTraveler {
func generateDate() -> Date {
return date
}
+
+ func generateTimeInterval() -> TimeInterval {
+ return date.timeIntervalSince1970
+ }
}
diff --git a/Tests/VisionOS/TestImmersiveSpaceState.swift b/Tests/VisionOS/TestImmersiveSpaceState.swift
new file mode 100644
index 000000000..2b14b3122
--- /dev/null
+++ b/Tests/VisionOS/TestImmersiveSpaceState.swift
@@ -0,0 +1,221 @@
+// Copyright (c) 2013-present 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 XCTest
+@testable import SnowplowTracker
+
+class TestImmersiveSpaceState: XCTestCase {
+ override func setUp() {
+ super.setUp()
+ }
+
+ override func tearDown() {
+ super.tearDown()
+ }
+
+ func testImmersiveSpaceStateMachine() {
+ let eventStore = MockEventStore()
+ 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.immersiveSpaceContext = true
+ }
+
+ // Send events
+
+ // no entity before OpenImmersiveSpaceEvent
+ track(Timing(category: "category", variable: "variable", timing: 123), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ var payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ var entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertFalse(entities!.contains("immersive_space"))
+
+ // OpenImmersiveSpaceEvent has the entity
+ track(OpenImmersiveSpaceEvent(id: "original_space_state"), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertTrue(entities!.contains("original_space_state"))
+
+ // as do subsequent events
+ track(Timing(category: "category", variable: "variable", timing: 123), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertTrue(entities!.contains("original_space_state"))
+
+ // tracking another OpenImmersiveSpaceEvent updates the state
+ track(OpenImmersiveSpaceEvent(id: "second_space"), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertFalse(entities!.contains("original_space_state"))
+ XCTAssertTrue(entities!.contains("second_space"))
+
+ // subsequent events have the new entity
+ track(Timing(category: "category", variable: "variable", timing: 123), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertTrue(entities!.contains("second_space"))
+
+ // the entity is also attached to the DismissImmersiveSpaceEvent
+ track(DismissImmersiveSpaceEvent(), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertTrue(entities!.contains("immersive_space"))
+ XCTAssertTrue(entities!.contains("second_space"))
+
+ // events following the dismiss event do not have the entity
+ // including other dismiss events
+ track(DismissImmersiveSpaceEvent(), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertFalse(entities!.contains("immersive_space"))
+
+ track(Foreground(index: 1), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertFalse(entities!.contains("immersive_space"))
+
+ // can start adding the entity again if open event is tracked
+ track(OpenImmersiveSpaceEvent(id: "a_new_space"), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertTrue(entities!.contains("immersive_space"))
+ XCTAssertFalse(entities!.contains("second_space"))
+ XCTAssertTrue(entities!.contains("a_new_space"))
+ }
+
+ func testEntityNotConfigured() {
+ let eventStore = MockEventStore()
+ 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.immersiveSpaceContext = false
+ }
+
+ // Send events
+
+ // no entity before OpenImmersiveSpaceEvent
+ track(Timing(category: "category", variable: "variable", timing: 123), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ var payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ var entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertFalse(entities!.contains("immersive_space"))
+
+ // OpenImmersiveSpaceEvent has the entity
+ track(OpenImmersiveSpaceEvent(id: "original space state"), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertTrue(entities!.contains("immersive_space"))
+
+ // other events do not
+ track(Timing(category: "category", variable: "variable", timing: 123), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertFalse(entities!.contains("immersive_space"))
+
+ // no entity for DismissImmersiveSpaceEvent
+ track(DismissImmersiveSpaceEvent(), tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertFalse(entities!.contains("immersive_space"))
+
+ // can add it manually
+ let event = Foreground(index: 1)
+ event.entities.append(ImmersiveSpaceEntity(id: "space"))
+ track(event, tracker)
+ if eventStore.lastInsertedRow == -1 {
+ XCTFail()
+ }
+ payload = eventStore.db[Int64(eventStore.lastInsertedRow)]
+ _ = eventStore.removeAllEvents()
+ entities = (payload?["co"]) as? String
+ XCTAssertNotNil(entities)
+ XCTAssertTrue(entities!.contains("immersive_space"))
+ }
+
+ private func track(_ event: Event, _ tracker: Tracker) {
+ InternalQueue.sync {
+ _ = tracker.track(event)
+ }
+ }
+}
diff --git a/Tests/VisionOS/TestVisionOSEntities.swift b/Tests/VisionOS/TestVisionOSEntities.swift
new file mode 100644
index 000000000..4c01b67ea
--- /dev/null
+++ b/Tests/VisionOS/TestVisionOSEntities.swift
@@ -0,0 +1,51 @@
+// Copyright (c) 2013-present 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 XCTest
+@testable import SnowplowTracker
+
+class TestVisionOSEntities: XCTestCase {
+ let uuid = UUID()
+
+ func testBuildsImmersiveSpaceEntity() {
+ let space = ImmersiveSpaceEntity(
+ id: "space_123",
+ viewId: uuid,
+ immersionStyle: ImmersionStyle.automatic,
+ upperLimbVisibility: UpperLimbVisibility.visible
+ )
+ let entity = space.data
+
+ XCTAssertEqual(swiftuiImmersiveSpaceSchema, space.schema)
+ XCTAssertEqual("space_123", entity["id"] as? String)
+ XCTAssertEqual(uuid.uuidString, entity["view_id"] as? String)
+ XCTAssertEqual("automatic", entity["immersion_style"] as? String)
+ XCTAssertEqual("visible", entity["upper_limb_visibility"] as? String)
+ }
+
+ func testBuildsWindowGroupEntity() {
+ let windows = WindowGroupEntity(
+ id: "group_id",
+ windowId: uuid,
+ titleKey: "title",
+ windowStyle: .plain
+ )
+ let entity = windows.data
+
+ XCTAssertEqual(swiftuiWindowGroupSchema, windows.schema)
+ XCTAssertEqual("group_id", entity["id"] as? String)
+ XCTAssertEqual(uuid.uuidString, entity["window_id"] as? String)
+ XCTAssertEqual("title", entity["title_key"] as? String)
+ XCTAssertEqual("plain", entity["window_style"] as? String)
+ }
+}
diff --git a/Tests/VisionOS/TestVisionOSEvents.swift b/Tests/VisionOS/TestVisionOSEvents.swift
new file mode 100644
index 000000000..28032a92c
--- /dev/null
+++ b/Tests/VisionOS/TestVisionOSEvents.swift
@@ -0,0 +1,155 @@
+// Copyright (c) 2013-present 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 XCTest
+@testable import SnowplowTracker
+
+class TestVisionOSEvents: XCTestCase {
+ var eventSink: EventSink?
+ var trackedEvents: [InspectableEvent] { return eventSink?.trackedEvents ?? [] }
+ var tracker: TrackerController?
+
+ override func setUp() {
+ tracker = createTracker()
+ }
+
+ override func tearDown() {
+ Snowplow.removeAllTrackers()
+ eventSink = nil
+ }
+
+ func testTrackOpenWindow() {
+ let event = OpenWindowEvent(
+ id: "group_id"
+ )
+
+ _ = tracker?.track(event)
+ waitForEventsToBeTracked()
+
+ XCTAssertEqual(1, trackedEvents.count)
+ XCTAssertEqual(swiftuiOpenWindowSchema, event.schema)
+
+ let entities = trackedEvents[0].entities
+ XCTAssertEqual(1, getWindowGroupEntities(entities).count)
+ }
+
+ func testTrackDismissWindow() {
+ let event = DismissWindowEvent(
+ id: "window",
+ windowStyle: .automatic
+ )
+
+ _ = tracker?.track(event)
+ waitForEventsToBeTracked()
+
+ XCTAssertEqual(1, trackedEvents.count)
+ XCTAssertEqual(swiftuiDismissWindowSchema, event.schema)
+
+ let entities = trackedEvents[0].entities
+ XCTAssertEqual(1, getWindowGroupEntities(entities).count)
+ }
+
+ func testTrackOpenImmersiveSpace() {
+ let event = OpenImmersiveSpaceEvent(
+ id: "space",
+ immersionStyle: .full
+ )
+
+ _ = tracker?.track(event)
+ waitForEventsToBeTracked()
+
+ XCTAssertEqual(1, trackedEvents.count)
+ XCTAssertEqual(swiftuiOpenImmersiveSpaceSchema, event.schema)
+
+ let entities = trackedEvents[0].entities
+ XCTAssertEqual(1, getImmersiveSpaceEntities(entities).count)
+
+ let spaceEntity = getImmersiveSpaceEntities(entities)[0] as? ImmersiveSpaceEntity
+ let viewId = spaceEntity!.viewId
+ XCTAssertNotNil(viewId)
+ }
+
+ func testTrackDismissImmersiveSpace() {
+ let event = DismissImmersiveSpaceEvent()
+
+ _ = tracker?.track(event)
+ waitForEventsToBeTracked()
+
+ XCTAssertEqual(1, trackedEvents.count)
+ XCTAssertEqual(swiftuiDismissImmersiveSpaceSchema, event.schema)
+
+ let entities = trackedEvents[0].entities
+ XCTAssertEqual(0, getImmersiveSpaceEntities(entities).count)
+ }
+
+ func testImmersiveSpaceEntityAddedByDefault() {
+ let event = OpenImmersiveSpaceEvent(id: "space")
+
+ _ = tracker?.track(event)
+ waitForEventsToBeTracked()
+
+ let event2 = ScreenView(name: "screen")
+
+ _ = tracker?.track(event2)
+ waitForEventsToBeTracked()
+
+ XCTAssertEqual(2, trackedEvents.count)
+
+ let entities = trackedEvents[1].entities
+ XCTAssertEqual(1, getImmersiveSpaceEntities(entities).count)
+ }
+
+ private func createTracker() -> TrackerController {
+ let networkConfig = NetworkConfiguration(networkConnection: MockNetworkConnection(requestOption: .post, statusCode: 200))
+ let trackerConfig = TrackerConfiguration()
+
+ let namespace = "testVisionOS" + String(describing: Int.random(in: 0..<100))
+ eventSink = EventSink()
+
+ return Snowplow.createTracker(namespace: namespace,
+ network: networkConfig,
+ 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
+ expect.fulfill()
+ }
+ wait(for: [expect], timeout: 1)
+ }
+
+ private func getWindowGroupEntities(_ all: [SelfDescribingJson]?) -> [SelfDescribingJson] {
+ var entities: [SelfDescribingJson] = []
+ if let all = all {
+ for entity in all {
+ if (entity.schema == swiftuiWindowGroupSchema) {
+ entities.append(entity)
+ }
+ }
+ }
+ return entities
+ }
+
+ private func getImmersiveSpaceEntities(_ all: [SelfDescribingJson]?) -> [SelfDescribingJson] {
+ var entities: [SelfDescribingJson] = []
+ if let all = all {
+ for entity in all {
+ if (entity.schema == swiftuiImmersiveSpaceSchema) {
+ entities.append(entity)
+ }
+ }
+ }
+ return entities
+ }
+}
diff --git a/VERSION b/VERSION
index 1bc788d3b..09b254e90 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-5.6.0
+6.0.0